moment/src/python/matrix_client.py

179 lines
5.7 KiB
Python

import asyncio
import inspect
import logging as log
import platform
from contextlib import suppress
from datetime import datetime
from types import ModuleType
from typing import Dict, Optional, Type
import nio
from nio.rooms import MatrixRoom
from . import __about__
from .events import rooms, users
from .events.rooms_timeline import EventType, HtmlMessageReceived
from .html_filter import HTML_FILTER
class MatrixClient(nio.AsyncClient):
def __init__(self,
user: str,
homeserver: str = "https://matrix.org",
device_id: Optional[str] = None) -> None:
# TODO: ensure homeserver starts with a scheme://
self.sync_task: Optional[asyncio.Future] = None
super().__init__(homeserver=homeserver, user=user, device_id=device_id)
self.connect_callbacks()
def __repr__(self) -> str:
return "%s(user_id=%r, homeserver=%r, device_id=%r)" % (
type(self).__name__, self.user_id, self.homeserver, self.device_id
)
@staticmethod
def _classes_defined_in(module: ModuleType) -> Dict[str, Type]:
return {
m[0]: m[1] for m in inspect.getmembers(module, inspect.isclass)
if not m[0].startswith("_") and
m[1].__module__.startswith(module.__name__)
}
def connect_callbacks(self) -> None:
for name, class_ in self._classes_defined_in(nio.responses).items():
with suppress(AttributeError):
self.add_response_callback(getattr(self, f"on{name}"), class_)
for name, class_ in self._classes_defined_in(nio.events).items():
with suppress(AttributeError):
self.add_event_callback(getattr(self, f"on{name}"), class_)
async def start_syncing(self) -> None:
self.sync_task = asyncio.ensure_future(
self.sync_forever(timeout=10_000)
)
def callback(task):
raise task.exception()
self.sync_task.add_done_callback(callback)
@property
def default_device_name(self) -> str:
os_ = f" on {platform.system()}".rstrip()
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""
return f"{__about__.__pretty_name__}{os_}"
async def login(self, password: str, device_name: str = "") -> None:
response = await super().login(
password, device_name or self.default_device_name
)
if isinstance(response, nio.LoginError):
print(response)
else:
await self.start_syncing()
async def resume(self, user_id: str, token: str, device_id: str) -> None:
response = nio.LoginResponse(user_id, device_id, token)
await self.receive_response(response)
await self.start_syncing()
async def logout(self) -> None:
if self.sync_task:
self.sync_task.cancel()
with suppress(asyncio.CancelledError):
await self.sync_task
await self.close()
async def request_user_update_event(self, user_id: str) -> None:
response = await self.get_profile(user_id)
if isinstance(response, nio.ProfileGetError):
log.warning("Error getting profile for %r: %s", user_id, response)
users.UserUpdated(
user_id = user_id,
display_name = getattr(response, "displayname", None),
avatar_url = getattr(response, "avatar_url", None),
status_message = None, # TODO
)
# Callbacks for nio responses
@staticmethod
def _get_room_name(room: MatrixRoom) -> Optional[str]:
# FIXME: reimplanted because of nio's non-standard room.display_name
name = room.name or room.canonical_alias
if name:
return name
name = room.group_name()
return None if name == "Empty room?" else name
async def onSyncResponse(self, resp: nio.SyncResponse) -> None:
for room_id, _ in resp.rooms.invite.items():
room: MatrixRoom = self.invited_rooms[room_id]
rooms.RoomUpdated(
user_id = self.user_id,
category = "Invites",
room_id = room_id,
display_name = self._get_room_name(room),
avatar_url = room.gen_avatar_url,
topic = room.topic,
inviter = room.inviter,
)
for room_id, _ in resp.rooms.join.items():
room = self.rooms[room_id]
rooms.RoomUpdated(
user_id = self.user_id,
category = "Rooms",
room_id = room_id,
display_name = self._get_room_name(room),
avatar_url = room.gen_avatar_url,
topic = room.topic,
)
for room_id, _ in resp.rooms.leave.items():
rooms.RoomUpdated(
user_id = self.user_id,
category = "Left",
room_id = room_id,
# left_event TODO
)
# Callbacks for nio events
async def onRoomMessageText(self, room: MatrixRoom, ev: nio.RoomMessageText
) -> None:
is_html = ev.format == "org.matrix.custom.html"
filter_ = HTML_FILTER.filter
HtmlMessageReceived(
type = EventType.html if is_html else EventType.text,
room_id = room.room_id,
event_id = ev.event_id,
sender_id = ev.sender,
date = datetime.fromtimestamp(ev.server_timestamp / 1000),
is_local_echo = False,
content = filter_(ev.formatted_body) if is_html else ev.body,
)