import inspect import time from threading import Thread from core.decorators import instance from core.functions import get_attrs from core.logger import Logger from core.registry import Registry @instance() class EventService: def __init__(self): self.handlers = {} self.logger = Logger(__name__) self.event_types = [] self.db_cache = {} def inject(self, registry): self.db = registry.get_instance("db") self.bot = registry.get_instance("bot") self.util = registry.get_instance("util") def pre_start(self): self.register_event_type("timer") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "event"): key = Registry.get_module_name(inst).split(".") # We dont want to load events, if their modules not enabled in our config... if key[0] not in self.bot.modules: continue attrs = getattr(method, "event") handler = getattr(inst, name) self.register(handler, attrs.event_type, attrs.description, inst.module_name, attrs.is_hidden, attrs.is_enabled) def register_event_type(self, event_type): """ Call during pre_start Args: event_type (str) """ event_type = event_type.lower() if event_type in self.event_types: self.logger.error("Could not register event type '%s': event type already registered" % event_type) return self.logger.debug("Registering event type '%s'" % event_type) self.event_types.append(event_type) def is_event_type(self, event_base_type): return event_base_type in self.event_types def register(self, handler, event_type, description, module, is_hidden, is_enabled): """ Call during pre_start Args: handler: (event_type, event_data) -> void event_type: str description: str module: str is_hidden: bool is_enabled: bool """ if len(inspect.signature(handler).parameters) != 2: raise Exception( f"Incorrect number of arguments for handler '{handler.__module__}.{handler.__name__}()'") event_base_type, event_sub_type = self.get_event_type_parts(event_type) module = module.lower() handler_name = self.util.get_handler_name(handler) is_hidden = 1 if is_hidden else 0 is_enabled = 1 if is_enabled else 0 if event_base_type not in self.event_types: self.logger.error( f"Could not register handler '{handler_name}' for event type '{event_type}': event type does not exist") return if not description: self.logger.warning(f"No description for event_type '{event_type}' and handler '{handler_name}'") row = self.db.query_single("SELECT 1 FROM event_config WHERE event_type = ? AND handler = ?", [event_base_type, handler_name]) if row is None: # add new event commands self.db.exec( "INSERT INTO event_config (event_type, event_sub_type, handler, " "description, module, enabled, verified, is_hidden) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [event_base_type, event_sub_type, handler_name, description, module, is_enabled, 1, is_hidden]) if event_base_type == "timer": self.db.exec( "INSERT INTO timer_event (event_type, event_sub_type, handler, next_run) VALUES (?, ?, ?, ?)", [event_base_type, event_sub_type, handler_name, int(time.time())]) else: # mark command as verified self.db.exec( "UPDATE event_config SET verified = ?, module = ?, description = ?, event_sub_type = ?, is_hidden = ? " "WHERE event_type = ? AND handler = ?", [1, module, description, event_sub_type, is_hidden, event_base_type, handler_name]) if event_base_type == "timer": self.db.exec("UPDATE timer_event SET event_sub_type = ? WHERE event_type = ? AND handler = ?", [event_sub_type, event_base_type, handler_name]) # load command handler self.handlers[handler_name] = handler def fire_event(self, event_type, event_data=None): event_base_type, event_sub_type = self.get_event_type_parts(event_type) if event_base_type not in self.event_types: self.logger.error(f"Could not fire event type '{event_type}': event type does not exist") return data = self.get_handlers(event_base_type, event_sub_type) # We dont want to spawn a new Thread for each event handler, but only per event type. def i(): for row in data: if event_type != "connect": self.call_handler(row.handler, event_type, event_data) else: self.call_handler(row.handler, event_type, event_data) if event_type != "connect": Thread(target=i, daemon=True).start() else: i() def call_handler(self, handler_method, event_type, event_data): handler = self.handlers.get(handler_method, None) if not handler: self.logger.error( f"Could not find handler callback for event type '{event_type}' and handler '{handler_method}'") return try: handler(event_type, event_data) except Exception as e: self.logger.error(f"error processing event '{event_type}'", e) def get_event_type_parts(self, event_type): parts = event_type.lower().split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_event_type_key(self, event_base_type, event_sub_type): return event_base_type + ":" + event_sub_type def check_for_timer_events(self, current_timestamp): data = self.db.query("SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t " "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler " "WHERE t.next_run <= ? AND e.enabled = 1", [current_timestamp]) for row in data: self.execute_timed_event(row, current_timestamp) def execute_timed_event(self, row, current_timestamp): event_type_key = self.get_event_type_key(row.event_type, row.event_sub_type) # timer event run times should be consistent, so we base the next run time off the last run time, # instead of the current timestamp next_run = row.next_run + int(row.event_sub_type) # prevents timer events from getting too far behind, or having a large "catch-up" after # the bot has been offline for a time if next_run < current_timestamp: next_run = current_timestamp + int(row.event_sub_type) self.db.exec("UPDATE timer_event SET next_run = ? WHERE event_type = ? AND handler = ?", [next_run, row.event_type, row.handler]) self.call_handler(row.handler, event_type_key, None) def update_event_status(self, event_base_type, event_sub_type, event_handler, enabled_status): # clear cache self.db_cache[event_base_type + ":" + event_sub_type] = None return self.db.exec( "UPDATE event_config SET enabled = ? WHERE event_type = ? AND event_sub_type = ? AND handler LIKE ?", [enabled_status, event_base_type, event_sub_type, event_handler]) def get_event_types(self): return self.event_types def get_handlers(self, event_base_type, event_sub_type): # check first in cache result = self.db_cache.get(event_base_type + ":" + event_sub_type, None) if result is not None: return result else: result = self.db.query( "SELECT handler FROM event_config WHERE event_type = ? AND event_sub_type = ? AND enabled = 1", [event_base_type, event_sub_type]) # store result in cache self.db_cache[event_base_type + ":" + event_sub_type] = result return result def run_timer_events_at_startup(self): t = int(time.time()) data = self.db.query("SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t " "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler " "WHERE e.event_type = ? AND e.enabled = 1", ["timer"]) for row in data: handler = self.handlers[row.handler] attrs = getattr(handler, "event") if attrs.get("run_at_startup", False): self.execute_timed_event(row, t)