From 45855fbe4f3cb8e42f0aa7af7e3244d570140c2f Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Mon, 20 Jan 2025 01:06:13 -0600 Subject: [PATCH] add event system --- lxmfy/__init__.py | 4 ++ lxmfy/config.py | 12 ++++- lxmfy/core.py | 65 +++++++++++++++++----- lxmfy/events.py | 125 +++++++++++++++++++++++++++++++++++++++++++ lxmfy/permissions.py | 8 ++- 5 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 lxmfy/events.py diff --git a/lxmfy/__init__.py b/lxmfy/__init__.py index 72c1d02..0c9746b 100644 --- a/lxmfy/__init__.py +++ b/lxmfy/__init__.py @@ -13,6 +13,7 @@ from .permissions import DefaultPerms, Role, PermissionManager from .validation import validate_bot, format_validation_results from .config import BotConfig +from .events import Event, EventManager, EventPriority __all__ = [ "LXMFBot", @@ -30,4 +31,7 @@ "validate_bot", "format_validation_results", "BotConfig", + "Event", + "EventManager", + "EventPriority", ] diff --git a/lxmfy/config.py b/lxmfy/config.py index ed842dc..55dcc1a 100644 --- a/lxmfy/config.py +++ b/lxmfy/config.py @@ -20,4 +20,14 @@ class BotConfig: permissions_enabled: bool = False storage_type: str = "json" storage_path: str = "data" - first_message_enabled: bool = True \ No newline at end of file + first_message_enabled: bool = True + event_logging_enabled: bool = True + max_logged_events: int = 1000 + event_middleware_enabled: bool = True + + def __post_init__(self): + if self.admins is None: + self.admins = set() + + def __str__(self): + return f"BotConfig(name={self.name}, announce={self.announce}, announce_immediately={self.announce_immediately}, admins={self.admins}, hot_reloading={self.hot_reloading}, rate_limit={self.rate_limit}, cooldown={self.cooldown}, max_warnings={self.max_warnings}, warning_timeout={self.warning_timeout}, command_prefix={self.command_prefix}, cogs_dir={self.cogs_dir}, permissions_enabled={self.permissions_enabled}, storage_type={self.storage_type}, storage_path={self.storage_path}, first_message_enabled={self.first_message_enabled}, event_logging_enabled={self.event_logging_enabled}, max_logged_events={self.max_logged_events}, event_middleware_enabled={self.event_middleware_enabled})" \ No newline at end of file diff --git a/lxmfy/core.py b/lxmfy/core.py index bed346c..8073736 100644 --- a/lxmfy/core.py +++ b/lxmfy/core.py @@ -16,6 +16,7 @@ from queue import Queue from types import SimpleNamespace from typing import Optional, Dict +import asyncio # Reticulum and LXMF imports import RNS @@ -30,6 +31,7 @@ from .permissions import PermissionManager, DefaultPerms from .config import BotConfig from .validation import validate_bot, format_validation_results +from .events import EventManager, Event, EventPriority class LXMFBot: @@ -148,6 +150,12 @@ def __init__(self, **kwargs): self.first_message_handlers = [] self.first_message_enabled = kwargs.get("first_message_enabled", True) + # Initialize event system + self.events = EventManager(self.storage) + + # Register built-in events + self._register_builtin_events() + def command(self, *args, **kwargs): def decorator(func): if len(args) > 0: @@ -187,13 +195,42 @@ def add_cog(self, cog): def is_admin(self, sender): return sender in self.admins + def _register_builtin_events(self): + """Register built-in event handlers""" + @self.events.on("message_received", EventPriority.HIGHEST) + def handle_message(event): + message = event.data["message"] + sender = event.data["sender"] + + # Check spam protection + if not self.permissions.has_permission(sender, DefaultPerms.BYPASS_SPAM): + allowed, msg = self.spam_protection.check_spam(sender) + if not allowed: + event.cancel() + self.send(sender, msg) + return + + # Process message + self._process_message(message, sender) + def _message_received(self, message): + """Handle received messages""" sender = RNS.hexrep(message.source_hash, delimit=False) receipt = RNS.hexrep(message.hash, delimit=False) - + if receipt in self.receipts: return - + + event = Event("message_received", { + "message": message, + "sender": sender, + "receipt": receipt + }) + + self.events.dispatch(event) + if event.cancelled: + return + # Check if this is user's first message is_first_message = not self.storage.exists(f"user:{sender}") if is_first_message: @@ -339,17 +376,19 @@ def send(self, destination, message, title="Reply"): self.queue.put(lxm) def run(self, delay=10): - RNS.log( - f"LXMF Bot `{self.local.display_name}` reporting for duty and awaiting messages...", - RNS.LOG_INFO, - ) - - while True: - for i in list(self.queue.queue): - lxm = self.queue.get() - self.router.handle_outbound(lxm) - self._announce() - time.sleep(delay) + """Run the bot""" + try: + while True: + # Process outbound queue + for i in list(self.queue.queue): + lxm = self.queue.get() + self.router.handle_outbound(lxm) + + self._announce() + time.sleep(delay) + + except KeyboardInterrupt: + self.transport.cleanup() def received(self, function): self.delivery_callbacks.append(function) diff --git a/lxmfy/events.py b/lxmfy/events.py new file mode 100644 index 0000000..cc88d21 --- /dev/null +++ b/lxmfy/events.py @@ -0,0 +1,125 @@ +"""Event system module for LXMFy. + +This module provides a comprehensive event handling system including: +- Custom event creation and dispatching +- Event middleware support +- Event logging and monitoring +""" + +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List +from enum import Enum +import logging +from datetime import datetime + +logger = logging.getLogger(__name__) + +class EventPriority(Enum): + """Priority levels for event handlers""" + LOWEST = 0 + LOW = 1 + NORMAL = 2 + HIGH = 3 + HIGHEST = 4 + MONITOR = 5 + +@dataclass +class Event: + """Base event class""" + name: str + data: Dict[str, Any] = field(default_factory=dict) + cancelled: bool = False + timestamp: datetime = field(default_factory=datetime.now) + + def cancel(self): + """Cancel the event""" + self.cancelled = True + +@dataclass +class EventHandler: + """Event handler container""" + callback: Callable + priority: EventPriority = EventPriority.NORMAL + middleware: List[Callable] = field(default_factory=list) + +class EventManager: + """Manages event registration, dispatching and middleware""" + + def __init__(self, storage=None): + self.handlers: Dict[str, List[EventHandler]] = {} + self.middleware: List[Callable] = [] + self.storage = storage + self.logger = logging.getLogger(__name__) + + def on(self, event_name: str, priority: EventPriority = EventPriority.NORMAL): + """Decorator to register an event handler""" + def decorator(func): + self.register_handler(event_name, func, priority) + return func + return decorator + + def register_handler(self, event_name: str, callback: Callable, + priority: EventPriority = EventPriority.NORMAL): + """Register an event handler""" + if event_name not in self.handlers: + self.handlers[event_name] = [] + + handler = EventHandler(callback=callback, priority=priority) + self.handlers[event_name].append(handler) + + # Sort handlers by priority + self.handlers[event_name].sort(key=lambda h: h.priority.value, reverse=True) + + def use(self, middleware: Callable): + """Add middleware to the event pipeline""" + self.middleware.append(middleware) + + def dispatch(self, event: Event) -> Event: + """Dispatch an event through middleware and to handlers""" + try: + # Run through middleware + for mw in self.middleware: + event = mw(event) + if event.cancelled: + return event + + if event.name in self.handlers: + for handler in self.handlers[event.name]: + try: + # Run handler middleware + for mw in handler.middleware: + event = mw(event) + if event.cancelled: + return event + + # Execute handler + handler.callback(event) + if event.cancelled: + break + + except Exception as e: + self.logger.error(f"Error in event handler: {str(e)}") + + # Log event if storage is configured + if self.storage: + self._log_event(event) + + return event + + except Exception as e: + self.logger.error(f"Error dispatching event: {str(e)}") + raise + + def _log_event(self, event: Event): + """Log event to storage""" + try: + events = self.storage.get("events:log", []) + events.append({ + "name": event.name, + "timestamp": event.timestamp.isoformat(), + "cancelled": event.cancelled, + "data": event.data + }) + self.storage.set("events:log", events[-1000:]) # Keep last 1000 events + except Exception as e: + self.logger.error(f"Error logging event: {str(e)}") \ No newline at end of file diff --git a/lxmfy/permissions.py b/lxmfy/permissions.py index 24a5f41..624a602 100644 --- a/lxmfy/permissions.py +++ b/lxmfy/permissions.py @@ -33,10 +33,16 @@ class DefaultPerms(Flag): BYPASS_SPAM = auto() VIEW_ADMIN_COMMANDS = auto() + # Event system permissions + VIEW_EVENTS = auto() + MANAGE_EVENTS = auto() + BYPASS_EVENT_CHECKS = auto() + # Combined permissions ALL = (USE_BOT | SEND_MESSAGES | USE_COMMANDS | MANAGE_MESSAGES | MANAGE_COMMANDS | MANAGE_USERS | - BYPASS_RATELIMIT | BYPASS_SPAM | VIEW_ADMIN_COMMANDS) + BYPASS_RATELIMIT | BYPASS_SPAM | VIEW_ADMIN_COMMANDS | + VIEW_EVENTS | MANAGE_EVENTS | BYPASS_EVENT_CHECKS) @dataclass