Create basic Bot functionality

Add decorator for adding listeners to the bot object
master
DustyP 6 years ago
parent d8f53a35be
commit 20629229c0

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Union, Optional, Dict from typing import Union, Optional, Dict, List
from .api import API from .api import API
from .room import Room from .room import Room
@ -30,10 +30,16 @@ class Client:
"rooms": self.process_room_events, "rooms": self.process_room_events,
"groups": self.process_group_events, "groups": self.process_group_events,
} }
self.event_dispatchers: Dict[str, callable] = {} self.event_dispatchers: Dict[str, List[callable]] = {}
self.users = [] 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: if not password and not token:
raise RuntimeError("Either the password or a token is required") raise RuntimeError("Either the password or a token is required")
self.user_id = user_id self.user_id = user_id
@ -95,9 +101,10 @@ class Client:
event_dict["room"] = room event_dict["room"] = room
event = self.process_event(event_dict) event = self.process_event(event_dict)
await room.update_state(event) await room.update_state(event)
handler = self.event_dispatchers.get(event.type) handlers = self.event_dispatchers.get(event.type)
if handler: if handlers:
await self.invoke(handler, event) for handler in handlers:
self.loop.create_task(self.invoke(handler, event))
# Process ephemeral events # Process ephemeral events
for event in data['ephemeral']['events']: for event in data['ephemeral']['events']:
@ -118,9 +125,10 @@ class Client:
if event not in room.message_cache: if event not in room.message_cache:
room.message_cache.append(event) room.message_cache.append(event)
if room.read_receipts[self.user_id][1] < event.origin_server_ts: if room.read_receipts[self.user_id][1] < event.origin_server_ts:
handler = self.event_dispatchers.get(event.type) handlers = self.event_dispatchers.get(event.type)
if handler: if handlers:
await self.invoke(handler, event) for handler in handlers:
self.loop.create_task(self.invoke(handler, event))
try: try:
await self.mark_event_read(event) await self.mark_event_read(event)
except RuntimeError as e: except RuntimeError as e:
@ -161,9 +169,15 @@ class Client:
await handler(event) await handler(event)
def register_handler(self, event_type, handler: callable): def register_handler(self, event_type, handler: callable):
if not event_type:
event_type = handler.__name__.replace('_', '.')
if not callable(handler): if not callable(handler):
raise TypeError(f'handler must be a callable not {type(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'): async def mark_event_read(self, event, receipt_type: str = 'm.read'):
from .events import RoomEvent from .events import RoomEvent

@ -1,5 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, List, Dict from typing import Optional, List, Dict
from inspect import isawaitable
@dataclass @dataclass
@ -97,5 +98,13 @@ class MessageRelation:
event_id: str 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(): def notification_power_levels_default_factory():
return {'room': 50} return {'room': 50}

@ -3,24 +3,68 @@ from typing import Union, Optional, Dict
from morpheus.core.client import Client from morpheus.core.client import Client
from morpheus.core.room import Room 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 from .context import Context
class Bot(Client): class Bot(Client):
def __init__( def __init__(
self, self,
prefix: Union[str, list, tuple], prefix: Union[str, list, tuple, callable],
homeserver: str = "https://matrixcoding.chat", homeserver: str = "https://matrixcoding.chat",
): ):
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
super(Bot, self).__init__(prefix=prefix, homeserver=homeserver) super(Bot, self).__init__(prefix=prefix, homeserver=homeserver)
def run(self, user_id: str = None, password: str = None, token: str = None): def run(self, user_id: str = None, password: str = None, token: str = None, loop: Optional[asyncio.AbstractEventLoop] = None):
loop = self.loop or asyncio.get_event_loop() loop = loop or self.loop or asyncio.get_event_loop()
loop.run_until_complete(super(Bot, self).run(user_id, password, token)) 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): 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

@ -1,6 +1,23 @@
from morpheus.core.client import Client from morpheus.core.client import Client
from morpheus.core.room import Room from morpheus.core.room import Room
from morpheus.core.events import RoomEvent
from morpheus.core.content import ContentBase
class Context: class Context:
def __init__(self, client: Client, room: Room, prefix: str, sender: str, ): def __init__(self, client: Client, room: Room, calling_prefix: str, sender: str, event: RoomEvent, content: ContentBase, called_with: str, body: str):
self.client: Client 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)

Loading…
Cancel
Save