9f1da9a00d
Changed the setting registration, removed the warnings. Loot roll messages are more obvious now. Superadmins are meant to stay mostily hidden, but are being exposed in !system again.
422 lines
19 KiB
Python
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 Tyrbot:
|
|
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 = "3"
|
|
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
|