Compare commits

...

4 Commits

3
.gitignore vendored

@ -137,3 +137,6 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Project specific files:
.sync_token

@ -1,23 +0,0 @@
aiofiles==0.6.0
aiohttp==3.7.4.post0
aiohttp-socks==0.6.0
async-timeout==3.0.1
attrs==21.2.0
chardet==4.0.0
future==0.18.2
h11==0.12.0
h2==4.0.0
hpack==4.0.0
hyperframe==6.0.1
idna==3.2
jsonschema==3.2.0
Logbook==1.5.3
matrix-nio==0.18.7
multidict==5.1.0
pycryptodome==3.10.4
pyrsistent==0.18.0
python-socks==1.2.4
six==1.16.0
typing-extensions==3.10.0.2
unpaddedbase64==2.1.0
yarl==1.6.3

@ -0,0 +1,2 @@
matrix-nio==0.18.7
aiopath==0.5.12

@ -0,0 +1 @@
python-dotenv==0.19.0

@ -11,4 +11,12 @@ packages = matrix_bot
python_requires = >=3.9.2
package_dir = =src
install_requires =
matrix-nio>=0.18.7
matrix-nio>=0.18.7
aiopath>=0.5.12
[options.extras_require]
testing =
python-dotenv>=0.19.0
[options.package_data]
matrix_bot = py.typed

@ -0,0 +1,17 @@
"""
Some utilities to help with async stuff.
"""
class Aobject(object):
"""
Inheriting this class allows you to define an async __init__.
So you can create objects by doing something like `await MyClass(params)`
Copied from https://newbedev.com/how-to-set-class-attribute-with-await-in-init
"""
async def __new__(cls, *a, **kw):
instance = super().__new__(cls)
await instance.__init__(*a, **kw)
return instance
async def __init__(self):
pass

