c04f76c0db
Fixed command & event threading Events are now threaded by event_type (i.e. all buddy_logon events get ran in the same one) Added default preferences Fixed recipe loading for multiple installs (i.e. on different machines)
219 lines
9.0 KiB
Python
219 lines
9.0 KiB
Python
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)
|