parent
4d4f97dbce
commit
2eee29e5e8
@ -0,0 +1,203 @@
|
|||||||
|
import json
|
||||||
|
from typing import Union
|
||||||
|
import uuid
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp.client_exceptions import ClientConnectionError
|
||||||
|
import asyncio
|
||||||
|
from urllib.parse import quote, urlencode, urlparse
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
MATRIX_API = "/_matrix/client/r0"
|
||||||
|
MATRIX_MEDIA = "/_matrix/media/r0"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class APIConfig:
|
||||||
|
max_retry: int = 10
|
||||||
|
max_wait_time: int = 3600
|
||||||
|
backoff_factor: float = 0.1
|
||||||
|
ssl: bool = None
|
||||||
|
proxy: str = None
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
base_url: str,
|
||||||
|
user_id: str,
|
||||||
|
password: str = None,
|
||||||
|
token: str = None,
|
||||||
|
device_id: str = None,
|
||||||
|
device_name: str = None,
|
||||||
|
config: APIConfig = APIConfig(),
|
||||||
|
):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.user_id = user_id
|
||||||
|
self.password = password
|
||||||
|
self.token = token
|
||||||
|
self.device_id = device_id
|
||||||
|
self.device_name = device_name
|
||||||
|
self.access_token = None
|
||||||
|
self.config = config
|
||||||
|
self.client_session = aiohttp.ClientSession()
|
||||||
|
|
||||||
|
def build_url(
|
||||||
|
self, endpoint: str, request_type: str = None, query: dict = None
|
||||||
|
) -> str:
|
||||||
|
path = f'{MATRIX_MEDIA if request_type == "MEDIA" else MATRIX_API}/{endpoint}'
|
||||||
|
path = self.base_url + quote(path)
|
||||||
|
if query:
|
||||||
|
path += f"?{urlencode(query).lower()}"
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_wait_time(self, num_timeouts: int) -> float:
|
||||||
|
if num_timeouts <= 2:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return min(
|
||||||
|
self.config.backoff_factor * (2 ** (num_timeouts - 1)),
|
||||||
|
self.config.max_wait_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self.client_session:
|
||||||
|
await self.client_session.close()
|
||||||
|
self.client_session = None
|
||||||
|
|
||||||
|
async def _send(
|
||||||
|
self, method: str, path: str, data: dict = None, headers: dict = {}
|
||||||
|
) -> Union[dict, bytes]:
|
||||||
|
if not self.client_session:
|
||||||
|
self.client_session = aiohttp.ClientSession()
|
||||||
|
|
||||||
|
raw_resp = await self.client_session.request(
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
json=data,
|
||||||
|
ssl=self.config.ssl,
|
||||||
|
proxy=self.config.proxy,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if raw_resp.content_type == "application/json":
|
||||||
|
return await raw_resp.json()
|
||||||
|
else:
|
||||||
|
return await raw_resp.read()
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self, method: str, path: str, data: dict = None, content_type: str = None
|
||||||
|
) -> dict:
|
||||||
|
if not self.access_token:
|
||||||
|
raise RuntimeError("Client is not logged in")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"content_type": content_type or "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
timeouts = 0
|
||||||
|
|
||||||
|
for _ in range(self.config.max_retry or 1):
|
||||||
|
try:
|
||||||
|
resp = await self._send(method, path, data, headers)
|
||||||
|
|
||||||
|
if isinstance(resp, bytes):
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(resp, dict) and resp.get("retry_after_ms"):
|
||||||
|
await asyncio.sleep(resp["retry_after_ms"] / 1000)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except (asyncio.TimeoutError, ClientConnectionError, TimeoutError):
|
||||||
|
timeouts += 1
|
||||||
|
await asyncio.sleep(self.get_wait_time(timeouts))
|
||||||
|
else:
|
||||||
|
raise RuntimeWarning(f"Max retries reached for {method} - {path} | {data}")
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def login(self):
|
||||||
|
path = self.build_url("login")
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if self.password and self.user_id:
|
||||||
|
data = {
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": {"user": self.user_id, "type": "m.id.user"},
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
elif self.token:
|
||||||
|
data = {"type": "m.login.token", "token": self.token}
|
||||||
|
else:
|
||||||
|
raise RuntimeError("No valid login types configured")
|
||||||
|
if self.device_id:
|
||||||
|
data["device_id"] = self.device_id
|
||||||
|
if self.device_name:
|
||||||
|
data["device_name"] = self.device_name
|
||||||
|
|
||||||
|
headers = {"content_type": "application/json"}
|
||||||
|
resp = await self._send("post", path, data=data, headers=headers)
|
||||||
|
self.access_token = resp.get("access_token")
|
||||||
|
self.device_id = resp.get("device_id")
|
||||||
|
if not self.user_id:
|
||||||
|
self.user_id = resp.get("user_id")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def logout(self):
|
||||||
|
path = self.build_url("logout")
|
||||||
|
await self.send("POST", path)
|
||||||
|
self.access_token = None
|
||||||
|
|
||||||
|
async def logout_all(self):
|
||||||
|
path = self.build_url("logout/all")
|
||||||
|
await self.send("POST", path)
|
||||||
|
self.access_token = None
|
||||||
|
|
||||||
|
async def room_send(self, room_id: str, event_type: str, content: dict):
|
||||||
|
if room_id.startswith("!") and ":" in room_id:
|
||||||
|
path = self.build_url(f"rooms/{room_id}/send/{event_type}/{uuid.uuid4()}")
|
||||||
|
elif room_id.startswith("#") and ":" in room_id:
|
||||||
|
path = self.build_url(f"directory/room/{room_id}")
|
||||||
|
resp = await self.send("GET", path)
|
||||||
|
if resp.get("room_id"):
|
||||||
|
path = self.build_url(
|
||||||
|
f'rooms/{resp["room_id"]}/send/{event_type}/{uuid.uuid4()}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeWarning(resp)
|
||||||
|
else:
|
||||||
|
raise RuntimeWarning(f"{room_id} is not a valid room id or alias")
|
||||||
|
|
||||||
|
return await self.send("PUT", path, data=content)
|
||||||
|
|
||||||
|
async def get_joined_rooms(self):
|
||||||
|
path = self.build_url("joined_rooms")
|
||||||
|
resp = await self.send("GET", path)
|
||||||
|
if resp.get("joined_rooms"):
|
||||||
|
return resp["joined_rooms"]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_sync(
|
||||||
|
self,
|
||||||
|
query_filter: str = None,
|
||||||
|
since: str = None,
|
||||||
|
full_state: bool = False,
|
||||||
|
set_presence: str = "online",
|
||||||
|
timeout: int = 10000,
|
||||||
|
):
|
||||||
|
query = {
|
||||||
|
"full_state": full_state,
|
||||||
|
"set_presence": set_presence,
|
||||||
|
"timeout": timeout,
|
||||||
|
}
|
||||||
|
if query_filter:
|
||||||
|
query["filter"] = query_filter
|
||||||
|
if since:
|
||||||
|
query["since"] = since
|
||||||
|
|
||||||
|
path = self.build_url("sync", query=query)
|
||||||
|
print(path)
|
||||||
|
resp = await self.send("GET", path)
|
||||||
|
|
||||||
|
return resp
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Union, Optional, Dict
|
||||||
|
|
||||||
|
from .api import API, APIConfig
|
||||||
|
from .room import Room
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix: Union[str, list, tuple],
|
||||||
|
homeserver: str = "https://matrixcoding.chat",
|
||||||
|
):
|
||||||
|
self.prefix = prefix
|
||||||
|
self.homeserver = homeserver
|
||||||
|
self.user_id: Optional[str] = None
|
||||||
|
self.password: Optional[str] = None
|
||||||
|
self.token: Optional[str] = None
|
||||||
|
self.rooms: Dict[str, Room] = {}
|
||||||
|
self.api: Optional[API] = None
|
||||||
|
self.running: bool = False
|
||||||
|
self.sync_timeout: int = 1000
|
||||||
|
self.sync_since: Optional[str] = None
|
||||||
|
self.sync_full_state: bool = False
|
||||||
|
self.sync_set_presence: str = "online"
|
||||||
|
self.sync_filter: Optional[str] = None
|
||||||
|
self.sync_delay: Optional[str] = None
|
||||||
|
self.sync_process_dispatcher = {
|
||||||
|
"presence": self.process_presence_events,
|
||||||
|
"rooms": self.process_room_events,
|
||||||
|
"groups": self.process_group_events,
|
||||||
|
}
|
||||||
|
self.event_dispatchers: Dict[str, callable] = {}
|
||||||
|
self.users = []
|
||||||
|
|
||||||
|
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
|
||||||
|
self.password = password
|
||||||
|
self.token = token
|
||||||
|
self.api = API(
|
||||||
|
base_url=self.homeserver, user_id=self.user_id, password=self.password, token=self.token
|
||||||
|
)
|
||||||
|
resp = await self.api.login()
|
||||||
|
if resp.get("errcode"):
|
||||||
|
raise RuntimeError(resp)
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
await self.sync()
|
||||||
|
if self.sync_delay:
|
||||||
|
await asyncio.sleep(self.sync_delay)
|
||||||
|
|
||||||
|
async def sync(self):
|
||||||
|
resp = await self.api.get_sync(
|
||||||
|
self.sync_filter,
|
||||||
|
self.sync_since,
|
||||||
|
self.sync_full_state,
|
||||||
|
self.sync_set_presence,
|
||||||
|
self.sync_timeout,
|
||||||
|
)
|
||||||
|
if resp.get("errcode"):
|
||||||
|
self.running = False
|
||||||
|
raise RuntimeError(resp)
|
||||||
|
self.sync_since = resp["next_batch"]
|
||||||
|
for key, value in resp.items():
|
||||||
|
if key == "next_batch":
|
||||||
|
self.sync_since = value
|
||||||
|
else:
|
||||||
|
if key in self.sync_process_dispatcher:
|
||||||
|
func = self.sync_process_dispatcher[key]
|
||||||
|
await func(value)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def process_presence_events(self, value: dict):
|
||||||
|
events = value["events"]
|
||||||
|
for event_dict in events:
|
||||||
|
event = self.process_event(event_dict)
|
||||||
|
# TODO Do something with presence event...
|
||||||
|
|
||||||
|
async def process_room_events(self, value: dict):
|
||||||
|
await self.process_room_join_events(value["join"])
|
||||||
|
await self.process_room_invite_events(value["invite"])
|
||||||
|
await self.process_room_leave_events(value["leave"])
|
||||||
|
|
||||||
|
async def process_room_join_events(self, rooms: dict):
|
||||||
|
from morpheus.core.events import StateEvent, MessageEvent
|
||||||
|
for room_id, data in rooms.items():
|
||||||
|
if room_id not in self.rooms:
|
||||||
|
self.rooms[room_id] = Room(room_id, self)
|
||||||
|
room = self.rooms[room_id]
|
||||||
|
|
||||||
|
# Process state events and update Room state
|
||||||
|
for event_dict in data["state"]["events"]:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Process timeline
|
||||||
|
for event_dict in data["timeline"]["events"]:
|
||||||
|
event_dict["room"] = room
|
||||||
|
event = self.process_event(event_dict)
|
||||||
|
if isinstance(event, StateEvent):
|
||||||
|
await room.update_state(event)
|
||||||
|
elif isinstance(event, MessageEvent):
|
||||||
|
if event not in room.message_cache:
|
||||||
|
room.message_cache.append(event)
|
||||||
|
handler = self.event_dispatchers.get(event.type)
|
||||||
|
if handler:
|
||||||
|
await self.invoke(handler, event)
|
||||||
|
|
||||||
|
# Process ephemeral events
|
||||||
|
for event in data['ephemeral']['events']:
|
||||||
|
if event['type'] == 'm.receipt':
|
||||||
|
room.update_read_receipts(event['content'])
|
||||||
|
# TODO Update read receipts for users
|
||||||
|
elif event['type'] == 'm.typing':
|
||||||
|
# TODO process typing messages
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def process_room_invite_events(self, rooms: dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def process_room_leave_events(self, rooms: dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def process_group_events(self, value: dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_event(self, event: dict):
|
||||||
|
from .events import (
|
||||||
|
EventBase,
|
||||||
|
RoomEvent,
|
||||||
|
StateEvent,
|
||||||
|
RedactionEvent,
|
||||||
|
MessageEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.get("redacted"):
|
||||||
|
return RedactionEvent.from_dict(self, event)
|
||||||
|
elif event.get("state_key") is not None:
|
||||||
|
return StateEvent.from_dict(self, event)
|
||||||
|
elif event["type"] == "m.presence":
|
||||||
|
return EventBase.from_dict(self, event)
|
||||||
|
elif event["type"] == "m.room.message":
|
||||||
|
return MessageEvent.from_dict(self, event)
|
||||||
|
else:
|
||||||
|
return RoomEvent.from_dict(self, event)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def invoke(handler: callable, event):
|
||||||
|
# handler must be a callable which takes the event as an argument
|
||||||
|
await handler(event)
|
||||||
|
|
||||||
|
def register_handler(self, event_type, handler: callable):
|
||||||
|
if not callable(handler):
|
||||||
|
raise TypeError(f'handler must be a callable not {type(handler)}')
|
||||||
|
self.event_dispatchers[event_type] = handler
|
||||||
@ -0,0 +1,213 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
|
from .utils import (
|
||||||
|
EncryptedFile,
|
||||||
|
ImageInfo,
|
||||||
|
FileInfo,
|
||||||
|
AudioInfo,
|
||||||
|
VideoInfo,
|
||||||
|
LocationInfo,
|
||||||
|
PreviousRoom,
|
||||||
|
Invite,
|
||||||
|
ReactionRelation,
|
||||||
|
notification_power_levels_default_factory
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContentBase:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageContentBase(ContentBase):
|
||||||
|
body: str
|
||||||
|
msgtype: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MTextContent(MessageContentBase):
|
||||||
|
format: Optional[str] = None
|
||||||
|
formatted_body: Optional[str] = None
|
||||||
|
msgtype = "m.text"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MEmoteContent(MTextContent):
|
||||||
|
msgtype = "m.emote"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MNoticeContent(MTextContent):
|
||||||
|
msgtype = "m.notice"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MImageContent(MessageContentBase):
|
||||||
|
msgtype = "m.image"
|
||||||
|
info: ImageInfo
|
||||||
|
url: Optional[str] = None
|
||||||
|
file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MFileContent(MessageContentBase):
|
||||||
|
msgtype = "m.file"
|
||||||
|
filename: str
|
||||||
|
info: FileInfo
|
||||||
|
url: Optional[str] = None
|
||||||
|
file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MAudioContent(MessageContentBase):
|
||||||
|
msgtype = "m.audio"
|
||||||
|
info: AudioInfo
|
||||||
|
url: Optional[str] = None
|
||||||
|
file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MLocationContent(MessageContentBase):
|
||||||
|
msgtype = "m.location"
|
||||||
|
geo_uri: str
|
||||||
|
info: LocationInfo
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MVideoContent(MessageContentBase):
|
||||||
|
msgtype = "m.video"
|
||||||
|
info: VideoInfo
|
||||||
|
url: Optional[str] = None
|
||||||
|
file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PresenceContent(ContentBase):
|
||||||
|
presence: str
|
||||||
|
last_active_ago: int
|
||||||
|
currently_active: bool
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
displayname: Optional[str] = None
|
||||||
|
status_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomAliasesContent(ContentBase):
|
||||||
|
aliases: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomCanonicalAliasContent(ContentBase):
|
||||||
|
alias: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomCreateContent(ContentBase):
|
||||||
|
creator: str
|
||||||
|
room_version: Optional[str] = "1"
|
||||||
|
m_federate: Optional[bool] = True
|
||||||
|
predecessor: Optional[PreviousRoom] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomJoinRulesContent(ContentBase):
|
||||||
|
join_rule: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomMemberContent(ContentBase):
|
||||||
|
membership: str
|
||||||
|
is_direct: bool = False
|
||||||
|
third_party_invite: Optional[Invite] = None
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
displayname: str = None
|
||||||
|
inviter: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomPowerLevelsContent(ContentBase):
|
||||||
|
ban: int = 50
|
||||||
|
events: Dict[str, int] = field(default_factory=dict)
|
||||||
|
events_default: int = 0
|
||||||
|
invite: int = 50
|
||||||
|
kick: int = 50
|
||||||
|
redact: int = 50
|
||||||
|
state_default: int = 50
|
||||||
|
users: Dict[str, int] = field(default_factory=dict)
|
||||||
|
users_default: int = 0
|
||||||
|
notifications: Dict[str, int] = field(default_factory=notification_power_levels_default_factory)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomRedactionContent(ContentBase):
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomRelatedGroupsContent(ContentBase):
|
||||||
|
groups: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomTopicContent(ContentBase):
|
||||||
|
topic: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomNameContent(ContentBase):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomHistoryVisibilityContent(ContentBase):
|
||||||
|
history_visibility: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomBotOptionsContent(ContentBase):
|
||||||
|
options: Dict[str, dict]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MReactionContent(ContentBase):
|
||||||
|
relation: ReactionRelation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomAvatarContent(ContentBase):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MRoomGuestAccessContent(ContentBase):
|
||||||
|
guest_access: str
|
||||||
|
|
||||||
|
|
||||||
|
content_dispatcher = {
|
||||||
|
"m.text": MTextContent,
|
||||||
|
"m.audio": MAudioContent,
|
||||||
|
"m.emote": MEmoteContent,
|
||||||
|
"m.notice": MNoticeContent,
|
||||||
|
"m.image": MImageContent,
|
||||||
|
"m.file": MFileContent,
|
||||||
|
"m.location": MLocationContent,
|
||||||
|
"m.video": MVideoContent,
|
||||||
|
"m.presence": PresenceContent,
|
||||||
|
"m.room.aliases": MRoomAliasesContent,
|
||||||
|
"m.room.canonical_alias": MRoomCanonicalAliasContent,
|
||||||
|
"m.room.create": MRoomCreateContent,
|
||||||
|
"m.room.join_rules": MRoomJoinRulesContent,
|
||||||
|
"m.room.member": MRoomMemberContent,
|
||||||
|
"m.room.power_levels": MRoomPowerLevelsContent,
|
||||||
|
"m.room.redaction": MRoomRedactionContent,
|
||||||
|
"m.room.related_groups": MRoomRelatedGroupsContent,
|
||||||
|
"m.room.topic": MRoomTopicContent,
|
||||||
|
"m.room.name": MRoomNameContent,
|
||||||
|
"m.room.history_visibility": MRoomHistoryVisibilityContent,
|
||||||
|
"m.room.bot.options": MRoomBotOptionsContent,
|
||||||
|
'm.reaction': MReactionContent,
|
||||||
|
'm.room.avatar': MRoomAvatarContent,
|
||||||
|
'm.room.guest_access': MRoomGuestAccessContent,
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# TODO Create Context
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from .client import Client
|
||||||
|
from .room import Room
|
||||||
|
from .content import ContentBase
|
||||||
|
from .utils import ReactionRelation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EventBase:
|
||||||
|
client: Client
|
||||||
|
content: ContentBase
|
||||||
|
type: str
|
||||||
|
sender: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, client: Client, event_dict: dict):
|
||||||
|
from .content import content_dispatcher
|
||||||
|
if event_dict['type'] == 'm.room.message':
|
||||||
|
content_class = content_dispatcher[event_dict['content']['msgtype']]
|
||||||
|
else:
|
||||||
|
content_class = content_dispatcher[event_dict['type']]
|
||||||
|
|
||||||
|
if event_dict['type'] == 'm.reaction':
|
||||||
|
content_dict = {'relation': ReactionRelation(**event_dict['content']['m.relates_to'])}
|
||||||
|
elif event_dict['type'] == 'm.room.bot.options':
|
||||||
|
content_dict = {'options': event_dict['content']}
|
||||||
|
else:
|
||||||
|
content_dict = event_dict['content']
|
||||||
|
del event_dict['content']
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
client=client,
|
||||||
|
content=content_class(**content_dict),
|
||||||
|
**event_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnsignedData:
|
||||||
|
age: int
|
||||||
|
redacted_because: Optional[EventBase] = None
|
||||||
|
transaction_id: Optional[str] = None
|
||||||
|
invite_room_state: Optional[List[EventBase]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RoomEvent(EventBase):
|
||||||
|
event_id: str
|
||||||
|
origin_server_ts: int
|
||||||
|
unsigned: UnsignedData
|
||||||
|
room: Room
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StateEvent(RoomEvent):
|
||||||
|
state_key: str
|
||||||
|
age: int = None
|
||||||
|
prev_content: Optional[EventBase] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RedactionEvent(RoomEvent):
|
||||||
|
redacts: EventBase
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MessageEvent(RoomEvent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PresenceEvent(EventBase):
|
||||||
|
pass
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
# TODO Add Room class
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from .content import (
|
||||||
|
MRoomPowerLevelsContent,
|
||||||
|
MRoomAliasesContent,
|
||||||
|
MRoomBotOptionsContent,
|
||||||
|
MRoomCanonicalAliasContent,
|
||||||
|
MRoomCreateContent,
|
||||||
|
MRoomHistoryVisibilityContent,
|
||||||
|
MRoomJoinRulesContent,
|
||||||
|
MRoomNameContent,
|
||||||
|
MRoomRelatedGroupsContent,
|
||||||
|
MRoomTopicContent,
|
||||||
|
)
|
||||||
|
from .utils import PreviousRoom
|
||||||
|
|
||||||
|
|
||||||
|
class Room:
|
||||||
|
def __init__(self, room_id: str, client):
|
||||||
|
from .client import Client
|
||||||
|
|
||||||
|
self.id = room_id
|
||||||
|
self.client: Client = client
|
||||||
|
self.groups: Optional[List[str]] = None
|
||||||
|
self.topic: str = ""
|
||||||
|
self.join_rule: Optional[str] = None
|
||||||
|
self.version: int = 4
|
||||||
|
self.creator: Optional[str] = None
|
||||||
|
self.created_at: Optional[datetime] = None
|
||||||
|
self.name: Optional[str] = None
|
||||||
|
self.aliases: Optional[List[str]] = None
|
||||||
|
self.history_visibility: Optional[str] = None
|
||||||
|
self.avatar_url: str = ""
|
||||||
|
self.canonical_alias: Optional[str] = None
|
||||||
|
self.power_levels: Optional[MRoomPowerLevelsContent] = None
|
||||||
|
self.bot_options: Optional[Dict[str, dict]] = None
|
||||||
|
self.federated: bool = True
|
||||||
|
self.predecessor: Optional[PreviousRoom] = None
|
||||||
|
self.heroes: Optional[List[str]] = None
|
||||||
|
self.joined_member_count: Optional[int] = None
|
||||||
|
self.invited_member_count: Optional[int] = None
|
||||||
|
self.read_receipts: Dict[str, Dict[str, int]] = {}
|
||||||
|
self.message_cache = deque(maxlen=1000)
|
||||||
|
|
||||||
|
def update_read_receipts(self, receipts: Dict[str, Dict[str, Dict[str, Dict[str, int]]]]):
|
||||||
|
for event_id, receipt in receipts.items():
|
||||||
|
users = receipt['m.read']
|
||||||
|
for user, time in users.items():
|
||||||
|
self.read_receipts[user] = {event_id: time['ts']}
|
||||||
|
|
||||||
|
async def update_state(self, state_event=None):
|
||||||
|
from .events import StateEvent
|
||||||
|
|
||||||
|
if not state_event or state_event.room != self:
|
||||||
|
path = self.client.api.build_url(f"rooms/{self.id}/state")
|
||||||
|
state_events = await self.client.api.send("GET", path)
|
||||||
|
for state_event in state_events:
|
||||||
|
self._update_state(self.client.process_event(state_event))
|
||||||
|
else:
|
||||||
|
if not isinstance(state_event, StateEvent) or state_event.type == "m.room.member":
|
||||||
|
return
|
||||||
|
self._update_state(state_event)
|
||||||
|
|
||||||
|
def _update_state(self, event):
|
||||||
|
content = event.content
|
||||||
|
if isinstance(content, MRoomTopicContent):
|
||||||
|
self.topic = content.topic
|
||||||
|
elif isinstance(content, MRoomNameContent):
|
||||||
|
self.name = content.name
|
||||||
|
elif isinstance(content, MRoomRelatedGroupsContent):
|
||||||
|
self.groups = content.groups
|
||||||
|
elif isinstance(content, MRoomJoinRulesContent):
|
||||||
|
self.join_rule = content.join_rule
|
||||||
|
elif isinstance(content, MRoomHistoryVisibilityContent):
|
||||||
|
self.history_visibility = content.history_visibility
|
||||||
|
elif isinstance(content, MRoomCreateContent):
|
||||||
|
self.creator = content.creator
|
||||||
|
self.federated = content.m_federate
|
||||||
|
self.version = content.room_version
|
||||||
|
self.predecessor = content.predecessor
|
||||||
|
elif isinstance(content, MRoomCanonicalAliasContent):
|
||||||
|
self.canonical_alias = content.alias
|
||||||
|
elif isinstance(content, MRoomAliasesContent):
|
||||||
|
self.aliases = content.aliases
|
||||||
|
elif isinstance(content, MRoomBotOptionsContent):
|
||||||
|
self.bot_options = content.options
|
||||||
|
elif isinstance(content, MRoomPowerLevelsContent):
|
||||||
|
self.power_levels = content
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return other.__class__ == self.__class__ and other.id == self.id
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class JSONWebKey:
|
||||||
|
key_opts: List[str]
|
||||||
|
k: str
|
||||||
|
ext: bool = True
|
||||||
|
alg: str = "A256CTR"
|
||||||
|
kty: str = "oct"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EncryptedFile:
|
||||||
|
url: str
|
||||||
|
key: JSONWebKey
|
||||||
|
iv: str
|
||||||
|
hashes: Dict[str, str]
|
||||||
|
v: str = "v2"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ImageInfoBase:
|
||||||
|
h: int
|
||||||
|
w: int
|
||||||
|
mimetype: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ImageInfo(ImageInfoBase):
|
||||||
|
thumbnail_info: ImageInfoBase
|
||||||
|
thumbnail_url: Optional[str] = None
|
||||||
|
thumbnail_file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FileInfo:
|
||||||
|
mimetype: str
|
||||||
|
size: int
|
||||||
|
thumbnail_info: ImageInfoBase
|
||||||
|
thumbnail_url: Optional[str] = None
|
||||||
|
thumbnail_file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AudioInfo:
|
||||||
|
duration: int
|
||||||
|
mimetype: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocationInfo:
|
||||||
|
thumbnail_info: ImageInfoBase
|
||||||
|
thumbnail_url: Optional[str] = None
|
||||||
|
thumbnail_file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VideoInfo(ImageInfoBase):
|
||||||
|
duration: int
|
||||||
|
thumbnail_info: ImageInfoBase
|
||||||
|
thumbnail_url: Optional[str] = None
|
||||||
|
thumbnail_file: Optional[EncryptedFile] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PreviousRoom:
|
||||||
|
room_id: str
|
||||||
|
event_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Signed:
|
||||||
|
mxid: str
|
||||||
|
signatures: Dict[str, Dict[str, str]]
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Invite:
|
||||||
|
display_name: str
|
||||||
|
signed: Signed
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReactionRelation:
|
||||||
|
rel_type: str
|
||||||
|
event_id: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
|
def notification_power_levels_default_factory():
|
||||||
|
return {'room': 50}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
aiohttp
|
||||||
Loading…
Reference in new issue