@ -5,18 +5,20 @@ Connect to the matrix server and handle interactions with the server.
import asyncio
import nio
from aiopath import AsyncPath
from typing import (
Optional,
NoReturn,
Union
)
from .async_utils import Aobject
from .utils import (
Room,
RoomAlias,
RoomId
)
class Client:
class Client(Aobject):
"""
Connect to the matrix server and handle interactions with the
server.
@ -24,17 +26,25 @@ class Client:
allowed_rooms: dict of the rooms where the bot is allowed to connect, indexed
by id (the name starting with '!'). If set to None, the bot connect to
all room where it is invited.
/!\ The client is initialized asyncronously: `client = await Client(...)`
"""
__client: nio.AsyncClient
__rooms_by_aliases: dict[RoomAlias, Room]
__rooms_by_id: dict[RoomId, Room]
__sync_token_file: AsyncPath
__sync_token:Optional[str]
__sync_token_queue:asyncio.Queue[str]
allowed_rooms: Optional[dict[RoomId, Room]]
def __init__(
async def __init__(
self,
username: str,
homeserver: str,
password: str,
allowed_rooms_names: Optional[list[Union[RoomAlias, RoomId]]]=None
allowed_rooms_names: Optional[list[Union[RoomAlias, RoomId]]]=None,
sync_token_file:str="./.sync_token",
):
"""
Initialize the Client.
@ -44,27 +54,33 @@ class Client:
allowed_rooms: the list of the rooms where the bot is allowed to connect
(given by room id (expl: '!xxx:matrix.org') of room alias (expl:
'#xxx:matrix.org'))
sync_token_file: the file where is stored the last sync token received from
the sync responses. This token avoid reloadind all the history of the
bot each time we start it.
"""
loop = asyncio.get_event_loop()
self.__client = nio.AsyncClient(
homeserver,
username
)
resp = loop.run_until_complete(self.__client.login(password))
self.__rooms_by_aliases = {}
self.__rooms_by_id = {}
self.__sync_token_file = AsyncPath(sync_token_file)
if (await self.__sync_token_file.is_file()):
async with self.__sync_token_file.open() as file:
self.__sync_token = (await file.read()).strip()
else:
self.__sync_token = None
self.__sync_token_queue = asyncio.Queue()
resp = await self.__client.login(password)
if isinstance(resp, nio.responses.LoginError):
raise RuntimeError(f"Fail to connect: {resp.message}")
# Where is the async map when you need it?
self.allowed_rooms = None
if allowed_rooms_names:
self.allowed_rooms = {}
for room_name in allowed_rooms_names:
room = loop.run_until_complete(self.resolve_room(room_name))
if room.id in self.allowed_rooms:
self.allowed_rooms[room.id].aliases.update(room.aliases)
else:
self.allowed_rooms[room.id] = room
rooms = await asyncio.gather(*(self.resolve_room(room_name) for room_name in allowed_rooms_names))
for room in rooms:
self.allowed_rooms[room.id] = room # room uniqueness is handled by self.resolve_room
async def resolve_room(
self,
@ -72,16 +88,82 @@ class Client:
)->Room:
"""
Lookup a room from its id or alias.
If the name has already been resolved by this client, the
room is return directly without querying the server.
"""
# If the room_name is empty:
if len(room_name) == 0:
raise ValueError(f"Invalid room_name: {room_name}")
if room_name[0] == '!':
return Room(id=room_name)
# If it is a known room id:
if room_name[0] == '!' and room_name in self.__rooms_by_id:
return self.__rooms_by_id[room_name]
# If it is a unknown room id:
elif room_name[0] == '!':
room = Room(id=room_name)
self.__rooms_by_id[room_name] = room
return room
# If it is not a room id nor a room alias:
elif room_name[0] != '#':
raise ValueError(f"Invalid room_name: {room_name}")
# If it is a known room alias:
elif room_name in self.__rooms_by_aliases:
return self.__rooms_by_aliases[room_name]
# If it is an unknown room alias:
else:
resp = await self.__client.room_resolve_alias(room_name)
if isinstance(resp, nio.responses.RoomResolveAliasError):
raise RuntimeError(f"Error while resolving alias: {resp.message}")
return Room(id=resp.room_id,aliases={room_name})
# If the room is already known:
if resp.room_id in self.__rooms_by_id:
room = self.__rooms_by_id[resp.room_id]
room.aliases.add(room_name)
# If the room is unknwon:
else:
room = Room(id=resp.room_id,aliases={room_name})
self.__rooms_by_id[resp.room_id] = room
self.__rooms_by_aliases[room_name] = room
return room
async def __save_sync_tocken(self)->NoReturn:
"""
Save the sync token.
"""
while True:
token = await self.__sync_token_queue.get()
async with self.__sync_token_file.open(mode='w') as file:
await file.write(token)
self.__sync_token_queue.task_done()
async def __sync(
self,
sync_timeout:int=30
)->NoReturn:
"""
Sync with the server every sync_delta seconds.
sync_timeout: the max time in sec between each sync.
"""
while True:
sync_resp = await self.__client.sync(
sync_delta*1000,
since=self.__sync_token
)
if isinstance(sync_resp, nio.responses.SyncError):
print(f"Error while syncronizing: {sync_resp.message}") # TODO: use proper logging
continue
self.__sync_token = sync_resp.next_batch
await self.__sync_token_queue.put(self.__sync_token)
async def run(
self,
sync_timeout:int=30
)->NoReturn:
"""
Run the bot: sync with the server and execute callbacks.
sync_timeout: the max time in sec between each sync.
"""
await asyncio.gather(
self.__sync(sync_delta=sync_delta),
self.__save_sync_tocken()
)

@ -2,18 +2,25 @@
Not really tests, because I don't know how to testt without a whole matrix server.
"""
import asyncio
import os
from dotenv import load_dotenv
from matrix_bot.client import Client
from getpass import getpass
if __name__ == "__main__":
async def main():
load_dotenv()
client = Client(
client = await Client(
os.environ["MUSER"],
os.environ["HOMESERVER"],
os.environ["PASSWD"],
os.environ["ROOMS"].split(",")
)
print(client.allowed_rooms)
await client.run()
if __name__ == "__main__":
asyncio.run(main())

Loading…
Cancel
Save