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