Initial Commit

Move files from old Repo
master
DustyP 6 years ago
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…
Cancel
Save