Files
igncore/core/igncore.py
T
Minidodo 810c2c8c4d Fix for mrelay - some bots are sending malformed messages, caused by wrong usage of their modules.
Accessing an external service through the bot for gathering tower data is nolonger supported; and should be done via external scripts.
Fix for callers, and missing alias'es for loot tables.
!accounts will only show mains now.
the character order of all alt lists has been reversed: [main] high => low instead of [main] low => high
!account add <name> also marks accounts as type 0, if an account gets re-enabled. might cause strange behaviour with member-logs, if used in onlinebots.
Member type is being displayed in !account now. [Member (X)]
2021-09-19 14:09:44 +02:00

422 lines
19 KiB
Python

import inspect
import threading
import time
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.lookup.character_service import CharacterService
from core.public_channel_service import PublicChannelService
from core.setting_service import SettingService
from core.text import Text
from modules.core.accounting.services.access_service import AccessService
@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.6"
self.minor_version = "5"
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 = 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) ENGINE MEMORY")
# 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)
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())
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):
# self.logger.log_tell('spam', 'To', self.character_service.get_char_name(char_id), msg)
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:
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 log_message:
self.logger.log_tell("spam", "To", 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):
# self.logger.log_tell(conn.id, "From", 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_char_id(self):
return self.conns["main"].char_id