import inspect import threading import time import typing from conf.config import BotConfig from core.aochat import server_packets, client_packets from core.aochat.extended_message import ExtendedMessage from core.bot_status import BotStatus from core.chat_blob import ChatBlob from core.conn import Conn from core.db import DB from core.decorators import instance from core.dict_object import DictObject from core.fifo_queue import FifoQueue from core.job_scheduler import JobScheduler from core.logger import Logger from core.text import Text from core.setting_types import BooleanSettingType if typing.TYPE_CHECKING: from core.lookup.character_service import CharacterService from core.public_channel_service import PublicChannelService from core.setting_service import SettingService from modules.core.accounting.services.access_service import AccessService from core.event_service import EventService @instance("bot") class IgnCore: CONNECT_EVENT = "connect" PACKET_EVENT = "packet" PRIVATE_MSG_EVENT = "private_msg" OUTGOING_ORG_MESSAGE_EVENT = "outgoing_org_message" OUTGOING_PRIVATE_MESSAGE_EVENT = "outgoing_private_message" OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT = "outgoing_private_channel_message" def __init__(self): super().__init__() self.logger = Logger(__name__) self.ready = False self.packet_handlers = {} self.superadmin = [] self.status: BotStatus = BotStatus.SHUTDOWN self.dimension = None self.last_timer_event = 0 self.start_time = int(time.time()) self.major_version = "IGNCore v2.9" self.minor_version = "8" self.incoming_queue = FifoQueue() self.mass_message_queue = None self.conns = DictObject() def inject(self, registry): self.db = registry.get_instance("db") self.character_service: CharacterService = registry.get_instance("character_service") self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") self.text: Text = registry.get_instance("text") self.setting_service: SettingService = registry.get_instance("setting_service") self.access_service: AccessService = registry.get_instance("access_service") self.event_service: EventService = registry.get_instance("event_service") self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler") self.command_service = registry.get_instance("command_service") def init(self, config: BotConfig, registry, mmdb_parser): self.mmdb_parser = mmdb_parser self.dimension = config.server.dimension self.name = config.character self.superadmin = config.superadmin self.modules = [x.split("/")[1] for x in config.module_paths] self.db.exec("CREATE TABLE IF NOT EXISTS command_config (" "command VARCHAR(50) NOT NULL, " "sub_command VARCHAR(50) NOT NULL, " "access_level VARCHAR(50) NOT NULL, " "channel VARCHAR(50) NOT NULL, " "module VARCHAR(50) NOT NULL, " "enabled SMALLINT NOT NULL, " "verified SMALLINT NOT NULL, " "PRIMARY KEY (`command`, `sub_command`, `access_level`, `channel`) USING BTREE)") self.db.exec("CREATE TABLE IF NOT EXISTS event_config (" "event_type VARCHAR(50) NOT NULL, " "event_sub_type VARCHAR(50) NOT NULL, " "handler VARCHAR(255) NOT NULL, " "description VARCHAR(255) NOT NULL, " "module VARCHAR(50) NOT NULL, " "enabled SMALLINT NOT NULL, " "verified SMALLINT NOT NULL, " "is_hidden SMALLINT NOT NULL, " "PRIMARY KEY (`handler`) USING BTREE, " "INDEX `event_type` (`event_type`) USING BTREE, " "INDEX `event_sub_type` (`event_sub_type`) USING BTREE)") self.db.exec("CREATE TABLE IF NOT EXISTS timer_event (" "event_type VARCHAR(50) NOT NULL, " "event_sub_type VARCHAR(50) NOT NULL, " "handler VARCHAR(255) NOT NULL, " "next_run INT NOT NULL, " "INDEX `next_run` (`next_run`) USING BTREE)") self.db.exec("CREATE TABLE IF NOT EXISTS setting (" "name VARCHAR(50) NOT NULL, " "value VARCHAR(255) NOT NULL, " "description VARCHAR(255) NOT NULL, " "module VARCHAR(50) NOT NULL, " "verified SMALLINT NOT NULL, " "PRIMARY KEY (`name`) USING BTREE)") self.db.exec("CREATE TABLE IF NOT EXISTS command_alias (" "alias VARCHAR(50) NOT NULL, " "command VARCHAR(1024) NOT NULL, " "enabled SMALLINT NOT NULL, " "INDEX `alias` (`alias`) USING BTREE, " "INDEX `command` (`command`) USING BTREE)") self.db.exec("CREATE TABLE IF NOT EXISTS command_usage (" "command VARCHAR(255) NOT NULL, " "handler VARCHAR(255) NOT NULL, " "char_id INT NOT NULL, " "channel VARCHAR(20) NOT NULL, " "created_at INT NOT NULL, " "INDEX `command` (`command`) USING BTREE, " "INDEX `char_id` (`char_id`) USING BTREE, " "INDEX `channel` (`channel`) USING BTREE)") # self.db.exec("UPDATE db_version SET verified = 0") self.db.exec("UPDATE db_version SET verified = 1 WHERE file = 'db_version'") # prepare commands, events, and settings self.db.exec("UPDATE command_config SET verified = 0 where 1") self.db.exec("UPDATE event_config SET verified = 0 where 1") self.db.exec("UPDATE setting SET verified = 0 where 1") # load modules registry.pre_start_all() registry.start_all() if self.db.shared != self.db: self.db: DB self.db.shared.pool.close() # Creates a Exception for some reason?? # self.db.shared = None # remove commands, events, and settings that are no longer registered # self.db.exec("DELETE FROM db_version WHERE verified = 0") self.db.exec("DELETE FROM command_config WHERE verified = 0") self.db.exec("DELETE FROM event_config WHERE verified = 0") self.db.exec("DELETE FROM timer_event WHERE handler NOT IN " "(SELECT handler FROM event_config WHERE event_type = ?)", ["timer"]) self.db.exec("DELETE FROM setting WHERE verified = 0") self.status = BotStatus.RUN def pre_start(self): self.event_service.register_event_type(self.CONNECT_EVENT) self.event_service.register_event_type(self.PACKET_EVENT) self.event_service.register_event_type(self.PRIVATE_MSG_EVENT) self.event_service.register_event_type(self.OUTGOING_ORG_MESSAGE_EVENT) self.event_service.register_event_type(self.OUTGOING_PRIVATE_MESSAGE_EVENT) self.event_service.register_event_type(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT) def start(self): self.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message, priority=40) self.setting_service.register("core.logging", "log_tells", "false", BooleanSettingType(), "Should tells get logged to file") self.setting_service.register("core.logging", "log_priv", "false", BooleanSettingType(), "Should the private channel get logged to file") self.setting_service.register("core.logging", "log_org", "false", BooleanSettingType(), "Should the org channel get logged to file") def connect(self, config): conn = self.create_conn("main") conn.connect(config.server.host, config.server.port) packet = conn.login(config.username, config.password, config.character) if not packet: self.status = BotStatus.ERROR return False else: self.incoming_queue.put((conn, packet)) self.create_conn_thread(conn) if hasattr(config, 'slaves'): self.mass_message_queue = FifoQueue() for i, slave in enumerate(config.slaves): conn = self.create_conn("slave" + str(i)) conn.connect(config.server.host, config.server.port) packet = conn.login(slave.username, slave.password, slave.character) if not packet: self.status = BotStatus.ERROR return False else: self.incoming_queue.put((conn, packet)) self.create_conn_thread(conn, self.mass_message_queue) return True def create_conn_thread(self, conn: Conn, mass_message_queue=None): def read_packets(): try: while self.status == BotStatus.RUN: packet = conn.read_packet() if packet: self.incoming_queue.put((conn, packet)) while mass_message_queue and not mass_message_queue.empty() and conn.packet_queue.is_empty(): packet = mass_message_queue.get_or_default(block=False) if packet: conn.add_packet_to_queue(packet) except (EOFError, OSError) as e: self.status = BotStatus.ERROR self.logger.error("", e) raise e dthread = threading.Thread(name=conn.char_name, target=read_packets, daemon=True) dthread.start() def create_conn(self, _id): if _id in self.conns: raise Exception(f"A connection with id {_id} already exists") def failure_callback(): self.status = BotStatus.ERROR conn = Conn(_id, failure_callback) self.conns[_id] = conn return conn # passthrough def send_packet(self, packet): self.conns["main"].send_packet(packet) def disconnect(self): # wait for all threads to stop reading packets, then disconnect them all time.sleep(2) for _id, conn in self.conns.items(): conn.disconnect() def run(self): start = time.time() # wait for flood of packets from login to stop sending time_waited = 0 while time_waited < 5: if not self.iterate(1): time_waited += 1 self.logger.info(f"Login complete ({time.time() - start:.2f}s)") start = time.time() self.event_service.fire_event("connect", None) self.event_service.run_timer_events_at_startup() self.logger.info(f"Connect events finished ({time.time() - start:.2f}s)") self.ready = True self.command_service.ignore = [] timestamp = int(time.time()) while self.status == BotStatus.RUN: try: timestamp = int(time.time()) self.check_for_timer_events(timestamp) self.iterate() except Exception as e: self.logger.error("", e) # run any pending jobs/events self.check_for_timer_events(timestamp + 1) return self.status def check_for_timer_events(self, timestamp): # timer events will execute no more often than once per second if self.last_timer_event < timestamp: self.last_timer_event = timestamp self.job_scheduler.check_for_scheduled_jobs(timestamp) self.event_service.check_for_timer_events(timestamp) def register_packet_handler(self, packet_id: int, handler, priority=50): """ Call during pre_start Args: packet_id: int handler: (conn, packet) -> void priority: int """ if len(inspect.signature(handler).parameters) != 2: raise Exception( f"Incorrect number of arguments for handler '{handler.__module__}.{handler.__name__}()'") handlers = self.packet_handlers.get(packet_id, []) handlers.append(DictObject({"priority": priority, "handler": handler})) self.packet_handlers[packet_id] = sorted(handlers, key=lambda x: x.priority) def remove_packet_handler(self, packet_id, handler): handlers = self.packet_handlers.get(packet_id, []) for h in handlers: if h.handler == handler: handlers.remove(h) def iterate(self, timeout=0.1): conn, packet = self.incoming_queue.get_or_default(timeout=timeout, default=(None, None)) if packet: if isinstance(packet, server_packets.SystemMessage): packet = self.system_message_ext_msg_handling(packet) self.logger.log_chat(conn.id, "SystemMessage", None, packet.extended_message.get_message()) elif isinstance(packet, server_packets.PublicChannelMessage): packet = self.public_channel_message_ext_msg_handling(packet) if isinstance(packet, server_packets.BuddyAdded): if packet.char_id == 0: return for handler in self.packet_handlers.get(packet.id, []): handler.handler(conn, packet) self.event_service.fire_event("packet:" + str(packet.id), packet) return packet def public_channel_message_ext_msg_handling(self, packet: server_packets.PublicChannelMessage): msg = packet.message if msg.startswith("~&") and msg.endswith("~"): try: msg = msg[2:-1].encode("utf-8") category_id = self.mmdb_parser.read_base_85(msg[0:5]) instance_id = self.mmdb_parser.read_base_85(msg[5: 10]) template = self.mmdb_parser.get_message_string(category_id, instance_id) params = self.mmdb_parser.parse_params(msg[10:]) packet.extended_message = ExtendedMessage(category_id, instance_id, template, params) except Exception as e: self.logger.error("Error handling extended message for packet: " + str(packet), e) return packet def system_message_ext_msg_handling(self, packet: server_packets.SystemMessage): try: category_id = 20000 instance_id = packet.message_id template = self.mmdb_parser.get_message_string(category_id, instance_id) params = self.mmdb_parser.parse_params(packet.message_args) packet.extended_message = ExtendedMessage(category_id, instance_id, template, params) except Exception as e: self.logger.error("Error handling extended message: " + str(packet), e) return packet def send_org_message(self, msg, add_color=True, fire_outgoing_event=True, conn_id="main"): org_channel_id = self.public_channel_service.org_channel_id if org_channel_id is None: self.logger.debug("ignoring message to org channel since the org_channel_id is unknown") else: color = self.setting_service.get("org_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("org_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PublicChannelMessage(org_channel_id, color + page, "") self.conns[conn_id].add_packet_to_queue(packet) if fire_outgoing_event: self.event_service.fire_event(self.OUTGOING_ORG_MESSAGE_EVENT, DictObject({"org_channel_id": org_channel_id, "message": msg})) def send_private_message(self, char_id, msg, add_color=True, fire_outgoing_event=True, conn_id="main"): if char_id is None: raise Exception("Cannot send message, char_id is empty") else: color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) if self.setting_service.get_value("log_tells") == "1": if type(msg) == ChatBlob: self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), f"[link]{msg.title}[/link]") else: self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg) for page in pages: # self.logger.log_tell(conn_id, "To", self.character_service.get_char_name(char_id), page) packet = client_packets.PrivateMessage(char_id, color + page, "\0") self.conns[conn_id].add_packet_to_queue(packet) if fire_outgoing_event: self.event_service.fire_event(self.OUTGOING_PRIVATE_MESSAGE_EVENT, DictObject({"char_id": char_id, "message": msg})) def send_private_channel_message(self, msg, private_channel_id=None, add_color=True, fire_outgoing_event=True, conn_id="main"): if private_channel_id is None: private_channel_id = self.get_char_id() color = self.setting_service.get("private_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PrivateChannelMessage(private_channel_id, color + page, "\0") self.conns[conn_id].send_packet(packet) if fire_outgoing_event and private_channel_id == self.get_char_id(): self.event_service.fire_event(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT, DictObject({"private_channel_id": private_channel_id, "message": msg})) def send_mass_message(self, char_id, msg, add_color=True, log_message=False): if not char_id: self.logger.warning("Could not send message to empty char_id") if len(self.conns.items()) == 1: self.send_private_message(char_id, msg, add_color, log_message) else: if self.setting_service.get_value("log_tells") == "1": if type(msg) == ChatBlob: self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), f"[link]{msg.title}[/link]") else: self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg) color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) for page in pages: # if self.log_mass_tell().get_value(): # self.logger.log_tell("spam", "->", self.character_service.get_char_name(char_id), page) if self.mass_message_queue: packet = client_packets.PrivateMessage(char_id, color + page, "\0") self.mass_message_queue.put(packet) else: packet = client_packets.PrivateMessage(char_id, color + page, "spam") self.conns["main"].send_packet(packet) def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage): if self.setting_service.get_value("log_tells") == "1": self.logger.log_tell(conn.id, "<-", self.character_service.get_char_name(packet.char_id), packet.message) self.event_service.fire_event(self.PRIVATE_MSG_EVENT, packet) def get_text_pages(self, msg, max_page_length): if isinstance(msg, ChatBlob): return self.text.paginate(msg, max_page_length=max_page_length) else: return [self.text.format_message(msg)] def is_ready(self): return self.ready def shutdown(self): self.status = BotStatus.SHUTDOWN def restart(self): self.status = BotStatus.RESTART def get_char_name(self): return self.conns["main"].char_name def get_conn(self): return self.conns["main"] def get_char_id(self): return self.conns["main"].char_id