From 20629229c0c4d853a8b87f667fb8e267820aa18d Mon Sep 17 00:00:00 2001 From: Dusty Pianalto Date: Fri, 1 Nov 2019 23:46:23 -0800 Subject: [PATCH] Create basic Bot functionality Add decorator for adding listeners to the bot object --- morpheus/core/client.py | 34 +++++++++++++++++-------- morpheus/core/utils.py | 9 +++++++ morpheus/exts/bot.py | 54 ++++++++++++++++++++++++++++++++++++---- morpheus/exts/context.py | 21 ++++++++++++++-- 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/morpheus/core/client.py b/morpheus/core/client.py index 5295a59..a8f70f0 100644 --- a/morpheus/core/client.py +++ b/morpheus/core/client.py @@ -1,5 +1,5 @@ import asyncio -from typing import Union, Optional, Dict +from typing import Union, Optional, Dict, List from .api import API from .room import Room @@ -30,10 +30,16 @@ class Client: "rooms": self.process_room_events, "groups": self.process_group_events, } - self.event_dispatchers: Dict[str, callable] = {} + self.event_dispatchers: Dict[str, List[callable]] = {} self.users = [] + self.loop: Optional[asyncio.AbstractEventLoop] = None + + async def run(self, user_id: str = None, password: str = None, token: str = None, loop: Optional[asyncio.AbstractEventLoop] = None): + if loop: + self.loop = loop + elif not self.loop: + self.loop = asyncio.get_event_loop() - async def run(self, user_id: str = None, password: str = None, token: str = None): if not password and not token: raise RuntimeError("Either the password or a token is required") self.user_id = user_id @@ -95,9 +101,10 @@ class Client: event_dict["room"] = room event = self.process_event(event_dict) await room.update_state(event) - handler = self.event_dispatchers.get(event.type) - if handler: - await self.invoke(handler, event) + handlers = self.event_dispatchers.get(event.type) + if handlers: + for handler in handlers: + self.loop.create_task(self.invoke(handler, event)) # Process ephemeral events for event in data['ephemeral']['events']: @@ -118,9 +125,10 @@ class Client: if event not in room.message_cache: room.message_cache.append(event) if room.read_receipts[self.user_id][1] < event.origin_server_ts: - handler = self.event_dispatchers.get(event.type) - if handler: - await self.invoke(handler, event) + handlers = self.event_dispatchers.get(event.type) + if handlers: + for handler in handlers: + self.loop.create_task(self.invoke(handler, event)) try: await self.mark_event_read(event) except RuntimeError as e: @@ -161,9 +169,15 @@ class Client: await handler(event) def register_handler(self, event_type, handler: callable): + if not event_type: + event_type = handler.__name__.replace('_', '.') + if not callable(handler): raise TypeError(f'handler must be a callable not {type(handler)}') - self.event_dispatchers[event_type] = handler + if event_type in self.event_dispatchers: + self.event_dispatchers[event_type].append(handler) + else: + self.event_dispatchers[event_type] = [handler] async def mark_event_read(self, event, receipt_type: str = 'm.read'): from .events import RoomEvent diff --git a/morpheus/core/utils.py b/morpheus/core/utils.py index dc678b7..038e48b 100644 --- a/morpheus/core/utils.py +++ b/morpheus/core/utils.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional, List, Dict +from inspect import isawaitable @dataclass @@ -97,5 +98,13 @@ class MessageRelation: event_id: str +async def maybe_coroutine(func, *args, **kwargs): + f = func(*args, **kwargs) + if isawaitable(f): + return await f + else: + return f + + def notification_power_levels_default_factory(): return {'room': 50} diff --git a/morpheus/exts/bot.py b/morpheus/exts/bot.py index 1f78938..8aeca4f 100644 --- a/morpheus/exts/bot.py +++ b/morpheus/exts/bot.py @@ -3,24 +3,68 @@ from typing import Union, Optional, Dict from morpheus.core.client import Client from morpheus.core.room import Room +from morpheus.core.utils import maybe_coroutine +from morpheus.core.events import RoomEvent +from morpheus.core.content import MessageContentBase from .context import Context class Bot(Client): def __init__( self, - prefix: Union[str, list, tuple], + prefix: Union[str, list, tuple, callable], homeserver: str = "https://matrixcoding.chat", ): self.loop = asyncio.get_event_loop() super(Bot, self).__init__(prefix=prefix, homeserver=homeserver) - def run(self, user_id: str = None, password: str = None, token: str = None): - loop = self.loop or asyncio.get_event_loop() - loop.run_until_complete(super(Bot, self).run(user_id, password, token)) + def run(self, user_id: str = None, password: str = None, token: str = None, loop: Optional[asyncio.AbstractEventLoop] = None): + loop = loop or self.loop or asyncio.get_event_loop() + loop.run_until_complete(super(Bot, self).run(user_id, password, token, loop=loop)) - async def get_context(self, event): + async def get_context(self, event: RoomEvent): + if not isinstance(event.content, MessageContentBase): + return None + if callable(self.prefix): + prefix = await maybe_coroutine(self.prefix, event) + elif isinstance(self.prefix, (str, list, tuple)): + prefix = self.prefix + else: + raise RuntimeError('Prefix must be a string, list of strings or callable') + + if isinstance(prefix, str): + return self._get_context(event, prefix) + elif isinstance(prefix, (list, tuple)): + prefixes = tuple(prefix) + for prefix in prefixes: + try: + ctx = self._get_context(event, prefix) + if ctx: + return ctx + except TypeError: + raise RuntimeError('Prefix must be a string or list of strings') + else: + return None + else: + raise RuntimeError('Prefix must be a string or list of strings') + + @staticmethod + def _get_context(event: RoomEvent, prefix: str): + if not isinstance(event.content, MessageContentBase): + return None + + raw_body = event.content.body + if not raw_body.startswith(prefix): + return None + raw_body = raw_body.lstrip(prefix) + called_with, body = raw_body.split(' ', 1) + return Context.get_context(event, prefix, called_with, body) async def check_event(self, event): + ctx = await self.get_context(event) + def listener(self, name=None): + def decorator(func): + self.register_handler(name, func) + return decorator diff --git a/morpheus/exts/context.py b/morpheus/exts/context.py index 9bd86a5..e999d66 100644 --- a/morpheus/exts/context.py +++ b/morpheus/exts/context.py @@ -1,6 +1,23 @@ from morpheus.core.client import Client from morpheus.core.room import Room +from morpheus.core.events import RoomEvent +from morpheus.core.content import ContentBase + class Context: - def __init__(self, client: Client, room: Room, prefix: str, sender: str, ): - self.client: Client + def __init__(self, client: Client, room: Room, calling_prefix: str, sender: str, event: RoomEvent, content: ContentBase, called_with: str, body: str): + self.client: Client = client + self.room: Room = room + self.calling_prefix: str = calling_prefix + self.sender: str = sender # TODO once the User class is created change this to type User + self.event: RoomEvent = event + self.content: ContentBase = content + self.called_with: str = called_with + self.body: str = body + + async def send_text(self, body: str, formatted_body: str = None, format_type: str = 'org.matrix.custom.html'): + await self.client.send_text(self.room, body, formatted_body, format_type) + + @classmethod + def get_context(cls, event: RoomEvent, calling_prefix: str, called_with: str, body: str): + return cls(event.client, event.room, calling_prefix, event.sender, event, event.content, called_with, body)