Rename tyrbot.py -> igncore.py
This commit is contained in:
+421
@@ -0,0 +1,421 @@
|
||||
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 = "1"
|
||||
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
|
||||
Reference in New Issue
Block a user