From 517c7d8b70eb0be45d41b4330d796803caf7d0ce Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Mar 2018 23:16:58 +0200 Subject: [PATCH] Move mautrix-appservice to separate repo. Fixes #37 --- mautrix_appservice/__init__.py | 5 - mautrix_appservice/appservice.py | 183 --------- mautrix_appservice/errors.py | 38 -- mautrix_appservice/intent_api.py | 592 ------------------------------ mautrix_appservice/state_store.py | 155 -------- requirements/base.txt | 2 +- requirements/optional.txt | 1 + setup.py | 5 +- 8 files changed, 5 insertions(+), 976 deletions(-) delete mode 100644 mautrix_appservice/__init__.py delete mode 100644 mautrix_appservice/appservice.py delete mode 100644 mautrix_appservice/errors.py delete mode 100644 mautrix_appservice/intent_api.py delete mode 100644 mautrix_appservice/state_store.py diff --git a/mautrix_appservice/__init__.py b/mautrix_appservice/__init__.py deleted file mode 100644 index 7a5ef73c..00000000 --- a/mautrix_appservice/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .appservice import AppService -from .errors import MatrixError, MatrixRequestError, IntentError - -__version__ = "0.1.0" -__author__ = "Tulir Asokan " diff --git a/mautrix_appservice/appservice.py b/mautrix_appservice/appservice.py deleted file mode 100644 index bd775df4..00000000 --- a/mautrix_appservice/appservice.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: future_fstrings -*- -# matrix-appservice-python - A Matrix Application Service framework written in Python. -# Copyright (C) 2018 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Partly based on github.com/Cadair/python-appservice-framework (MIT license) -from contextlib import contextmanager -from aiohttp import web -import aiohttp -import asyncio -import logging - -from .intent_api import HTTPAPI -from .state_store import StateStore - - -class AppService: - def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None, - verify_ssl=True, query_user=None, query_alias=None): - self.server = server - self.domain = domain - self.verify_ssl = verify_ssl - self.as_token = as_token - self.hs_token = hs_token - self.bot_mxid = f"@{bot_localpart}:{domain}" - self.state_store = StateStore(autosave_file="mx-state.json") - self.state_store.load("mx-state.json") - - self.transactions = [] - - self._http_session = None - self._intent = None - - self.loop = loop or asyncio.get_event_loop() - self.log = (logging.getLogger(log) if isinstance(log, str) - else log or logging.getLogger("mautrix_appservice")) - - async def default_query_handler(_): - return None - - self.query_user = query_user or default_query_handler - self.query_alias = query_alias or default_query_handler - - self.event_handlers = [] - - self.app = web.Application(loop=self.loop) - self.app.router.add_route("PUT", "/transactions/{transaction_id}", - self._http_handle_transaction) - self.app.router.add_route("GET", "/rooms/{alias}", self._http_query_alias) - self.app.router.add_route("GET", "/users/{user_id}", self._http_query_user) - - self.matrix_event_handler(self.update_state_store) - - @property - def http_session(self): - if self._http_session is None: - raise AttributeError("the http_session attribute can only be used " - "from within the `AppService.run` context manager") - else: - return self._http_session - - @property - def intent(self): - if self._intent is None: - raise AttributeError("the intent attribute can only be used from " - "within the `AppService.run` context manager") - else: - return self._intent - - @contextmanager - def run(self, host="127.0.0.1", port=8080): - connector = None - if self.server.startswith("https://") and not self.verify_ssl: - connector = aiohttp.TCPConnector(verify_ssl=False) - self._http_session = aiohttp.ClientSession(loop=self.loop, connector=connector) - self._intent = HTTPAPI(base_url=self.server, domain=self.domain, bot_mxid=self.bot_mxid, - token=self.as_token, log=self.log, state_store=self.state_store, - client_session=self._http_session).bot_intent() - - yield self.loop.create_server(self.app.make_handler(), host, port) - - self._intent = None - self._http_session.close() - self._http_session = None - - def _check_token(self, request): - try: - token = request.rel_url.query["access_token"] - except KeyError: - return False - - if token != self.hs_token: - return False - - return True - - async def _http_query_user(self, request): - if not self._check_token(request): - return web.Response(status=401) - - user_id = request.match_info["userId"] - - try: - response = await self.query_user(user_id) - except Exception: - self.log.exception("Exception in user query handler") - return web.Response(status=500) - - if not response: - return web.Response(status=404) - return web.json_response(response) - - async def _http_query_alias(self, request): - if not self._check_token(request): - return web.Response(status=401) - - alias = request.match_info["alias"] - - try: - response = await self.query_alias(alias) - except Exception: - self.log.exception("Exception in alias query handler") - return web.Response(status=500) - - if not response: - return web.Response(status=404) - return web.json_response(response) - - async def _http_handle_transaction(self, request): - if not self._check_token(request): - return web.Response(status=401) - - transaction_id = request.match_info["transaction_id"] - if transaction_id in self.transactions: - return web.Response(status=200) - - json = await request.json() - - try: - events = json["events"] - except KeyError: - return web.Response(status=400) - - for event in events: - self.handle_matrix_event(event) - - self.transactions.append(transaction_id) - - return web.json_response({}) - - async def update_state_store(self, event): - event_type = event["type"] - if event_type == "m.room.power_levels": - self.state_store.set_power_levels(event["room_id"], event["content"]) - elif event_type == "m.room.member": - self.state_store.set_membership(event["room_id"], event["state_key"], - event["content"]["membership"]) - - def handle_matrix_event(self, event): - async def try_handle(handler): - try: - await handler(event) - except Exception: - self.log.exception("Exception in Matrix event handler") - - for handler in self.event_handlers: - asyncio.ensure_future(try_handle(handler), loop=self.loop) - - def matrix_event_handler(self, func): - self.event_handlers.append(func) - return func diff --git a/mautrix_appservice/errors.py b/mautrix_appservice/errors.py deleted file mode 100644 index 02c4f87b..00000000 --- a/mautrix_appservice/errors.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: future_fstrings -*- -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2018 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -class MatrixError(Exception): - """A generic Matrix error. Specific errors will subclass this.""" - pass - - -class IntentError(MatrixError): - def __init__(self, message, source): - super().__init__(message) - self.source = source - - -class MatrixRequestError(MatrixError): - """ The home server returned an error response. """ - - def __init__(self, code=0, text="", errcode=None, message=None): - super().__init__(f"{code}: {text}") - self.code = code - self.text = text - self.errcode = errcode - self.message = message diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py deleted file mode 100644 index 5a7495a5..00000000 --- a/mautrix_appservice/intent_api.py +++ /dev/null @@ -1,592 +0,0 @@ -# -*- coding: future_fstrings -*- -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2018 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -from urllib.parse import quote -from time import time -from json.decoder import JSONDecodeError -from aiohttp.client_exceptions import ContentTypeError -import re -import json -import magic -import asyncio - -from .errors import MatrixError, MatrixRequestError, IntentError - - -class HTTPAPI: - def __init__(self, base_url, domain=None, bot_mxid=None, token=None, identity=None, log=None, - state_store=None, client_session=None, child=False): - self.base_url = base_url - self.token = token - self.identity = identity - self.validate_cert = True - self.session = client_session - - self.domain = domain - self.bot_mxid = bot_mxid - self._bot_intent = None - self.state_store = state_store - - if child: - self.log = log - else: - self.intent_log = log.getChild("intent") - self.log = log.getChild("api") - self.txn_id = 0 - self.children = {} - - def user(self, user): - try: - return self.children[user] - except KeyError: - child = ChildHTTPAPI(user, self) - self.children[user] = child - return child - - def bot_intent(self): - if self._bot_intent: - return self._bot_intent - return IntentAPI(self.bot_mxid, self, state_store=self.state_store, log=self.intent_log) - - def intent(self, user): - return IntentAPI(user, self.user(user), self.bot_intent(), self.state_store, - self.intent_log) - - async def _send(self, method, endpoint, content, query_params, headers): - while True: - query_params["access_token"] = self.token - request = self.session.request(method, endpoint, params=query_params, - data=content, headers=headers) - async with request as response: - if response.status < 200 or response.status >= 300: - errcode = message = None - try: - response_data = await response.json() - errcode = response_data["errcode"] - message = response_data["error"] - except (JSONDecodeError, ContentTypeError, KeyError): - pass - raise MatrixRequestError(code=response.status, text=await response.text(), - errcode=errcode, message=message) - - if response.status == 429: - await asyncio.sleep(response.json()["retry_after_ms"] / 1000) - else: - return await response.json() - - def _log_request(self, method, path, content, query_params): - log_content = content if not isinstance(content, bytes) else f"<{len(content)} bytes>" - log_content = log_content or "(No content)" - query_identity = query_params["user_id"] if "user_id" in query_params else "No identity" - self.log.debug("%s %s %s as user %s", method, path, log_content, query_identity) - - def request(self, method, path, content=None, query_params=None, headers=None, - api_path="/_matrix/client/r0"): - content = content or {} - query_params = query_params or {} - headers = headers or {} - - method = method.upper() - if method not in ["GET", "PUT", "DELETE", "POST"]: - raise MatrixError("Unsupported HTTP method: %s" % method) - - if "Content-Type" not in headers: - headers["Content-Type"] = "application/json" - if headers["Content-Type"] == "application/json": - content = json.dumps(content) - - if self.identity: - query_params["user_id"] = self.identity - - self._log_request(method, path, content, query_params) - - endpoint = self.base_url + api_path + path - return self._send(method, endpoint, content, query_params, headers or {}) - - def get_download_url(self, mxcurl): - if mxcurl.startswith('mxc://'): - return f"{self.base_url}/_matrix/media/r0/download/{mxcurl[6:]}" - else: - raise ValueError("MXC URL did not begin with 'mxc://'") - - async def get_display_name(self, user_id): - content = await self.request("GET", f"/profile/{user_id}/displayname") - return content.get('displayname', None) - - async def get_avatar_url(self, user_id): - content = await self.request("GET", f"/profile/{user_id}/avatar_url") - return content.get('avatar_url', None) - - async def get_room_id(self, room_alias): - content = await self.request("GET", f"/directory/room/{quote(room_alias)}") - return content.get("room_id", None) - - def set_typing(self, room_id, is_typing=True, timeout=5000, user=None): - content = { - "typing": is_typing - } - if is_typing: - content["timeout"] = timeout - user = user or self.identity - return self.request("PUT", f"/rooms/{room_id}/typing/{user}", content) - - -class ChildHTTPAPI(HTTPAPI): - def __init__(self, user, parent): - super().__init__(parent.base_url, parent.domain, parent.bot_mxid, parent.token, user, - parent.log, parent.state_store, parent.session, child=True) - self.parent = parent - - @property - def txn_id(self): - return self.parent.txn_id - - @txn_id.setter - def txn_id(self, value): - self.parent.txn_id = value - - -class IntentAPI: - mxid_regex = re.compile("@(.+):(.+)") - - def __init__(self, mxid, client, bot=None, state_store=None, log=None): - self.client = client - self.bot = bot - self.mxid = mxid - self.log = log - - results = self.mxid_regex.match(mxid) - if not results: - raise ValueError("invalid MXID") - self.localpart = results.group(1) - - self.state_store = state_store - - def user(self, user): - if not self.bot: - return self.client.intent(user) - else: - self.log.warning("Called IntentAPI#user() of child intent object.") - return self.bot.client.intent(user) - - # region User actions - - async def get_joined_rooms(self): - await self.ensure_registered() - response = await self.client.request("GET", "/joined_rooms") - return response["joined_rooms"] - - async def set_display_name(self, name): - await self.ensure_registered() - content = {"displayname": name} - return await self.client.request("PUT", f"/profile/{self.mxid}/displayname", content) - - async def set_presence(self, status="online", ignore_cache=False): - await self.ensure_registered() - if not ignore_cache and self.state_store.has_presence(self.mxid, status): - return - content = { - "presence": status - } - resp = await self.client.request("PUT", f"/presence/{self.mxid}/status", content) - self.state_store.set_presence(self.mxid, status) - return resp - - async def set_avatar(self, url): - await self.ensure_registered() - content = {"avatar_url": url} - return await self.client.request("PUT", f"/profile/{self.mxid}/avatar_url", content) - - async def upload_file(self, data, mime_type=None): - await self.ensure_registered() - mime_type = mime_type or magic.from_buffer(data, mime=True) - return await self.client.request("POST", "", content=data, - headers={"Content-Type": mime_type}, - api_path="/_matrix/media/r0/upload") - - async def download_file(self, url): - await self.ensure_registered() - url = self.client.get_download_url(url) - async with self.client.session.get(url) as response: - return await response.read() - - # endregion - # region Room actions - - async def create_room(self, alias=None, is_public=False, name=None, topic=None, - is_direct=False, invitees=None, initial_state=None, - guests_can_join=False): - await self.ensure_registered() - content = { - "visibility": "private", - "is_direct": is_direct, - "preset": "public_chat" if is_public else "private_chat", - "guests_can_join": guests_can_join, - } - if alias: - content["room_alias_name"] = alias - if invitees: - content["invite"] = invitees - if name: - content["name"] = name - if topic: - content["topic"] = topic - if initial_state: - content["initial_state"] = initial_state - - return await self.client.request("POST", "/createRoom", content) - - def _invite_direct(self, room_id, user_id): - content = {"user_id": user_id} - return self.client.request("POST", "/rooms/" + room_id + "/invite", content) - - async def invite(self, room_id, user_id, check_cache=False): - await self.ensure_joined(room_id) - try: - ok_states = {"invite", "join"} - do_invite = (not check_cache - or self.state_store.get_membership(room_id, user_id) not in ok_states) - if do_invite: - response = await self._invite_direct(room_id, user_id) - self.state_store.invited(room_id, user_id) - return response - except MatrixRequestError as e: - if e.errcode != "M_FORBIDDEN": - raise IntentError(f"Failed to invite {user_id} to {room_id}", e) - if "is already in the room" in e.message: - self.state_store.joined(room_id, user_id) - - def set_room_avatar(self, room_id, avatar_url, info=None): - content = { - "url": avatar_url, - } - if info: - content["info"] = info - return self.send_state_event(room_id, "m.room.avatar", content) - - async def add_room_alias(self, room_id, localpart, override=True): - await self.ensure_registered() - content = {"room_id": room_id} - alias = f"#{localpart}:{self.client.domain}" - try: - return await self.client.request("PUT", f"/directory/room/{quote(alias)}", content) - except MatrixRequestError as e: - if override and e.code == 409: - await self.remove_room_alias(localpart) - return await self.client.request("PUT", f"/directory/room/{quote(alias)}", content) - - async def remove_room_alias(self, localpart): - await self.ensure_registered() - alias = f"#{localpart}:{self.client.domain}" - return await self.client.request("DELETE", f"/directory/room/{quote(alias)}") - - def set_room_name(self, room_id, name): - body = {"name": name} - return self.send_state_event(room_id, "m.room.name", body) - - async def get_power_levels(self, room_id, ignore_cache=False): - await self.ensure_joined(room_id) - if not ignore_cache: - try: - return self.state_store.get_power_levels(room_id) - except KeyError: - pass - levels = await self.client.request("GET", - f"/rooms/{quote(room_id)}/state/m.room.power_levels") - self.state_store.set_power_levels(room_id, levels) - return levels - - async def set_power_levels(self, room_id, content): - if "events" not in content: - content["events"] = {} - response = await self.send_state_event(room_id, "m.room.power_levels", content) - self.state_store.set_power_levels(room_id, content) - return response - - async def get_pinned_messages(self, room_id): - await self.ensure_joined(room_id) - response = await self.client.request("GET", f"/rooms/{room_id}/state/m.room.pinned_events") - return response["content"]["pinned"] - - def set_pinned_messages(self, room_id, events): - return self.send_state_event(room_id, "m.room.pinned_events", { - "pinned": events - }) - - async def pin_message(self, room_id, event_id): - events = await self.get_pinned_messages(room_id) - if event_id not in events: - events.append(event_id) - await self.set_pinned_messages(room_id, events) - - async def unpin_message(self, room_id, event_id): - events = await self.get_pinned_messages(room_id) - if event_id in events: - events.remove(event_id) - await self.set_pinned_messages(room_id, events) - - async def set_join_rule(self, room_id, join_rule): - if join_rule not in ("public", "knock", "invite", "private"): - raise ValueError(f"Invalid join rule \"{join_rule}\"") - await self.send_state_event(room_id, "m.room.join_rules", { - "join_rule": join_rule, - }) - - async def get_event(self, room_id, event_id): - await self.ensure_joined(room_id) - return await self.client.request("GET", f"/rooms/{room_id}/event/{event_id}") - - async def set_typing(self, room_id, is_typing=True, timeout=5000, ignore_cache=False): - await self.ensure_joined(room_id) - if not ignore_cache and is_typing == self.state_store.is_typing(room_id, self.mxid): - return - content = { - "typing": is_typing - } - if is_typing: - content["timeout"] = timeout - resp = await self.client.request("PUT", f"/rooms/{room_id}/typing/{self.mxid}", content) - self.state_store.set_typing(room_id, self.mxid, is_typing, timeout) - return resp - - async def mark_read(self, room_id, event_id): - await self.ensure_joined(room_id) - return await self.client.request("POST", f"/rooms/{room_id}/receipt/m.read/{event_id}", - content={}) - - def send_notice(self, room_id, text, html=None, relates_to=None): - return self.send_text(room_id, text, html, "m.notice", relates_to) - - def send_emote(self, room_id, text, html=None, relates_to=None): - return self.send_text(room_id, text, html, "m.emote", relates_to) - - def send_image(self, room_id, url, info=None, text=None, relates_to=None): - return self.send_file(room_id, url, info or {}, text, "m.image", relates_to) - - def send_file(self, room_id, url, info=None, text=None, file_type="m.file", relates_to=None): - return self.send_message(room_id, { - "msgtype": file_type, - "url": url, - "body": text or "Uploaded file", - "info": info or {}, - "m.relates_to": relates_to or None, - }) - - def send_text(self, room_id, text, html=None, msgtype="m.text", relates_to=None): - if html: - if not text: - text = html - return self.send_message(room_id, { - "body": text, - "msgtype": msgtype, - "format": "org.matrix.custom.html", - "formatted_body": html or text, - "m.relates_to": relates_to or None, - }) - else: - return self.send_message(room_id, { - "body": text, - "msgtype": msgtype, - "m.relates_to": relates_to or None, - }) - - def send_message(self, room_id, body): - return self.send_event(room_id, "m.room.message", body) - - async def error_and_leave(self, room_id, text, html=None): - await self.ensure_joined(room_id) - await self.send_notice(room_id, text, html=html) - await self.leave_room(room_id) - - def kick(self, room_id, user_id, message): - return self.set_membership(room_id, user_id, "leave", message) - - def get_membership(self, room_id, user_id): - return self.get_state_event(room_id, "m.room.member", state_key=user_id) - - def set_membership(self, room_id, user_id, membership, reason="", profile=None): - body = { - "membership": membership, - "reason": reason - } - profile = profile or {} - if "displayname" in profile: - body["displayname"] = profile["displayname"] - if "avatar_url" in profile: - body["avatar_url"] = profile["avatar_url"] - - return self.send_state_event(room_id, "m.room.member", body, state_key=user_id) - - def redact(self, room_id, event_id, reason=None, txn_id=None): - txn_id = txn_id or str(self.client.txn_id) + str(int(time() * 1000)) - self.client.txn_id += 1 - content = {} - if reason: - content["reason"] = reason - return self.client.request("PUT", - f"/rooms/{quote(room_id)}/redact/{quote(event_id)}/{txn_id}", - content) - - @staticmethod - def _get_event_url(room_id, event_type, txn_id): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - elif not txn_id: - raise ValueError("Transaction ID not given") - return f"/rooms/{quote(room_id)}/send/{quote(event_type)}/{quote(txn_id)}" - - async def send_event(self, room_id, event_type, content, txn_id=None): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - await self.ensure_joined(room_id) - await self._ensure_has_power_level_for(room_id, event_type) - - txn_id = txn_id or str(self.client.txn_id) + str(int(time() * 1000)) - self.client.txn_id += 1 - - url = self._get_event_url(room_id, event_type, txn_id) - - return await self.client.request("PUT", url, content) - - @staticmethod - def _get_state_url(room_id, event_type, state_key=""): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - url = f"/rooms/{quote(room_id)}/state/{quote(event_type)}" - if state_key: - url += f"/{quote(state_key)}" - return url - - async def send_state_event(self, room_id, event_type, content, state_key=""): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - await self.ensure_joined(room_id) - await self._ensure_has_power_level_for(room_id, event_type, is_state_event=True) - url = self._get_state_url(room_id, event_type, state_key) - return await self.client.request("PUT", url, content) - - async def get_state_event(self, room_id, event_type, state_key=""): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - await self.ensure_joined(room_id) - url = self._get_state_url(room_id, event_type, state_key) - return await self.client.request("GET", url) - - def join_room(self, room_id): - if not room_id: - raise ValueError("Room ID not given") - return self.ensure_joined(room_id, ignore_cache=True) - - def _join_room_direct(self, room): - if not room: - raise ValueError("Room ID not given") - return self.client.request("POST", f"/join/{quote(room)}") - - def leave_room(self, room_id): - if not room_id: - raise ValueError("Room ID not given") - try: - self.state_store.left(room_id, self.mxid) - return self.client.request("POST", f"/rooms/{quote(room_id)}/leave") - except MatrixRequestError as e: - if "not in room" not in e.message: - raise - - def get_room_memberships(self, room_id): - if not room_id: - raise ValueError("Room ID not given") - return self.client.request("GET", f"/rooms/{quote(room_id)}/members") - - async def get_room_members(self, room_id, allowed_memberships=("join",)): - memberships = await self.get_room_memberships(room_id) - return [membership["state_key"] for membership in memberships["chunk"] if - membership["content"]["membership"] in allowed_memberships] - - async def get_room_state(self, room_id): - await self.ensure_joined(room_id) - state = await self.client.request("GET", f"/rooms/{quote(room_id)}/state") - # TODO update values based on state? - return state - - # endregion - # region Ensure functions - - async def ensure_joined(self, room_id, ignore_cache=False): - if not room_id: - raise ValueError("Room ID not given") - if not ignore_cache and self.state_store.is_joined(room_id, self.mxid): - return - await self.ensure_registered() - try: - await self._join_room_direct(room_id) - self.state_store.joined(room_id, self.mxid) - except MatrixRequestError as e: - if e.errcode != "M_FORBIDDEN" or not self.bot: - raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e) - try: - await self.bot.invite(room_id, self.mxid) - await self._join_room_direct(room_id) - self.state_store.joined(room_id, self.mxid) - except MatrixRequestError as e2: - raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e2) - - def _register(self): - content = {"username": self.localpart} - query_params = {"kind": "user"} - return self.client.request("POST", "/register", content, query_params) - - async def ensure_registered(self): - if self.state_store.is_registered(self.mxid): - return - try: - await self._register() - except MatrixRequestError as e: - if e.errcode != "M_USER_IN_USE": - self.log.exception(f"Failed to register {self.mxid}!") - # raise IntentError(f"Failed to register {self.mxid}", e) - return - self.state_store.registered(self.mxid) - - async def _ensure_has_power_level_for(self, room_id, event_type, is_state_event=False): - if not room_id: - raise ValueError("Room ID not given") - elif not event_type: - raise ValueError("Event type not given") - - if not self.state_store.has_power_levels(room_id): - await self.get_power_levels(room_id) - if self.state_store.has_power_level(room_id, self.mxid, event_type, - is_state_event=is_state_event): - return - elif not self.bot: - self.log.warning( - f"Power level of {self.mxid} is not enough for {event_type} in {room_id}") - # raise IntentError(f"Power level of {self.mxid} is not enough" - # f"for {event_type} in {room_id}") - return - # TODO implement - - # endregion diff --git a/mautrix_appservice/state_store.py b/mautrix_appservice/state_store.py deleted file mode 100644 index f376426e..00000000 --- a/mautrix_appservice/state_store.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: future_fstrings -*- -# matrix-appservice-python - A Matrix Application Service framework written in Python. -# Copyright (C) 2018 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -import json -import time - - -class StateStore: - def __init__(self, autosave_file=None): - self.autosave_file = autosave_file - - # Persistent storage - self.registrations = set() - self.memberships = {} - self.power_levels = {} - - # Non-persistent storage - self.presence = {} - self.typing = {} - - def save(self, file): - if isinstance(file, str): - output = open(file, "w") - else: - output = file - - json.dump({ - "registrations": list(self.registrations), - "memberships": self.memberships, - "power_levels": self.power_levels, - }, output) - - if isinstance(file, str): - output.close() - - def load(self, file): - if isinstance(file, str): - try: - input_source = open(file, "r") - except FileNotFoundError: - return - else: - input_source = file - - data = json.load(input_source) - if "registrations" in data: - self.registrations = set(data["registrations"]) - if "memberships" in data: - self.memberships = data["memberships"] - if "power_levels" in data: - self.power_levels = data["power_levels"] - - if isinstance(file, str): - input_source.close() - - def _autosave(self): - if self.autosave_file: - self.save(self.autosave_file) - - def set_presence(self, user, presence): - self.presence[user] = presence - - def has_presence(self, user, presence): - try: - return self.presence[user] == presence - except KeyError: - return False - - def set_typing(self, room_id, user, is_typing, timeout=0): - if is_typing: - ts = int(round(time.time() * 1000)) - self.typing[(room_id, user)] = ts + timeout - else: - del self.typing[(room_id, user)] - - def is_typing(self, room_id, user): - ts = int(round(time.time() * 1000)) - try: - return self.typing[(room_id, user)] > ts - except KeyError: - return False - - def is_registered(self, user): - return user in self.registrations - - def registered(self, user): - self.registrations.add(user) - self._autosave() - - def get_membership(self, room, user): - return self.memberships.get(room, {}).get(user, "left") - - def is_joined(self, room, user): - return self.get_membership(room, user) == "join" - - def set_membership(self, room, user, membership): - if room not in self.memberships: - self.memberships[room] = {} - self.memberships[room][user] = membership - self._autosave() - - def joined(self, room, user): - return self.set_membership(room, user, "join") - - def invited(self, room, user): - return self.set_membership(room, user, "invite") - - def left(self, room, user): - return self.set_membership(room, user, "left") - - def has_power_levels(self, room): - return room in self.power_levels - - def get_power_levels(self, room): - return self.power_levels[room] - - def has_power_level(self, room, user, event, is_state_event=False, default=None): - room_levels = self.power_levels.get(room, {}) - default_required = default or (room_levels.get("state_default", 50) if is_state_event - else room_levels.get("events_default", 0)) - required = room_levels.get("events", {}).get(event, default_required) - has = room_levels.get("users", {}).get(user, room_levels.get("users_default", 0)) - return has >= required - - def set_power_level(self, room, user, level): - if room not in self.power_levels: - self.power_levels[room] = { - "users": {}, - "events": {}, - } - elif "users" not in self.power_levels[room]: - self.power_levels[room]["users"] = {} - self.power_levels[room]["users"][user] = level - self._autosave() - - def set_power_levels(self, room, content): - if "events" not in content: - content["events"] = {} - if "users" not in content: - content["users"] = {} - self.power_levels[room] = content - self._autosave() diff --git a/requirements/base.txt b/requirements/base.txt index e16861af..b7b8cc90 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ aiohttp +mautrix-appservice ruamel.yaml python-magic SQLAlchemy @@ -6,4 +7,3 @@ alembic Markdown Pillow future-fstrings -cryptg diff --git a/requirements/optional.txt b/requirements/optional.txt index ab90481d..10087cb8 100644 --- a/requirements/optional.txt +++ b/requirements/optional.txt @@ -1 +1,2 @@ lxml +cryptg diff --git a/setup.py b/setup.py index eabecf3d..a9292e9e 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ setuptools.setup( install_requires=[ "aiohttp>=3.0.1,<4", + "mautrix-telegram>=0.1,<0.2", "SQLAlchemy>=1.2.3,<2", "alembic>=0.9.8,<0.10", "Markdown>=2.6.11,<3", @@ -25,7 +26,6 @@ setuptools.setup( "Pillow>=5.0.0,<6", "future-fstrings>=0.4.2", "python-magic>=0.4.15,<0.5", - "cryptg>=0.1,<0.2", "telethon-aio>=0.18,<0.19" if sys.version_info >= (3, 6) else "telethon-aio-git", ], dependency_links=[ @@ -33,10 +33,11 @@ setuptools.setup( ], extras_require={ "highlight_edits": ["lxml>=4.1.1,<5"], + "fast_crypto": ["cryptg>=0.1,<0.2"], }, classifiers=[ - "Development Status :: 4 Beta", + "Development Status :: 4 - Beta", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Topic :: Communications :: Chat", "Programming Language :: Python",