From d8653961af3289ceb040128c0168aebdddeb10ff Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 4 Aug 2019 01:41:10 +0300 Subject: [PATCH] Even even more migrations to mautrix-python --- mautrix_telegram/__init__.py | 2 +- mautrix_telegram/__main__.py | 6 +- mautrix_telegram/abstract_user.py | 6 +- mautrix_telegram/bot.py | 2 +- mautrix_telegram/commands/__init__.py | 13 +- mautrix_telegram/commands/clean_rooms.py | 14 +- mautrix_telegram/commands/handler.py | 343 +++--------------- mautrix_telegram/commands/matrix_auth.py | 38 +- mautrix_telegram/commands/meta.py | 71 ---- mautrix_telegram/commands/portal/admin.py | 24 +- mautrix_telegram/commands/portal/bridge.py | 23 +- mautrix_telegram/commands/portal/config.py | 18 +- .../commands/portal/create_chat.py | 4 +- mautrix_telegram/commands/portal/filter.py | 9 +- mautrix_telegram/commands/portal/misc.py | 14 +- mautrix_telegram/commands/portal/unbridge.py | 11 +- mautrix_telegram/commands/portal/util.py | 47 +-- mautrix_telegram/commands/telegram/account.py | 2 + mautrix_telegram/config.py | 2 - mautrix_telegram/db/__init__.py | 3 +- mautrix_telegram/db/base.py | 58 --- mautrix_telegram/db/base.pyi | 26 -- mautrix_telegram/db/bot_chat.py | 19 +- mautrix_telegram/db/message.py | 45 +-- mautrix_telegram/db/portal.py | 45 ++- mautrix_telegram/db/puppet.py | 47 ++- mautrix_telegram/db/telegram_file.py | 44 +-- mautrix_telegram/db/user.py | 55 ++- mautrix_telegram/formatter/from_telegram.py | 136 +++---- mautrix_telegram/formatter/util.py | 21 -- mautrix_telegram/portal.py | 252 ++++++------- mautrix_telegram/util/__init__.py | 6 - mautrix_telegram/web/common/auth_api.py | 4 +- mautrix_telegram/web/provisioning/__init__.py | 11 +- 34 files changed, 475 insertions(+), 946 deletions(-) delete mode 100644 mautrix_telegram/commands/meta.py delete mode 100644 mautrix_telegram/db/base.py delete mode 100644 mautrix_telegram/db/base.pyi diff --git a/mautrix_telegram/__init__.py b/mautrix_telegram/__init__.py index 4300d91b..ceba3d65 100644 --- a/mautrix_telegram/__init__.py +++ b/mautrix_telegram/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.6.0" +__version__ = "0.7.0+dev" __author__ = "Tulir Asokan " diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 40235217..fe30799d 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -15,16 +15,18 @@ # along with this program. If not, see . from itertools import chain -from mautrix.bridge import Bridge from alchemysession import AlchemySessionContainer +from mautrix.bridge import Bridge +from mautrix.bridge.db import Base + from .web.provisioning import ProvisioningAPI from .web.public import PublicBridgeWebsite from .abstract_user import init as init_abstract_user from .bot import Bot, init as init_bot from .config import Config from .context import Context -from .db import Base, init as init_db +from .db import init as init_db from .formatter import init as init_formatter from .matrix import MatrixHandler from .portal import init as init_portal diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index c15dcaf8..64752a78 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -381,8 +381,7 @@ class AbstractUser(ABC): return for message_id in update.messages: - messages = DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid) - for message in messages: + for message in DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid): message.delete() number_left = DBMessage.count_spaces_by_mxid(message.mxid, message.mx_room) if number_left == 0: @@ -395,8 +394,7 @@ class AbstractUser(ABC): channel_id = TelegramID(update.channel_id) for message_id in update.messages: - messages = DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id) - for message in messages: + for message in DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id): message.delete() await self._try_redact(message) diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py index 6ac2b138..1eacfbc2 100644 --- a/mautrix_telegram/bot.py +++ b/mautrix_telegram/bot.py @@ -141,7 +141,7 @@ class Bot(AbstractUser): del self.chats[chat_id] except KeyError: pass - BotChat.delete(chat_id) + BotChat.delete_by_id(chat_id) async def _can_use_commands(self, chat: TypePeer, tgid: TelegramID) -> bool: if tgid in self.tg_whitelist: diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py index cb11c5f0..b33fe63b 100644 --- a/mautrix_telegram/commands/__init__.py +++ b/mautrix_telegram/commands/__init__.py @@ -1,5 +1,8 @@ -from .handler import (command_handler, command_handlers as _command_handlers, - CommandHandler, CommandProcessor, CommandEvent, - SECTION_GENERAL, SECTION_AUTH, SECTION_CREATING_PORTALS, - SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN) -from . import portal, telegram, clean_rooms, matrix_auth, meta +from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent, + SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT, + SECTION_MISC, SECTION_ADMIN) +from . import portal, telegram, clean_rooms, matrix_auth + +__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent", + "SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS", + "SECTION_PORTAL_MANAGEMENT"] diff --git a/mautrix_telegram/commands/clean_rooms.py b/mautrix_telegram/commands/clean_rooms.py index 1501bc2f..8182f746 100644 --- a/mautrix_telegram/commands/clean_rooms.py +++ b/mautrix_telegram/commands/clean_rooms.py @@ -13,11 +13,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, NamedTuple, Optional, Tuple, Union +from typing import List, NamedTuple, Tuple, Union from mautrix.appservice import IntentAPI from mautrix.errors import MatrixRequestError -from mautrix.types import RoomID, UserID +from mautrix.types import RoomID, UserID, EventID from . import command_handler, CommandEvent, SECTION_ADMIN from .. import puppet as pu, portal as po @@ -61,7 +61,7 @@ async def _find_rooms(intent: IntentAPI) -> Tuple[List[ManagementRoom], List[Roo @command_handler(needs_admin=True, needs_auth=False, management_only=True, name="clean-rooms", help_section=SECTION_ADMIN, help_text="Clean up unused portal/management rooms.") -async def clean_rooms(evt: CommandEvent) -> Optional[Dict]: +async def clean_rooms(evt: CommandEvent) -> EventID: management_rooms, unidentified_rooms, portals, empty_portals = await _find_rooms(evt.az.intent) reply = ["#### Management rooms (M)"] @@ -107,10 +107,10 @@ async def clean_rooms(evt: CommandEvent) -> Optional[Dict]: async def set_rooms_to_clean(evt, management_rooms: List[ManagementRoom], - unidentified_rooms: List[MatrixRoomID], portals: List["po.Portal"], + unidentified_rooms: List[RoomID], portals: List["po.Portal"], empty_portals: List["po.Portal"]) -> None: command = evt.args[0] - rooms_to_clean = [] # type: List[Union[po.Portal, MatrixRoomID]] + rooms_to_clean: List[Union[po.Portal, RoomID]] = [] if command == "clean-recommended": rooms_to_clean += empty_portals rooms_to_clean += unidentified_rooms @@ -159,7 +159,7 @@ async def set_rooms_to_clean(evt, management_rooms: List[ManagementRoom], "`$cmdprefix+sp confirm-clean`.") -async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, MatrixRoomID]]) -> None: +async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, RoomID]]) -> None: if len(evt.args) > 0 and evt.args[0] == "confirm-clean": await evt.reply(f"Cleaning {len(rooms_to_clean)} rooms. " "This might take a while.") @@ -168,7 +168,7 @@ async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, Matrix if isinstance(room, po.Portal): await room.cleanup_and_delete() cleaned += 1 - elif isinstance(room, str): # str is aliased by MatrixRoomID + else: await po.Portal.cleanup_room(evt.az.intent, room, message="Room deleted") cleaned += 1 evt.sender.command_status = None diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 1647ea6e..f8a1b3c0 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -14,24 +14,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """This module contains classes handling commands issued by Matrix users.""" -from typing import Awaitable, Callable, Dict, List, NamedTuple, Optional -import logging -import traceback - -import commonmark +from typing import Awaitable, Callable, List, Optional, NamedTuple, Any from telethon.errors import FloodWaitError from mautrix.types import RoomID, EventID +from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent, + CommandHandler as BaseCommandHandler, + CommandProcessor as BaseCommandProcessor, + CommandHandlerFunc, command_handler as base_command_handler) from ..util import format_duration from .. import user as u, context as c -command_handlers: Dict[str, 'CommandHandler'] = {} +HelpCacheKey = NamedTuple('HelpCacheKey', + is_management=bool, is_portal=bool, puppet_whitelisted=bool, + matrix_puppet_whitelisted=bool, is_admin=bool, is_logged_in=bool) -HelpSection = NamedTuple('HelpSection', [('name', str), ('order', int), ('description', str)]) - -SECTION_GENERAL = HelpSection("General", 0, "") SECTION_AUTH = HelpSection("Authentication", 10, "") SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "") SECTION_PORTAL_MANAGEMENT = HelpSection("Portal management", 30, "") @@ -39,186 +38,42 @@ SECTION_MISC = HelpSection("Miscellaneous", 40, "") SECTION_ADMIN = HelpSection("Administration", 50, "") -class HtmlEscapingRenderer(commonmark.HtmlRenderer): - def __init__(self, allow_html: bool = False): - super().__init__() - self.allow_html = allow_html +class CommandEvent(BaseCommandEvent): + sender: u.User - def lit(self, s): - if self.allow_html: - return super().lit(s) - return super().lit(s.replace("<", "<").replace(">", ">")) - - def image(self, node, entering): - prev = self.allow_html - self.allow_html = True - super().image(node, entering) - self.allow_html = prev - - -md_parser = commonmark.Parser() -md_renderer = HtmlEscapingRenderer() - - -def ensure_trailing_newline(s: str) -> str: - """Returns the passed string, but with a guaranteed trailing newline.""" - return s + ("" if s[-1] == "\n" else "\n") - - -class CommandEvent: - """Holds information about a command issued in a Matrix room. - - When a Matrix command was issued to the bot, CommandEvent will hold - information regarding the event. - - Attributes: - room_id: The id of the Matrix room in which the command was issued. - event_id: The id of the matrix event which contained the command. - sender: The user who issued the command. - command: The issued command. - args: Arguments given with the issued command. - is_management: Determines whether the room in which the command wa - issued is a management room. - is_portal: Determines whether the room in which the command was issued - is a portal. - """ - - def __init__(self, processor: 'CommandProcessor', room: RoomID, event: EventID, + def __init__(self, processor: 'CommandProcessor', room_id: RoomID, event_id: EventID, sender: u.User, command: str, args: List[str], is_management: bool, is_portal: bool) -> None: - self.az = processor.az - self.log = processor.log - self.loop = processor.loop + super().__init__(processor, room_id, event_id, sender, command, args, is_management, + is_portal) self.tgbot = processor.tgbot self.config = processor.config self.public_website = processor.public_website - self.command_prefix = processor.command_prefix - self.room_id = room - self.event_id = event - self.sender = sender - self.command = command - self.args = args - self.is_management = is_management - self.is_portal = is_portal - def reply(self, message: str, allow_html: bool = False, render_markdown: bool = True - ) -> Awaitable[EventID]: - """Write a reply to the room in which the command was issued. - - Replaces occurences of "$cmdprefix" in the message with the command - prefix and replaces occurences of "$cmdprefix+sp " with the command - prefix if the command was not issued in a management room. - If allow_html and render_markdown are both False, the message will not - be rendered to html and sending of html is disabled. - - Args: - message: The message to post in the room. - allow_html: Escape html in the message or don't render html at all - if markdown is disabled. - render_markdown: Use markdown formatting to render the passed - message to html. - - Returns: - Handler for the message sending function. - """ - message_cmd = self._replace_command_prefix(message) - html = self._render_message(message_cmd, allow_html=allow_html, - render_markdown=render_markdown) - - return self.az.intent.send_notice(self.room_id, message_cmd, html=html) - - def mark_read(self) -> Awaitable[Dict]: - """Marks the command as read by the bot.""" - return self.az.intent.mark_read(self.room_id, self.event_id) - - def _replace_command_prefix(self, message: str) -> str: - """Returns the string with the proper command prefix entered.""" - message = message.replace( - "$cmdprefix+sp ", "" if self.is_management else f"{self.command_prefix} " - ) - return message.replace("$cmdprefix", self.command_prefix) - - @staticmethod - def _render_message(message: str, allow_html: bool, render_markdown: bool) -> Optional[str]: - """Renders the message as HTML. - - Args: - allow_html: Flag to allow custom HTML in the message. - render_markdown: If true, markdown styling is applied to the message. - - Returns: - The message rendered as HTML. - None is returned if no styled output is required. - """ - html = "" - if render_markdown: - md_renderer.allow_html = allow_html - html = md_renderer.render(md_parser.parse(message)) - elif allow_html: - html = message - return ensure_trailing_newline(html) if html else None + async def get_help_key(self) -> HelpCacheKey: + return HelpCacheKey(self.is_management, self.is_portal, self.sender.puppet_whitelisted, + self.sender.matrix_puppet_whitelisted, self.sender.is_admin, + await self.sender.is_logged_in()) -class CommandHandler: - """A command which can be executed from a Matrix room. +class CommandHandler(BaseCommandHandler): + name: str - The command manages its permission and help texts. - When called, it will check the permission of the command event and execute - the command or, in case of error, report back to the user. - - Attributes: - needs_auth: Flag indicating if the sender is required to be logged in. - needs_puppeting: Flag indicating if the sender is required to use - Telegram puppeteering for this command. - needs_matrix_puppeting: Flag indicating if the sender is required to use - Matrix pupeteering. - needs_admin: Flag for whether only admin users can issue this command. - management_only: Whether the command can exclusively be issued in a - management room. - name: The name of this command. - help_section: Section of the help in which this command will appear. - """ + management_only: bool + needs_auth: bool + needs_puppeting: bool + needs_matrix_puppeting: bool + needs_admin: bool def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]], needs_auth: bool, needs_puppeting: bool, needs_matrix_puppeting: bool, needs_admin: bool, management_only: bool, name: str, help_text: str, help_args: str, help_section: HelpSection) -> None: - """ - Args: - handler: The function handling the execution of this command. - needs_auth: Flag indicating if the sender is required to be logged in. - needs_puppeting: Flag indicating if the sender is required to use - Telegram puppeteering for this command. - needs_matrix_puppeting: Flag indicating if the sender is required to - use Matrix pupeteering. - needs_admin: Flag for whether only admin users can issue this command. - management_only: Whether the command can exclusively be issued - in a management room. - name: The name of this command. - help_text: The text displayed in the help for this command. - help_args: Help text for the arguments of this command. - help_section: Section of the help in which this command will appear. - """ - self._handler = handler - self.needs_auth = needs_auth - self.needs_puppeting = needs_puppeting - self.needs_matrix_puppeting = needs_matrix_puppeting - self.needs_admin = needs_admin - self.management_only = management_only - self.name = name - self._help_text = help_text - self._help_args = help_args - self.help_section = help_section + super().__init__(handler, management_only, name, help_text, help_args, help_section, + needs_auth=needs_auth, needs_puppeting=needs_puppeting, + needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin) async def get_permission_error(self, evt: CommandEvent) -> Optional[str]: - """Returns the reason why the command could not be issued. - - Args: - evt: The event for which to get the error information. - - Returns: - A string describing the error or None if there was no error. - """ if self.management_only and not evt.is_management: return (f"`{evt.command}` is a restricted command: " "you may only run it in management rooms.") @@ -232,134 +87,40 @@ class CommandHandler: return "This command requires you to be logged in." return None - def has_permission(self, is_management: bool, puppet_whitelisted: bool, - matrix_puppet_whitelisted: bool, is_admin: bool, is_logged_in: bool) -> bool: - """Checks the permission for this command with the given status. - - Args: - is_management: If the room in which the command will be issued is a - management room. - puppet_whitelisted: If the connected Telegram account puppet is - allowed to issue the command. - matrix_puppet_whitelisted: If the connected Matrix account puppet is - allowed to issue the command. - is_admin: If the issuing user is an admin. - is_logged_in: If the issuing user is logged in. - - Returns: - True if a user with the given state is allowed to issue the - command. - """ - return ((not self.management_only or is_management) and - (not self.needs_puppeting or puppet_whitelisted) and - (not self.needs_matrix_puppeting or matrix_puppet_whitelisted) and - (not self.needs_admin or is_admin) and - (not self.needs_auth or is_logged_in)) - - async def __call__(self, evt: CommandEvent) -> EventID: - """Executes the command if evt was issued with proper rights. - - Args: - evt: The CommandEvent for which to check permissions. - - Returns: - The result of the command or the error message function. - - Raises: - FloodWaitError - """ - error = await self.get_permission_error(evt) - if error is not None: - return await evt.reply(error) - return await self._handler(evt) - - @property - def has_help(self) -> bool: - """Returns true if this command has a help text.""" - return bool(self.help_section) and bool(self._help_text) - - @property - def help(self) -> str: - """Returns the help text to this command.""" - return f"**{self.name}** {self._help_args} - {self._help_text}" + def has_permission(self, key: HelpCacheKey) -> bool: + return ((not self.management_only or key.is_management) and + (not self.needs_puppeting or key.puppet_whitelisted) and + (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted) and + (not self.needs_admin or key.is_admin) and + (not self.needs_auth or key.is_logged_in)) -def command_handler(_func: Optional[Callable[[CommandEvent], Awaitable[EventID]]] = None, *, - needs_auth: bool = True, needs_puppeting: bool = True, - needs_matrix_puppeting: bool = False, needs_admin: bool = False, - management_only: bool = False, name: Optional[str] = None, - help_text: str = "", help_args: str = "", help_section: HelpSection = None - ) -> Callable[[Callable[[CommandEvent], Awaitable[Optional[EventID]]]], - CommandHandler]: - def decorator(func: Callable[[CommandEvent], Awaitable[Optional[EventID]]]) -> CommandHandler: - actual_name = name or func.__name__.replace("_", "-") - handler = CommandHandler(func, needs_auth, needs_puppeting, needs_matrix_puppeting, - needs_admin, management_only, actual_name, help_text, help_args, - help_section) - command_handlers[handler.name] = handler - return handler - - return decorator if _func is None else decorator(_func) +def command_handler(_func: Optional[CommandHandlerFunc] = None, *, needs_auth: bool = True, + needs_puppeting: bool = True, needs_matrix_puppeting: bool = False, + needs_admin: bool = False, management_only: bool = False, + name: Optional[str] = None, help_text: str = "", help_args: str = "", + help_section: HelpSection = None) -> Callable[[CommandHandlerFunc], + CommandHandler]: + return base_command_handler( + _func, _handler_class=CommandHandler, name=name, help_text=help_text, help_args=help_args, + help_section=help_section, management_only=management_only, needs_auth=needs_auth, + needs_admin=needs_admin, needs_puppeting=needs_puppeting, + needs_matrix_puppeting=needs_matrix_puppeting) -class CommandProcessor: - """Handles the raw commands issued by a user to the Matrix bot.""" - log = logging.getLogger("mau.commands") - +class CommandProcessor(BaseCommandProcessor): def __init__(self, context: c.Context) -> None: + super().__init__(az=context.az, config=context.config, event_class=CommandEvent, + loop=context.loop) + self.tgbot = context.bot self.az, self.config, self.loop, self.tgbot = context.core self.public_website = context.public_website self.command_prefix = self.config["bridge.command_prefix"] - async def handle(self, room: RoomID, event_id: EventID, sender: u.User, - command: str, args: List[str], is_management: bool, is_portal: bool - ) -> Optional[EventID]: - """Handles the raw commands issued by a user to the Matrix bot. - - If the command is not known, it might be a followup command and is - delegated to a command handler registered for that purpose in the - senders command_status as "next". - - Args: - room: ID of the Matrix room in which the command was issued. - event_id: ID of the event by which the command was issued. - sender: The sender who issued the command. - command: The issued command, case insensitive. - args: Arguments given with the command. - is_management: Whether the room is a management room. - is_portal: Whether the room is a portal. - - Returns: - The result of the error message function or None if no error - occured. Unknown and delegated commands do not count as errors. - """ - if not command_handlers or "unknown-command" not in command_handlers: - raise ValueError("command_handlers are not properly initialized.") - - evt = CommandEvent(self, room, event_id, sender, command, args, is_management, is_portal) - orig_command = command - command = command.lower() + @staticmethod + async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent + ) -> Any: try: - handler = command_handlers[command] - except KeyError: - if sender.command_status and "next" in sender.command_status: - args.insert(0, orig_command) - evt.command = "" - handler = sender.command_status["next"] - else: - handler = command_handlers["unknown-command"] - try: - await handler(evt) + return await handler(evt) except FloodWaitError as e: return await evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}") - except Exception: - self.log.exception("Unhandled error while handling command " - f"{evt.command} {' '.join(args)} from {sender.mxid}") - if evt.sender.is_admin and evt.is_management: - return await evt.reply("Unhandled error while handling command:\n\n" - "```traceback\n" - f"{traceback.format_exc()}" - "```") - return await evt.reply("Unhandled error while handling command. " - "Check logs for more details.") - return None diff --git a/mautrix_telegram/commands/matrix_auth.py b/mautrix_telegram/commands/matrix_auth.py index 9b37d794..70949e75 100644 --- a/mautrix_telegram/commands/matrix_auth.py +++ b/mautrix_telegram/commands/matrix_auth.py @@ -13,17 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Optional +from mautrix.types import EventID +from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf from . import command_handler, CommandEvent, SECTION_AUTH from .. import puppet as pu @command_handler(needs_auth=True, needs_matrix_puppeting=True, - help_section=SECTION_AUTH, - help_text="Revert your Telegram account's Matrix puppet to use the default Matrix " - "account.") -async def logout_matrix(evt: CommandEvent) -> Optional[Dict]: + help_section=SECTION_AUTH, help_text="Revert your Telegram account's Matrix " + "puppet to use the default Matrix account.") +async def logout_matrix(evt: CommandEvent) -> EventID: puppet = pu.Puppet.get(evt.sender.tgid) if not puppet.is_real_user: return await evt.reply("You are not logged in with your Matrix account.") @@ -35,7 +35,7 @@ async def logout_matrix(evt: CommandEvent) -> Optional[Dict]: help_section=SECTION_AUTH, help_text="Replace your Telegram account's Matrix puppet with your own Matrix " "account.") -async def login_matrix(evt: CommandEvent) -> Optional[Dict]: +async def login_matrix(evt: CommandEvent) -> EventID: puppet = pu.Puppet.get(evt.sender.tgid) if puppet.is_real_user: return await evt.reply("You have already logged in with your Matrix account. " @@ -70,31 +70,29 @@ async def login_matrix(evt: CommandEvent) -> Optional[Dict]: @command_handler(needs_auth=True, needs_matrix_puppeting=True, help_section=SECTION_AUTH, help_text="Pings the server with the stored matrix authentication.") -async def ping_matrix(evt: CommandEvent) -> Optional[Dict]: +async def ping_matrix(evt: CommandEvent) -> EventID: puppet = pu.Puppet.get(evt.sender.tgid) if not puppet.is_real_user: return await evt.reply("You are not logged in with your Matrix account.") - resp = await puppet.init_custom_mxid() - if resp == pu.PuppetError.InvalidAccessToken: + try: + await puppet.init_custom_mxid() + except InvalidAccessToken: return await evt.reply("Your access token is invalid.") - elif resp == pu.PuppetError.Success: - return await evt.reply("Your Matrix login is working.") - return await evt.reply(f"Unknown response while checking your Matrix login: {resp}.") + return await evt.reply("Your Matrix login is working.") -async def enter_matrix_token(evt: CommandEvent) -> Dict: +async def enter_matrix_token(evt: CommandEvent) -> EventID: evt.sender.command_status = None puppet = pu.Puppet.get(evt.sender.tgid) if puppet.is_real_user: return await evt.reply("You have already logged in with your Matrix account. " "Log out with `$cmdprefix+sp logout-matrix` first.") - - resp = await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid) - if resp == pu.PuppetError.OnlyLoginSelf: + try: + await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid) + except OnlyLoginSelf: return await evt.reply("You can only log in as your own Matrix user.") - elif resp == pu.PuppetError.InvalidAccessToken: + except InvalidAccessToken: return await evt.reply("Failed to verify access token.") - assert resp == pu.PuppetError.Success, "Encountered an unhandled PuppetError." - return await evt.reply( - f"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}.") + return await evt.reply("Replaced your Telegram account's Matrix puppet " + f"with {puppet.custom_mxid}.") diff --git a/mautrix_telegram/commands/meta.py b/mautrix_telegram/commands/meta.py deleted file mode 100644 index 55f86335..00000000 --- a/mautrix_telegram/commands/meta.py +++ /dev/null @@ -1,71 +0,0 @@ -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2019 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -from typing import Dict, List, Optional, Tuple - -from . import command_handler, CommandEvent, _command_handlers, SECTION_GENERAL -from .handler import HelpSection - - -@command_handler(needs_auth=False, needs_puppeting=False, - help_section=SECTION_GENERAL, - help_text="Cancel an ongoing action (such as login)") -async def cancel(evt: CommandEvent) -> Optional[Dict]: - if evt.sender.command_status: - action = evt.sender.command_status["action"] - evt.sender.command_status = None - return await evt.reply(f"{action} cancelled.") - else: - return await evt.reply("No ongoing command.") - - -@command_handler(needs_auth=False, needs_puppeting=False) -async def unknown_command(evt: CommandEvent) -> Optional[Dict]: - return await evt.reply("Unknown command. Try `$cmdprefix+sp help` for help.") - - -help_cache = {} # type: Dict[Tuple[bool, bool, bool, bool, bool], str] - - -async def _get_help_text(evt: CommandEvent) -> str: - cache_key = (evt.is_management, evt.sender.puppet_whitelisted, - evt.sender.matrix_puppet_whitelisted, evt.sender.is_admin, - await evt.sender.is_logged_in()) - if cache_key not in help_cache: - help_sections = {} # type: Dict[HelpSection, List[str]] - for handler in _command_handlers.values(): - if handler.has_help and handler.has_permission(*cache_key): - help_sections.setdefault(handler.help_section, []) - help_sections[handler.help_section].append(handler.help + " ") - help_sorted = sorted(help_sections.items(), key=lambda item: item[0].order) - helps = ["#### {}\n{}\n".format(key.name, "\n".join(value)) for key, value in help_sorted] - help_cache[cache_key] = "\n".join(helps) - return help_cache[cache_key] - - -def _get_management_status(evt: CommandEvent) -> str: - if evt.is_management: - return "This is a management room: prefixing commands with `$cmdprefix` is not required." - elif evt.is_portal: - return ("**This is a portal room**: you must always prefix commands with `$cmdprefix`.\n" - "Management commands will not be sent to Telegram.") - return "**This is not a management room**: you must prefix commands with `$cmdprefix`." - - -@command_handler(name="help", needs_auth=False, needs_puppeting=False, - help_section=SECTION_GENERAL, - help_text="Show this help message.") -async def help_cmd(evt: CommandEvent) -> Optional[Dict]: - return await evt.reply(_get_management_status(evt) + "\n" + await _get_help_text(evt)) diff --git a/mautrix_telegram/commands/portal/admin.py b/mautrix_telegram/commands/portal/admin.py index b8e1e3ff..7388251b 100644 --- a/mautrix_telegram/commands/portal/admin.py +++ b/mautrix_telegram/commands/portal/admin.py @@ -13,10 +13,10 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict import asyncio -from mautrix_appservice import MatrixRequestError +from mautrix.errors import MatrixRequestError +from mautrix.types import EventID from ... import portal as po, puppet as pu, user as u from .. import command_handler, CommandEvent, SECTION_ADMIN @@ -26,7 +26,7 @@ from .. import command_handler, CommandEvent, SECTION_ADMIN help_section=SECTION_ADMIN, help_args="<_level_> [_mxid_]", help_text="Set a temporary power level without affecting Telegram.") -async def set_power_level(evt: CommandEvent) -> Dict: +async def set_power_level(evt: CommandEvent) -> EventID: try: level = int(evt.args[0]) except KeyError: @@ -35,20 +35,19 @@ async def set_power_level(evt: CommandEvent) -> Dict: return await evt.reply("The level must be an integer.") levels = await evt.az.intent.get_power_levels(evt.room_id) mxid = evt.args[1] if len(evt.args) > 1 else evt.sender.mxid - levels["users"][mxid] = level + levels.users[mxid] = level try: - await evt.az.intent.set_power_levels(evt.room_id, levels) + return await evt.az.intent.set_power_levels(evt.room_id, levels) except MatrixRequestError: evt.log.exception("Failed to set power level.") return await evt.reply("Failed to set power level.") - return {} @command_handler(needs_admin=True, needs_auth=False, help_section=SECTION_ADMIN, help_args="<`portal`|`puppet`|`user`>", help_text="Clear internal bridge caches") -async def clear_db_cache(evt: CommandEvent) -> Dict: +async def clear_db_cache(evt: CommandEvent) -> EventID: try: section = evt.args[0].lower() except IndexError: @@ -62,9 +61,8 @@ async def clear_db_cache(evt: CommandEvent) -> Dict: for puppet in pu.Puppet.by_custom_mxid.values(): puppet.sync_task.cancel() pu.Puppet.by_custom_mxid = {} - await asyncio.gather( - *[puppet.init_custom_mxid() for puppet in pu.Puppet.all_with_custom_mxid()], - loop=evt.loop) + await asyncio.gather(*[puppet.start() for puppet in pu.Puppet.all_with_custom_mxid()], + loop=evt.loop) await evt.reply("Cleared puppet cache and restarted custom puppet syncers") elif section == "user": u.User.by_mxid = { @@ -80,7 +78,7 @@ async def clear_db_cache(evt: CommandEvent) -> Dict: help_section=SECTION_ADMIN, help_args="[_mxid_]", help_text="Reload and reconnect a user") -async def reload_user(evt: CommandEvent) -> Dict: +async def reload_user(evt: CommandEvent) -> EventID: if len(evt.args) > 0: mxid = evt.args[0] else: @@ -96,5 +94,5 @@ async def reload_user(evt: CommandEvent) -> Dict: user = u.User.get_by_mxid(mxid) await user.ensure_started() if puppet: - await puppet.init_custom_mxid() - await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})") + await puppet.start() + return await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})") diff --git a/mautrix_telegram/commands/portal/bridge.py b/mautrix_telegram/commands/portal/bridge.py index c52830bb..4b6ee928 100644 --- a/mautrix_telegram/commands/portal/bridge.py +++ b/mautrix_telegram/commands/portal/bridge.py @@ -13,13 +13,14 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Optional, Tuple, Coroutine +from typing import Optional, Tuple, Coroutine import asyncio from telethon.tl.types import ChatForbidden, ChannelForbidden -from ...types import MatrixRoomID, TelegramID -from ...util import ignore_coro +from mautrix.types import EventID, RoomID + +from ...types import TelegramID from ... import portal as po from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS from .util import user_has_power_level, get_initial_state @@ -31,7 +32,7 @@ from .util import user_has_power_level, get_initial_state help_text="Bridge the current Matrix room to the Telegram chat with the given " "ID. The ID must be the prefixed version that you get with the `/id` " "command of the Telegram-side bot.") -async def bridge(evt: CommandEvent) -> Dict: +async def bridge(evt: CommandEvent) -> EventID: if len(evt.args) == 0: return await evt.reply("**Usage:** " "`$cmdprefix+sp bridge [Matrix room ID]`") @@ -39,7 +40,7 @@ async def bridge(evt: CommandEvent) -> Dict: if evt.args[0] == "--usebot" and evt.sender.is_admin: force_use_bot = True evt.args = evt.args[1:] - room_id = MatrixRoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id + room_id = RoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id that_this = "This" if room_id == evt.room_id else "That" portal = po.Portal.get_by_mxid(room_id) @@ -104,7 +105,8 @@ async def bridge(evt: CommandEvent) -> Dict: async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal" - ) -> Tuple[bool, Optional[Coroutine[None, None, None]]]: + ) -> Tuple[ + bool, Optional[Coroutine[None, None, None]]]: if not portal.mxid: await evt.reply("The portal seems to have lost its Matrix room between you" "calling `$cmdprefix+sp bridge` and this command.\n\n" @@ -127,7 +129,7 @@ async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Porta return False, None -async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]: +async def confirm_bridge(evt: CommandEvent) -> Optional[EventID]: status = evt.sender.command_status try: portal = po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"]) @@ -142,7 +144,7 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]: if not ok: return None elif coro: - ignore_coro(asyncio.ensure_future(coro, loop=evt.loop)) + asyncio.ensure_future(coro, loop=evt.loop) await evt.reply("Cleaning up previous portal room...") elif portal.mxid: evt.sender.command_status = None @@ -179,8 +181,7 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]: portal.photo_id = "" portal.save() - ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, - levels=levels), - loop=evt.loop)) + asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels), + loop=evt.loop) return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.") diff --git a/mautrix_telegram/commands/portal/config.py b/mautrix_telegram/commands/portal/config.py index bcf0b381..2ce9c283 100644 --- a/mautrix_telegram/commands/portal/config.py +++ b/mautrix_telegram/commands/portal/config.py @@ -13,10 +13,12 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Awaitable +from typing import Awaitable from io import StringIO -from ...config import yaml +from mautrix.util.config import yaml +from mautrix.types import EventID + from ... import portal as po, util from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT @@ -54,7 +56,7 @@ async def config(evt: CommandEvent) -> None: portal.save() -def config_help(evt: CommandEvent) -> Awaitable[Dict]: +def config_help(evt: CommandEvent) -> Awaitable[EventID]: return evt.reply("""**Usage:** `$cmdprefix config [...]`. Subcommands: * **help** - View this help text. @@ -67,13 +69,13 @@ def config_help(evt: CommandEvent) -> Awaitable[Dict]: """) -def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[Dict]: +def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]: stream = StringIO() yaml.dump(portal.local_config, stream) return evt.reply(f"Room-specific config:\n\n```yaml\n{stream.getvalue()}```") -def config_defaults(evt: CommandEvent) -> Awaitable[Dict]: +def config_defaults(evt: CommandEvent) -> Awaitable[EventID]: stream = StringIO() yaml.dump({ "bridge_notices": { @@ -89,7 +91,7 @@ def config_defaults(evt: CommandEvent) -> Awaitable[Dict]: return evt.reply(f"Bridge instance wide config:\n\n```yaml\n{stream.getvalue()}```") -def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[Dict]: +def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[EventID]: if not key or value is None: return evt.reply(f"**Usage:** `$cmdprefix+sp config set `") elif util.recursive_set(portal.local_config, key, value): @@ -99,7 +101,7 @@ def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Aw "Does the path contain non-map types?") -def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Dict]: +def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[EventID]: if not key: return evt.reply(f"**Usage:** `$cmdprefix+sp config unset `") elif util.recursive_del(portal.local_config, key): @@ -109,7 +111,7 @@ def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Di def config_add_del(evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str - ) -> Awaitable[Dict]: + ) -> Awaitable[EventID]: if not key or value is None: return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} `") diff --git a/mautrix_telegram/commands/portal/create_chat.py b/mautrix_telegram/commands/portal/create_chat.py index 6c052bef..535d7200 100644 --- a/mautrix_telegram/commands/portal/create_chat.py +++ b/mautrix_telegram/commands/portal/create_chat.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict +from mautrix.types import EventID from ... import portal as po from ...types import TelegramID @@ -26,7 +26,7 @@ from .util import user_has_power_level, get_initial_state help_text="Create a Telegram chat of the given type for the current Matrix room. " "The type is either `group`, `supergroup` or `channel` (defaults to " "`group`).") -async def create(evt: CommandEvent) -> Dict: +async def create(evt: CommandEvent) -> EventID: type = evt.args[0] if len(evt.args) > 0 else "group" if type not in {"chat", "group", "supergroup", "channel"}: return await evt.reply( diff --git a/mautrix_telegram/commands/portal/filter.py b/mautrix_telegram/commands/portal/filter.py index ab87114d..d02b12e4 100644 --- a/mautrix_telegram/commands/portal/filter.py +++ b/mautrix_telegram/commands/portal/filter.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Optional +from mautrix.types import EventID from ... import portal as po from .. import command_handler, CommandEvent, SECTION_ADMIN @@ -24,7 +24,7 @@ from .. import command_handler, CommandEvent, SECTION_ADMIN help_args="<`whitelist`|`blacklist`>", help_text="Change whether the bridge will allow or disallow bridging rooms by " "default.") -async def filter_mode(evt: CommandEvent) -> Dict: +async def filter_mode(evt: CommandEvent) -> EventID: try: mode = evt.args[0] if mode not in ("whitelist", "blacklist"): @@ -49,7 +49,7 @@ async def filter_mode(evt: CommandEvent) -> Dict: help_section=SECTION_ADMIN, help_args="<`whitelist`|`blacklist`> <_chat ID_>", help_text="Allow or disallow bridging a specific chat.") -async def edit_filter(evt: CommandEvent) -> Optional[Dict]: +async def edit_filter(evt: CommandEvent) -> EventID: try: action = evt.args[0] if action not in ("whitelist", "blacklist", "add", "remove"): @@ -91,4 +91,5 @@ async def edit_filter(evt: CommandEvent) -> Optional[Dict]: filter_id_list.remove(filter_id) save() return await evt.reply(f"Chat ID removed from {mode}.") - return None + else: + return await evt.reply("**Usage:** `$cmdprefix+sp filter `") diff --git a/mautrix_telegram/commands/portal/misc.py b/mautrix_telegram/commands/portal/misc.py index 21063375..d42000c2 100644 --- a/mautrix_telegram/commands/portal/misc.py +++ b/mautrix_telegram/commands/portal/misc.py @@ -13,11 +13,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict - from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError) +from mautrix.types import EventID + from ... import portal as po from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC from .util import user_has_power_level @@ -26,7 +26,7 @@ from .util import user_has_power_level @command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False, help_section=SECTION_MISC, help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.") -async def sync_state(evt: CommandEvent) -> Dict: +async def sync_state(evt: CommandEvent) -> EventID: portal = po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") @@ -40,7 +40,7 @@ async def sync_state(evt: CommandEvent) -> Dict: @command_handler(name="id", needs_admin=False, needs_puppeting=False, needs_auth=False, help_section=SECTION_MISC, help_text="Get the ID of the Telegram chat where this room is bridged.") -async def get_id(evt: CommandEvent) -> Dict: +async def get_id(evt: CommandEvent) -> EventID: portal = po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") @@ -54,7 +54,7 @@ async def get_id(evt: CommandEvent) -> Dict: @command_handler(help_section=SECTION_PORTAL_MANAGEMENT, help_text="Get a Telegram invite link to the current chat.") -async def invite_link(evt: CommandEvent) -> Dict: +async def invite_link(evt: CommandEvent) -> EventID: portal = po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") @@ -73,7 +73,7 @@ async def invite_link(evt: CommandEvent) -> Dict: @command_handler(help_section=SECTION_PORTAL_MANAGEMENT, help_text="Upgrade a normal Telegram group to a supergroup.") -async def upgrade(evt: CommandEvent) -> Dict: +async def upgrade(evt: CommandEvent) -> EventID: portal = po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") @@ -95,7 +95,7 @@ async def upgrade(evt: CommandEvent) -> Dict: help_args="<_name_|`-`>", help_text="Change the username of a supergroup/channel. " "To disable, use a dash (`-`) as the name.") -async def group_name(evt: CommandEvent) -> Dict: +async def group_name(evt: CommandEvent) -> EventID: if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp group-name `") diff --git a/mautrix_telegram/commands/portal/unbridge.py b/mautrix_telegram/commands/portal/unbridge.py index 850433b7..a947e7d3 100644 --- a/mautrix_telegram/commands/portal/unbridge.py +++ b/mautrix_telegram/commands/portal/unbridge.py @@ -15,7 +15,8 @@ # along with this program. If not, see . from typing import Dict, Callable, Optional -from ...types import MatrixRoomID +from mautrix.types import RoomID, EventID + from ... import portal as po from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT from .util import user_has_power_level @@ -24,7 +25,7 @@ from .util import user_has_power_level async def _get_portal_and_check_permission(evt: CommandEvent, permission: str, action: Optional[str] = None ) -> Optional[po.Portal]: - room_id = MatrixRoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id + room_id = RoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id portal = po.Portal.get_by_mxid(room_id) if not portal: @@ -41,7 +42,7 @@ async def _get_portal_and_check_permission(evt: CommandEvent, permission: str, def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str, completed_message: str) -> Dict: - async def post_confirm(confirm) -> Optional[Dict]: + async def post_confirm(confirm) -> Optional[EventID]: confirm.sender.command_status = None if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}": await function() @@ -62,7 +63,7 @@ def _get_portal_murder_function(action: str, room_id: str, function: Callable, c help_text="Remove all users from the current portal room and forget the portal. " "Only works for group chats; to delete a private chat portal, simply " "leave the room.") -async def delete_portal(evt: CommandEvent) -> Optional[Dict]: +async def delete_portal(evt: CommandEvent) -> Optional[EventID]: portal = await _get_portal_and_check_permission(evt, "unbridge") if not portal: return None @@ -83,7 +84,7 @@ async def delete_portal(evt: CommandEvent) -> Optional[Dict]: @command_handler(needs_auth=False, needs_puppeting=False, help_section=SECTION_PORTAL_MANAGEMENT, help_text="Remove puppets from the current portal room and forget the portal.") -async def unbridge(evt: CommandEvent) -> Optional[Dict]: +async def unbridge(evt: CommandEvent) -> Optional[EventID]: portal = await _get_portal_and_check_permission(evt, "unbridge") if not portal: return None diff --git a/mautrix_telegram/commands/portal/util.py b/mautrix_telegram/commands/portal/util.py index 98cc93f8..05c020a9 100644 --- a/mautrix_telegram/commands/portal/util.py +++ b/mautrix_telegram/commands/portal/util.py @@ -13,43 +13,48 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Tuple +from typing import Tuple, Optional -from mautrix_appservice import MatrixRequestError, IntentAPI +from mautrix.errors import MatrixRequestError +from mautrix.appservice import IntentAPI +from mautrix.types import RoomID, EventType, PowerLevelStateEventContent from ... import user as u +OptStr = Optional[str] -async def get_initial_state(intent: IntentAPI, room_id: str) -> Tuple[str, str, Dict]: - state = await intent.get_room_state(room_id) - title = None - about = None - levels = None + +async def get_initial_state(intent: IntentAPI, room_id: RoomID + ) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent]]: + state = await intent.get_state(room_id) + title: OptStr = None + about: OptStr = None + levels: Optional[PowerLevelStateEventContent] = None for event in state: try: - if event["type"] == "m.room.name": - title = event["content"]["name"] - elif event["type"] == "m.room.topic": - about = event["content"]["topic"] - elif event["type"] == "m.room.power_levels": - levels = event["content"] - elif event["type"] == "m.room.canonical_alias": - title = title or event["content"]["alias"] + if event.type == EventType.ROOM_NAME: + title = event.content.name + elif event.type == EventType.ROOM_TOPIC: + about = event.content.topic + elif event.type == EventType.ROOM_POWER_LEVELS: + levels = event.content + elif event.type == EventType.ROOM_CANONICAL_ALIAS: + title = title or event.content.canonical_alias except KeyError: # Some state event probably has empty content pass return title, about, levels -async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50 - ) -> bool: +async def user_has_power_level(room_id: RoomID, intent: IntentAPI, sender: u.User, + event: str) -> bool: if sender.is_admin: return True # Make sure the state store contains the power levels. try: - await intent.get_power_levels(room) + await intent.get_power_levels(room_id) except MatrixRequestError: return False - return intent.state_store.has_power_level(room, sender.mxid, - event=f"net.maunium.telegram.{event}", - default=default) + event_type = EventType.find(f"net.maunium.telegram.{event}") + event_type.t_class = EventType.Class.STATE + return intent.state_store.has_power_level(room_id, sender.mxid, event_type) diff --git a/mautrix_telegram/commands/telegram/account.py b/mautrix_telegram/commands/telegram/account.py index 666ce5c4..28d2bd7e 100644 --- a/mautrix_telegram/commands/telegram/account.py +++ b/mautrix_telegram/commands/telegram/account.py @@ -21,6 +21,8 @@ from telethon.tl.types import Authorization from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest, ResetAuthorizationRequest, UpdateProfileRequest) +from mautrix.types import EventID + from .. import command_handler, CommandEvent, SECTION_AUTH diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index e23ae7a6..ff2335b5 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -15,8 +15,6 @@ # along with this program. If not, see . from typing import Any, Dict, List, NamedTuple from ruamel.yaml.comments import CommentedMap -import random -import string import os from mautrix.types import UserID diff --git a/mautrix_telegram/db/__init__.py b/mautrix_telegram/db/__init__.py index d2544765..e66a6545 100644 --- a/mautrix_telegram/db/__init__.py +++ b/mautrix_telegram/db/__init__.py @@ -13,7 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from .base import Base +from mautrix.bridge.db import UserProfile, RoomState + from .bot_chat import BotChat from .message import Message from .portal import Portal diff --git a/mautrix_telegram/db/base.py b/mautrix_telegram/db/base.py deleted file mode 100644 index 266a8b53..00000000 --- a/mautrix_telegram/db/base.py +++ /dev/null @@ -1,58 +0,0 @@ -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2019 Tulir Asokan -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -from abc import abstractmethod - -from sqlalchemy import Table -from sqlalchemy.engine.base import Engine -from sqlalchemy.engine.result import RowProxy -from sqlalchemy.sql.base import ImmutableColumnCollection -from sqlalchemy.ext.declarative import declarative_base - - -class BaseBase: - db: Engine = None - t: Table = None - __table__: Table = None - c: ImmutableColumnCollection = None - - @classmethod - @abstractmethod - def _one_or_none(cls, rows: RowProxy): - pass - - @classmethod - def _select_one_or_none(cls, *args): - return cls._one_or_none(cls.db.execute(cls.t.select().where(*args))) - - @property - @abstractmethod - def _edit_identity(self): - pass - - def update(self, **values) -> None: - with self.db.begin() as conn: - conn.execute(self.t.update() - .where(self._edit_identity) - .values(**values)) - for key, value in values.items(): - setattr(self, key, value) - - def delete(self) -> None: - with self.db.begin() as conn: - conn.execute(self.t.delete().where(self._edit_identity)) - - -Base = declarative_base(cls=BaseBase) diff --git a/mautrix_telegram/db/base.pyi b/mautrix_telegram/db/base.pyi deleted file mode 100644 index 8575893d..00000000 --- a/mautrix_telegram/db/base.pyi +++ /dev/null @@ -1,26 +0,0 @@ -from abc import abstractmethod - -from sqlalchemy import Table -from sqlalchemy.engine.base import Engine -from sqlalchemy.engine.result import RowProxy -from sqlalchemy.sql.base import ImmutableColumnCollection -from sqlalchemy.ext.declarative import declarative_base - -class Base(declarative_base): - db: Engine - t: Table - __table__: Table - c: ImmutableColumnCollection - - @classmethod - @abstractmethod - def _one_or_none(cls, rows: RowProxy): ... - - @classmethod - def _select_one_or_none(cls, *args): ... - - def _edit_identity(self): ... - - def update(self, **values) -> None: ... - - def delete(self) -> None: ... diff --git a/mautrix_telegram/db/bot_chat.py b/mautrix_telegram/db/bot_chat.py index e6298102..9903a630 100644 --- a/mautrix_telegram/db/bot_chat.py +++ b/mautrix_telegram/db/bot_chat.py @@ -16,28 +16,31 @@ from typing import Iterable from sqlalchemy import Column, Integer, String +from sqlalchemy.engine.result import RowProxy + +from mautrix.bridge.db import Base from ..types import TelegramID -from .base import Base # Fucking Telegram not telling bots what chats they are in 3:< class BotChat(Base): __tablename__ = "bot_chat" - id = Column(Integer, primary_key=True) # type: TelegramID - type = Column(String, nullable=False) + id: TelegramID = Column(Integer, primary_key=True) + type: str = Column(String, nullable=False) @classmethod - def delete(cls, chat_id: TelegramID) -> None: + def delete_by_id(cls, chat_id: TelegramID) -> None: with cls.db.begin() as conn: conn.execute(cls.t.delete().where(cls.c.id == chat_id)) + @classmethod + def scan(cls, row: RowProxy) -> 'BotChat': + return cls(id=row[0], type=row[1]) + @classmethod def all(cls) -> Iterable['BotChat']: - rows = cls.db.execute(cls.t.select()) - for row in rows: - chat_id, chat_type = row - yield cls(id=chat_id, type=chat_type) + return cls._select_all() def insert(self) -> None: with self.db.begin() as conn: diff --git a/mautrix_telegram/db/message.py b/mautrix_telegram/db/message.py index c1c942e2..eb0a26df 100644 --- a/mautrix_telegram/db/message.py +++ b/mautrix_telegram/db/message.py @@ -13,42 +13,35 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional, Iterator + from sqlalchemy import Column, UniqueConstraint, Integer, String, and_, func, desc, select from sqlalchemy.engine.result import RowProxy -from typing import Optional, List +from sqlalchemy.sql.expression import ClauseElement -from ..types import MatrixRoomID, MatrixEventID, TelegramID -from .base import Base +from mautrix.types import RoomID, EventID +from mautrix.bridge.db import Base + +from ..types import TelegramID class Message(Base): __tablename__ = "message" - mxid = Column(String) # type: MatrixEventID - mx_room = Column(String) # type: MatrixRoomID - tgid = Column(Integer, primary_key=True) # type: TelegramID - tg_space = Column(Integer, primary_key=True) # type: TelegramID - edit_index = Column(Integer, primary_key=True) # type: int + mxid: EventID = Column(String) + mx_room: RoomID = Column(String) + tgid: TelegramID = Column(Integer, primary_key=True) + tg_space: TelegramID = Column(Integer, primary_key=True) + edit_index: int = Column(Integer, primary_key=True) __table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"),) @classmethod - def _one_or_none(cls, rows: RowProxy) -> Optional['Message']: - try: - mxid, mx_room, tgid, tg_space, edit_index = next(rows) - return cls(mxid=mxid, mx_room=mx_room, tgid=tgid, tg_space=tg_space, - edit_index=edit_index) - except StopIteration: - return None - - @staticmethod - def _all(rows: RowProxy) -> List['Message']: - return [Message(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3], - edit_index=row[4]) - for row in rows] + def scan(cls, row: RowProxy) -> 'Message': + return cls(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3], edit_index=row[4]) @classmethod - def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> List['Message']: + def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> Iterator['Message']: return cls._all(cls.db.execute(cls.t.select().where(and_(cls.c.tgid == tgid, cls.c.tg_space == tg_space)))) @@ -68,7 +61,7 @@ class Message(Base): return cls._one_or_none(cls.db.execute(query)) @classmethod - def count_spaces_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID) -> int: + def count_spaces_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> int: rows = cls.db.execute(select([func.count(cls.c.tg_space)]) .where(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room))) try: @@ -78,7 +71,7 @@ class Message(Base): return 0 @classmethod - def get_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID, tg_space: TelegramID + def get_by_mxid(cls, mxid: EventID, mx_room: RoomID, tg_space: TelegramID ) -> Optional['Message']: return cls._select_one_or_none(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room, @@ -94,14 +87,14 @@ class Message(Base): .values(**values)) @classmethod - def update_by_mxid(cls, s_mxid: MatrixEventID, s_mx_room: MatrixRoomID, **values) -> None: + def update_by_mxid(cls, s_mxid: EventID, s_mx_room: RoomID, **values) -> None: with cls.db.begin() as conn: conn.execute(cls.t.update() .where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room)) .values(**values)) @property - def _edit_identity(self): + def _edit_identity(self) -> ClauseElement: return and_(self.c.tgid == self.tgid, self.c.tg_space == self.tg_space, self.c.edit_index == self.edit_index) diff --git a/mautrix_telegram/db/portal.py b/mautrix_telegram/db/portal.py index 245b1df5..c5a04846 100644 --- a/mautrix_telegram/db/portal.py +++ b/mautrix_telegram/db/portal.py @@ -13,55 +13,52 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import Column, Integer, String, Boolean, Text, and_ -from sqlalchemy.engine.result import RowProxy from typing import Optional -from ..types import MatrixRoomID, TelegramID -from .base import Base +from sqlalchemy import Column, Integer, String, Boolean, Text, and_ +from sqlalchemy.engine.result import RowProxy +from sqlalchemy.sql.expression import ClauseElement + +from mautrix.types import RoomID +from mautrix.bridge.db import Base + +from ..types import TelegramID class Portal(Base): __tablename__ = "portal" # Telegram chat information - tgid = Column(Integer, primary_key=True) # type: TelegramID - tg_receiver = Column(Integer, primary_key=True) # type: TelegramID - peer_type = Column(String, nullable=False) - megagroup = Column(Boolean) + tgid: TelegramID = Column(Integer, primary_key=True) + tg_receiver: TelegramID = Column(Integer, primary_key=True) + peer_type: str = Column(String, nullable=False) + megagroup: bool = Column(Boolean) # Matrix portal information - mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID] + mxid: RoomID = Column(String, unique=True, nullable=True) - config = Column(Text, nullable=True) + config: str = Column(Text, nullable=True) # Telegram chat metadata - username = Column(String, nullable=True) - title = Column(String, nullable=True) - about = Column(String, nullable=True) - photo_id = Column(String, nullable=True) + username: str = Column(String, nullable=True) + title: str = Column(String, nullable=True) + about: str = Column(String, nullable=True) + photo_id: str = Column(String, nullable=True) @classmethod - def scan(cls, row) -> Optional['Portal']: + def scan(cls, row: RowProxy) -> Optional['Portal']: (tgid, tg_receiver, peer_type, megagroup, mxid, config, username, title, about, photo_id) = row return cls(tgid=tgid, tg_receiver=tg_receiver, peer_type=peer_type, megagroup=megagroup, mxid=mxid, config=config, username=username, title=title, about=about, photo_id=photo_id) - @classmethod - def _one_or_none(cls, rows: RowProxy) -> Optional['Portal']: - try: - return cls.scan(next(rows)) - except StopIteration: - return None - @classmethod def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Optional['Portal']: return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_receiver == tg_receiver)) @classmethod - def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['Portal']: + def get_by_mxid(cls, mxid: RoomID) -> Optional['Portal']: return cls._select_one_or_none(cls.c.mxid == mxid) @classmethod @@ -69,7 +66,7 @@ class Portal(Base): return cls._select_one_or_none(cls.c.username == username) @property - def _edit_identity(self): + def _edit_identity(self) -> ClauseElement: return and_(self.c.tgid == self.tgid, self.c.tg_receiver == self.tg_receiver) def insert(self) -> None: diff --git a/mautrix_telegram/db/puppet.py b/mautrix_telegram/db/puppet.py index b00c1dbe..bc22d09c 100644 --- a/mautrix_telegram/db/puppet.py +++ b/mautrix_telegram/db/puppet.py @@ -13,31 +13,35 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import Column, Integer, String, Boolean -from sqlalchemy.engine.result import RowProxy -from sqlalchemy.sql import expression from typing import Optional, Iterable -from ..types import MatrixUserID, MatrixRoomID, TelegramID -from .base import Base +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.sql import expression +from sqlalchemy.engine.result import RowProxy +from sqlalchemy.sql.expression import ClauseElement + +from mautrix.types import UserID +from mautrix.bridge.db import Base + +from ..types import TelegramID class Puppet(Base): __tablename__ = "puppet" - id = Column(Integer, primary_key=True) # type: TelegramID - custom_mxid = Column(String, nullable=True) # type: Optional[MatrixUserID] - access_token = Column(String, nullable=True) - displayname = Column(String, nullable=True) - displayname_source = Column(Integer, nullable=True) # type: Optional[TelegramID] - username = Column(String, nullable=True) - photo_id = Column(String, nullable=True) - is_bot = Column(Boolean, nullable=True) - matrix_registered = Column(Boolean, nullable=False, server_default=expression.false()) - disable_updates = Column(Boolean, nullable=False, server_default=expression.false()) + id: TelegramID = Column(Integer, primary_key=True) + custom_mxid: UserID = Column(String, nullable=True) + access_token: str = Column(String, nullable=True) + displayname: str = Column(String, nullable=True) + displayname_source: TelegramID = Column(Integer, nullable=True) + username: str = Column(String, nullable=True) + photo_id: str = Column(String, nullable=True) + is_bot: bool = Column(Boolean, nullable=True) + matrix_registered: bool = Column(Boolean, nullable=False, server_default=expression.false()) + disable_updates: bool = Column(Boolean, nullable=False, server_default=expression.false()) @classmethod - def scan(cls, row) -> Optional['Puppet']: + def scan(cls, row: RowProxy) -> Optional['Puppet']: (id, custom_mxid, access_token, displayname, displayname_source, username, photo_id, is_bot, matrix_registered, disable_updates) = row return cls(id=id, custom_mxid=custom_mxid, access_token=access_token, @@ -45,13 +49,6 @@ class Puppet(Base): username=username, photo_id=photo_id, is_bot=is_bot, matrix_registered=matrix_registered, disable_updates=disable_updates) - @classmethod - def _one_or_none(cls, rows: RowProxy) -> Optional['Puppet']: - try: - return cls.scan(next(rows)) - except StopIteration: - return None - @classmethod def all_with_custom_mxid(cls) -> Iterable['Puppet']: rows = cls.db.execute(cls.t.select().where(cls.c.custom_mxid != None)) @@ -63,7 +60,7 @@ class Puppet(Base): return cls._select_one_or_none(cls.c.id == tgid) @classmethod - def get_by_custom_mxid(cls, mxid: MatrixUserID) -> Optional['Puppet']: + def get_by_custom_mxid(cls, mxid: UserID) -> Optional['Puppet']: return cls._select_one_or_none(cls.c.custom_mxid == mxid) @classmethod @@ -75,7 +72,7 @@ class Puppet(Base): return cls._select_one_or_none(cls.c.displayname == displayname) @property - def _edit_identity(self): + def _edit_identity(self) -> ClauseElement: return self.c.id == self.id def insert(self) -> None: diff --git a/mautrix_telegram/db/telegram_file.py b/mautrix_telegram/db/telegram_file.py index f0e4b045..658d2f55 100644 --- a/mautrix_telegram/db/telegram_file.py +++ b/mautrix_telegram/db/telegram_file.py @@ -13,40 +13,40 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean from typing import Optional -from mautrix.types import ContentURI +from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean +from sqlalchemy.engine.result import RowProxy -from .base import Base +from mautrix.types import ContentURI +from mautrix.bridge.db import Base class TelegramFile(Base): __tablename__ = "telegram_file" - id = Column(String, primary_key=True) + id: str = Column(String, primary_key=True) mxc: ContentURI = Column(String) - mime_type = Column(String) - was_converted = Column(Boolean) - timestamp = Column(BigInteger) - size = Column(Integer, nullable=True) - width = Column(Integer, nullable=True) - height = Column(Integer, nullable=True) - thumbnail_id = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True) - thumbnail = None # type: Optional[TelegramFile] + mime_type: str = Column(String) + was_converted: bool = Column(Boolean) + timestamp: int = Column(BigInteger) + size: int = Column(Integer, nullable=True) + width: int = Column(Integer, nullable=True) + height: int = Column(Integer, nullable=True) + thumbnail_id: str = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True) + thumbnail: Optional['TelegramFile'] = None + + def scan(cls, row: RowProxy) -> 'TelegramFile': + loc_id, mxc, mime, conv, ts, s, w, h, thumb_id = row + thumb = None + if thumb_id: + thumb = cls.get(thumb_id) + return cls(id=loc_id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts, + size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb) @classmethod def get(cls, loc_id: str) -> Optional['TelegramFile']: - rows = cls.db.execute(cls.t.select().where(cls.c.id == loc_id)) - try: - loc_id, mxc, mime, conv, ts, s, w, h, thumb_id = next(rows) - thumb = None - if thumb_id: - thumb = cls.get(thumb_id) - return cls(id=loc_id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts, - size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb) - except StopIteration: - return None + return cls._select_one_or_none(cls.c.id == loc_id) def insert(self) -> None: with self.db.begin() as conn: diff --git a/mautrix_telegram/db/user.py b/mautrix_telegram/db/user.py index 2f5a0fee..812230a1 100644 --- a/mautrix_telegram/db/user.py +++ b/mautrix_telegram/db/user.py @@ -13,46 +13,43 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String -from sqlalchemy.engine.result import RowProxy from typing import Optional, Iterable, Tuple -from ..types import MatrixUserID, TelegramID -from .base import Base +from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String +from sqlalchemy.engine.result import RowProxy +from sqlalchemy.sql.expression import ClauseElement + +from mautrix.types import UserID +from mautrix.bridge.db import Base + +from ..types import TelegramID class User(Base): __tablename__ = "user" - mxid = Column(String, primary_key=True) # type: MatrixUserID - tgid = Column(Integer, nullable=True, unique=True) # type: Optional[TelegramID] - tg_username = Column(String, nullable=True) - tg_phone = Column(String, nullable=True) - saved_contacts = Column(Integer, default=0, nullable=False) + mxid: UserID = Column(String, primary_key=True) + tgid: Optional[TelegramID] = Column(Integer, nullable=True, unique=True) + tg_username: str = Column(String, nullable=True) + tg_phone: str = Column(String, nullable=True) + saved_contacts: int = Column(Integer, default=0, nullable=False) @classmethod - def _one_or_none(cls, rows: RowProxy) -> Optional['User']: - try: - mxid, tgid, tg_username, tg_phone, saved_contacts = next(rows) - return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone, - saved_contacts=saved_contacts) - except StopIteration: - return None + def scan(cls, row: RowProxy) -> 'User': + mxid, tgid, tg_username, tg_phone, saved_contacts = row + return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone, + saved_contacts=saved_contacts) @classmethod def all(cls) -> Iterable['User']: - rows = cls.db.execute(cls.t.select()) - for row in rows: - mxid, tgid, tg_username, tg_phone, saved_contacts = row - yield cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone, - saved_contacts=saved_contacts) + return cls._select_all() @classmethod def get_by_tgid(cls, tgid: TelegramID) -> Optional['User']: return cls._select_one_or_none(cls.c.tgid == tgid) @classmethod - def get_by_mxid(cls, mxid: MatrixUserID) -> Optional['User']: + def get_by_mxid(cls, mxid: UserID) -> Optional['User']: return cls._select_one_or_none(cls.c.mxid == mxid) @classmethod @@ -60,7 +57,7 @@ class User(Base): return cls._select_one_or_none(cls.c.tg_username == username) @property - def _edit_identity(self): + def _edit_identity(self) -> ClauseElement: return self.c.mxid == self.mxid def insert(self) -> None: @@ -112,10 +109,10 @@ class User(Base): class UserPortal(Base): __tablename__ = "user_portal" - user = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", ondelete="CASCADE"), - primary_key=True) # type: TelegramID - portal = Column(Integer, primary_key=True) # type: TelegramID - portal_receiver = Column(Integer, primary_key=True) # type: TelegramID + user: TelegramID = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", + ondelete="CASCADE"), primary_key=True) + portal: TelegramID = Column(Integer, primary_key=True) + portal_receiver: TelegramID = Column(Integer, primary_key=True) __table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"), ("portal.tgid", "portal.tg_receiver"), @@ -125,5 +122,5 @@ class UserPortal(Base): class Contact(Base): __tablename__ = "contact" - user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) # type: TelegramID - contact = Column(Integer, ForeignKey("puppet.id"), primary_key=True) # type: TelegramID + user: TelegramID = Column(Integer, ForeignKey("user.tgid"), primary_key=True) + contact: TelegramID = Column(Integer, ForeignKey("puppet.id"), primary_key=True) diff --git a/mautrix_telegram/formatter/from_telegram.py b/mautrix_telegram/formatter/from_telegram.py index b3273249..fb23528b 100644 --- a/mautrix_telegram/formatter/from_telegram.py +++ b/mautrix_telegram/formatter/from_telegram.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Optional, Tuple, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from html import escape import logging import re @@ -26,14 +26,15 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M MessageEntityBlockquote, MessageEntityStrike, MessageFwdHeader, MessageEntityUnderline, PeerUser) -from mautrix_appservice import MatrixRequestError -from mautrix_appservice.intent_api import IntentAPI +from mautrix.errors import MatrixRequestError +from mautrix.appservice import IntentAPI +from mautrix.types import (TextMessageEventContent, RelatesTo, RelationType, Format, MessageType, + MessageEvent) from .. import user as u, puppet as pu, portal as po from ..types import TelegramID from ..db import Message as DBMessage -from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html, - trim_reply_fallback_text) +from .util import (add_surrogates, remove_surrogates) if TYPE_CHECKING: from ..abstract_user import AbstractUser @@ -41,29 +42,22 @@ if TYPE_CHECKING: log: logging.Logger = logging.getLogger("mau.fmt.tg") -def telegram_reply_to_matrix(evt: Message, source: 'AbstractUser') -> Dict: +def telegram_reply_to_matrix(evt: Message, source: 'AbstractUser') -> Optional[RelatesTo]: if evt.reply_to_msg_id: space = (evt.to_id.channel_id if isinstance(evt, Message) and isinstance(evt.to_id, PeerChannel) else source.tgid) msg = DBMessage.get_one_by_tgid(evt.reply_to_msg_id, space) if msg: - return { - "m.in_reply_to": { - "event_id": msg.mxid, - "room_id": msg.mx_room, - }, - "rel_type": "m.reference", - "event_id": msg.mxid, - "room_id": msg.mx_room, - } - return {} + return RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid) + return None -async def _add_forward_header(source, text: str, html: Optional[str], - fwd_from: MessageFwdHeader) -> Tuple[str, str]: - if not html: - html = escape(text) +async def _add_forward_header(source: 'AbstractUser', content: TextMessageEventContent, + fwd_from: MessageFwdHeader) -> None: + if not content.formatted_body or content.format != Format.HTML: + content.format = Format.HTML + content.formatted_body = escape(content.body) fwd_from_html, fwd_from_text = None, None if fwd_from.from_id: user = u.User.get_by_tgid(TelegramID(fwd_from.from_id)) @@ -106,64 +100,32 @@ async def _add_forward_header(source, text: str, html: Optional[str], fwd_from_text = "Unknown source" fwd_from_html = f"{fwd_from_text}" - text = "\n".join([f"> {line}" for line in text.split("\n")]) - text = f"Forwarded from {fwd_from_text}:\n{text}" - html = (f"Forwarded message from {fwd_from_html}
" - f"
{html}
") - return text, html + content.body = "\n".join([f"> {line}" for line in content.body.split("\n")]) + content.body = f"Forwarded from {fwd_from_text}:\n{content.body}" + content.formatted_body = ( + f"Forwarded message from {fwd_from_html}
" + f"
{content.formatted_body}
") -async def _add_reply_header(source: "AbstractUser", text: str, html: str, evt: Message, - relates_to: Dict, main_intent: IntentAPI) -> Tuple[str, str]: +async def _add_reply_header(source: 'AbstractUser', content: TextMessageEventContent, evt: Message, + main_intent: IntentAPI): space = (evt.to_id.channel_id if isinstance(evt, Message) and isinstance(evt.to_id, PeerChannel) else source.tgid) msg = DBMessage.get_one_by_tgid(evt.reply_to_msg_id, space) if not msg: - return text, html + return - relates_to["rel_type"] = "m.reference" - relates_to["event_id"] = msg.mxid - relates_to["room_id"] = msg.mx_room - relates_to["m.in_reply_to"] = { - "event_id": msg.mxid, - "room_id": msg.mx_room, - } + content.relates_to = RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid) try: - event = await main_intent.get_event(msg.mx_room, msg.mxid) - - content = event["content"] - r_sender = event["sender"] - - r_text_body = trim_reply_fallback_text(content["body"]) - r_html_body = trim_reply_fallback_html(content["formatted_body"] - if "formatted_body" in content - else escape(content["body"])) - - puppet = pu.Puppet.get_by_mxid(r_sender, create=False) - r_displayname = puppet.displayname if puppet else r_sender - r_sender_link = f"{escape(r_displayname)}" - except (ValueError, KeyError, MatrixRequestError): - r_sender_link = "unknown user" - r_displayname = "unknown user" - r_text_body = "Failed to fetch message" - r_html_body = "Failed to fetch message" - - r_msg_link = f"In reply to" - html = ( - f"
{r_msg_link} {r_sender_link}\n{r_html_body}
" - + (html or escape(text))) - - lines = r_text_body.strip().split("\n") - text_with_quote = f"> <{r_displayname}> {lines.pop(0)}" - for line in lines: - if line: - text_with_quote += f"\n> {line}" - text_with_quote += "\n\n" - text_with_quote += text - return text_with_quote, html + event: MessageEvent = await main_intent.get_event(msg.mx_room, msg.mxid) + if isinstance(event.content, TextMessageEventContent): + event.content.trim_reply_fallback() + content.set_reply(event) + except MatrixRequestError: + pass async def telegram_to_matrix(evt: Message, source: "AbstractUser", @@ -171,33 +133,43 @@ async def telegram_to_matrix(evt: Message, source: "AbstractUser", prefix_text: Optional[str] = None, prefix_html: Optional[str] = None, override_text: str = None, override_entities: List[TypeMessageEntity] = None, - no_reply_fallback: bool = False) -> Tuple[str, str, Dict]: - text = add_surrogates(override_text or evt.message) + no_reply_fallback: bool = False) -> TextMessageEventContent: + content = TextMessageEventContent( + msgtype=MessageType.TEXT, + body=add_surrogates(override_text or evt.message), + ) entities = override_entities or evt.entities - html = _telegram_entities_to_matrix_catch(text, entities) if entities else None - relates_to = {} # type: Dict + if entities: + content.format = Format.HTML + content.formatted_body = _telegram_entities_to_matrix_catch(content.body, entities) if prefix_html: - html = prefix_html + (html or escape(text)) + if not content.formatted_body: + content.format = Format.HTML + content.formatted_body = escape(content.body) + content.formatted_body = prefix_html + content.formatted_body if prefix_text: - text = prefix_text + text + content.body = prefix_text + content.body if evt.fwd_from: - text, html = await _add_forward_header(source, text, html, evt.fwd_from) + await _add_forward_header(source, content, evt.fwd_from) if evt.reply_to_msg_id and not no_reply_fallback: - text, html = await _add_reply_header(source, text, html, evt, relates_to, main_intent) + await _add_reply_header(source, content, evt, main_intent) if isinstance(evt, Message) and evt.post and evt.post_author: - if not html: - html = escape(text) - text += f"\n- {evt.post_author}" - html += f"
- {evt.post_author}" + if not content.formatted_body: + content.formatted_body = escape(content.body) + content.body += f"\n- {evt.post_author}" + content.formatted_body += f"
- {evt.post_author}" - if html: - html = html.replace("\n", "
") + if content.formatted_body: + content.formatted_body = content.formatted_body.replace("\n", "
") - return remove_surrogates(text), remove_surrogates(html), relates_to + content.body = remove_surrogates(content.body) + content.formatted_body = remove_surrogates(content.formatted_body) + + return content def _telegram_entities_to_matrix_catch(text: str, entities: List[TypeMessageEntity]) -> str: diff --git a/mautrix_telegram/formatter/util.py b/mautrix_telegram/formatter/util.py index 6c6dfb10..bde76adb 100644 --- a/mautrix_telegram/formatter/util.py +++ b/mautrix_telegram/formatter/util.py @@ -32,24 +32,3 @@ def remove_surrogates(text: Optional[str]) -> Optional[str]: if text is None: return None return text.encode("utf-16", "surrogatepass").decode("utf-16") - - -# trim_reply_fallback_text, html_reply_fallback_regex and trim_reply_fallback_html are Matrix -# reply fallback utility functions. -# You may copy and use them under any OSI-approved license. -def trim_reply_fallback_text(text: str) -> str: - if not text.startswith("> ") or "\n" not in text: - return text - lines = text.split("\n") - while len(lines) > 0 and lines[0].startswith("> "): - lines.pop(0) - return "\n".join(lines) - - -html_reply_fallback_regex: Pattern = re.compile("^" - r"[\s\S]+?" - "") - - -def trim_reply_fallback_html(html: str) -> str: - return html_reply_fallback_regex.sub("", html) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index d9f329e3..6a6a523d 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import (Awaitable, Dict, List, Optional, Pattern, Tuple, Union, Any, Deque, cast, +from typing import (Awaitable, Dict, List, Optional, Pattern, Tuple, Union, Any, Deque, TYPE_CHECKING) from html import escape as escape_html from collections import deque @@ -64,14 +64,17 @@ from telethon.tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre, InputMediaUploadedDocument, InputPeerPhotoFileLocation) -from mautrix.errors import MatrixRequestError, IntentError +from mautrix.errors import MatrixRequestError, IntentError, MForbidden from mautrix.appservice import AppService, IntentAPI -from mautrix.types import EventID, RoomID, UserID, RoomCreatePreset, ContentURI, MessageType +from mautrix.bridge import BasePortal +from mautrix.types import (EventID, RoomID, UserID, RoomCreatePreset, ContentURI, MessageType, + ImageInfo, ThumbnailInfo, EventType, PowerLevelStateEventContent, + RoomAlias, TextMessageEventContent, Format) from .types import TelegramID from .context import Context from .db import Portal as DBPortal, Message as DBMessage, TelegramFile as DBTelegramFile -from .util import ignore_coro, sane_mimetypes +from .util import sane_mimetypes from . import puppet as p, user as u, formatter, util if TYPE_CHECKING: @@ -88,7 +91,7 @@ DedupMXID = Tuple[EventID, TelegramID] InviteList = Union[UserID, List[UserID]] -class Portal: +class Portal(BasePortal): base_log: logging.Logger = logging.getLogger("mau.portal") az: AppService = None bot: 'Bot' = None @@ -225,7 +228,7 @@ class Portal: # endregion # region Permission checks - async def can_user_perform(self, user: 'u.User', event: str, default: int = 50) -> bool: + async def can_user_perform(self, user: 'u.User', event: str) -> bool: if user.is_admin: return True if not self.mxid: @@ -235,10 +238,9 @@ class Portal: await self.main_intent.get_power_levels(self.mxid) except MatrixRequestError: return False - return self.main_intent.state_store.has_power_level( - self.mxid, user.mxid, - event=f"net.maunium.telegram.{event}", - default=default) + evt_type = EventType.find(f"net.maunium.telegram.{event}") + evt_type.t_class = EventType.Class.STATE + return self.main_intent.state_store.has_power_level(self.mxid, user.mxid, event=evt_type) # endregion # region Deduplication @@ -340,7 +342,8 @@ class Portal: await self.main_intent.invite_user(self.mxid, users, check_cache=True) async def update_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User], - direct: bool, puppet: p.Puppet = None, levels: Dict = None, + direct: bool, puppet: p.Puppet = None, + levels: PowerLevelStateEventContent = None, users: List[User] = None, participants: List[TypeParticipant] = None) -> None: if not direct: @@ -368,7 +371,7 @@ class Portal: if synchronous: await update else: - ignore_coro(asyncio.ensure_future(update, loop=self.loop)) + asyncio.ensure_future(update, loop=self.loop) await self.invite_to_matrix(invites or []) return self.mxid async with self._room_create_lock: @@ -388,7 +391,7 @@ class Portal: entity = await self.get_entity(user) self.log.debug("Fetched data: %s", entity) - self.log.debug(f"Creating room") + self.log.debug("Creating room") try: self.title = entity.title @@ -414,14 +417,14 @@ class Portal: # TODO? properly handle existing room aliases await self.main_intent.remove_room_alias(alias) - power_levels = self._get_base_power_levels({}, entity) + power_levels = self._get_base_power_levels(entity=entity) users = participants = None if not direct: users, participants = await self._get_users(user, entity) self._participants_to_power_levels(participants, power_levels) initial_state = [{ - "type": "m.room.power_levels", - "content": power_levels, + "type": EventType.ROOM_POWER_LEVELS.serialize(), + "content": power_levels.serialize(), }] if config["appservice.community_id"]: initial_state.append({ @@ -440,62 +443,56 @@ class Portal: self.save() self.az.state_store.set_power_levels(self.mxid, power_levels) user.register_portal(self) - ignore_coro(asyncio.ensure_future(self.update_matrix_room(user, entity, direct, puppet, - levels=power_levels, users=users, - participants=participants), - loop=self.loop)) + asyncio.ensure_future(self.update_matrix_room(user, entity, direct, puppet, + levels=power_levels, users=users, + participants=participants), loop=self.loop) return self.mxid - def _get_base_power_levels(self, levels: dict = None, entity: TypeChat = None) -> dict: - levels = levels or {} + def _get_base_power_levels(self, levels: PowerLevelStateEventContent = None, + entity: TypeChat = None) -> PowerLevelStateEventContent: + levels = levels or PowerLevelStateEventContent() if self.peer_type == "user": - levels["ban"] = 100 - levels["kick"] = 100 - levels["invite"] = 100 - levels.setdefault("events", {}) - levels["events"]["m.room.name"] = 0 - levels["events"]["m.room.avatar"] = 0 - levels["events"]["m.room.topic"] = 0 - levels["state_default"] = 0 - levels["users_default"] = 0 - levels["events_default"] = 0 + levels.ban = 100 + levels.kick = 100 + levels.invite = 100 + levels.events[EventType.ROOM_NAME] = 0 + levels.events[EventType.ROOM_AVATAR] = 0 + levels.events[EventType.ROOM_TOPIC] = 0 + levels.state_default = 0 + levels.users_default = 0 + levels.events_default = 0 else: dbr = entity.default_banned_rights if not dbr: self.log.debug(f"default_banned_rights is None in {entity}") dbr = ChatBannedRights(invite_users=True, change_info=True, pin_messages=True, send_stickers=False, send_messages=False, until_date=0) - levels["ban"] = 99 - levels["kick"] = 50 - levels["invite"] = 50 if dbr.invite_users else 0 - levels.setdefault("events", {}) - levels["events"]["m.room.name"] = 50 if dbr.change_info else 0 - levels["events"]["m.room.avatar"] = 50 if dbr.change_info else 0 - levels["events"]["m.room.topic"] = 50 if dbr.change_info else 0 - levels["events"][ - "m.room.pinned_events"] = 50 if dbr.pin_messages else 0 - levels["events"]["m.room.power_levels"] = 75 - levels["events"]["m.room.history_visibility"] = 75 - levels["state_default"] = 50 - levels["users_default"] = 0 - levels["events_default"] = (50 if (self.peer_type == "channel" and not entity.megagroup - or entity.default_banned_rights.send_messages) - else 0) - levels["events"]["m.sticker"] = 50 if dbr.send_stickers else levels["events_default"] - if "users" not in levels: - levels["users"] = { - self.main_intent.mxid: 100 - } - else: - levels["users"][self.main_intent.mxid] = 100 + levels.ban = 99 + levels.kick = 50 + levels.invite = 50 if dbr.invite_users else 0 + levels.events[EventType.ROOM_ENCRYPTED] = 99 + levels.events[EventType.ROOM_TOMBSTONE] = 99 + levels.events[EventType.ROOM_NAME] = 50 if dbr.change_info else 0 + levels.events[EventType.ROOM_AVATAR] = 50 if dbr.change_info else 0 + levels.events[EventType.ROOM_TOPIC] = 50 if dbr.change_info else 0 + levels.events[EventType.ROOM_PINNED_EVENTS] = 50 if dbr.pin_messages else 0 + levels.events[EventType.ROOM_POWER_LEVELS] = 75 + levels.events[EventType.ROOM_HISTORY_VISIBILITY] = 75 + levels.state_default = 50 + levels.users_default = 0 + levels.events_default = (50 if (self.peer_type == "channel" and not entity.megagroup + or entity.default_banned_rights.send_messages) + else 0) + levels.events[EventType.STICKER] = 50 if dbr.send_stickers else levels.events_default + levels.users[self.main_intent.mxid] = 100 return levels @property - def alias(self) -> Optional[str]: + def alias(self) -> Optional[RoomAlias]: if not self.username: return None - return f"#{self._get_alias_localpart()}:{self.hs_domain}" + return RoomAlias(f"#{self._get_alias_localpart()}:{self.hs_domain}") def _get_alias_localpart(self, username: Optional[str] = None) -> Optional[str]: username = username or self.username @@ -537,8 +534,7 @@ class Portal: and Portal.max_initial_member_sync == -1 and (self.megagroup or self.peer_type != "channel")) if trust_member_list: - joined_mxids = cast(List[UserID], - await self.main_intent.get_room_members(self.mxid)) + joined_mxids = await self.main_intent.get_room_members(self.mxid) for user_mxid in joined_mxids: if user_mxid == self.az.bot_mxid: continue @@ -547,7 +543,7 @@ class Portal: if self.bot and puppet_id == self.bot.tgid: self.bot.remove_chat(self.tgid) await self.main_intent.kick_user(self.mxid, user_mxid, - "User had left this Telegram chat.") + "User had left this Telegram chat.") continue mx_user = u.User.get_by_mxid(user_mxid, create=False) if mx_user and mx_user.is_bot and mx_user.tgid not in allowed_tgids: @@ -555,14 +551,14 @@ class Portal: if mx_user and not self.has_bot and mx_user.tgid not in allowed_tgids: await self.main_intent.kick_user(self.mxid, mx_user.mxid, - "You had left this Telegram chat.") + "You had left this Telegram chat.") continue async def add_telegram_user(self, user_id: TelegramID, source: Optional['AbstractUser'] = None ) -> None: puppet = p.Puppet.get(user_id) if source: - entity = await source.client.get_entity(PeerUser(user_id)) # type: User + entity: User = await source.client.get_entity(PeerUser(user_id)) await puppet.update_info(source, entity) await puppet.intent.join_room(self.mxid) @@ -577,12 +573,21 @@ class Portal: kick_message = (f"Kicked by {sender.displayname}" if sender and sender.tgid != puppet.tgid else "Left Telegram chat") - if sender and sender.tgid != puppet.tgid: - await self.main_intent.kick_user(self.mxid, puppet.mxid, kick_message) + if sender.tgid != puppet.tgid: + try: + await sender.intent.kick_user(self.mxid, puppet.mxid) + except MForbidden: + await self.main_intent.kick_user(self.mxid, puppet.mxid, kick_message) else: await puppet.intent.leave_room(self.mxid) if user: user.unregister_portal(self) + if sender.tgid != puppet.tgid: + try: + await sender.intent.kick_user(self.mxid, puppet.mxid) + return + except MForbidden: + pass await self.main_intent.kick_user(self.mxid, user.mxid, kick_message) async def update_info(self, user: 'AbstractUser', entity: TypeChat = None) -> None: @@ -706,7 +711,7 @@ class Portal: return False async def _get_users(self, user: 'AbstractUser', - entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser] + entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser, InputChannel] ) -> Tuple[List[TypeUser], List[TypeParticipant]]: if self.peer_type == "chat": chat = await user.client(GetFullChatRequest(chat_id=self.tgid)) @@ -764,13 +769,13 @@ class Portal: members = await self.main_intent.get_room_members(self.mxid) except MatrixRequestError: return [] - authenticated = [] # type: List[u.User] + authenticated: List[u.User] = [] has_bot = self.has_bot for member_str in members: member = UserID(member_str) if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid: continue - user = await u.User.get_by_mxid(member).ensure_started() # type: u.User + user = await u.User.get_by_mxid(member).ensure_started() authenticated_through_bot = has_bot and user.relaybot_whitelisted if authenticated_through_bot or await user.has_full_access(allow_bot=True): authenticated.append(user) @@ -825,30 +830,28 @@ class Portal: return local return config[f"bridge.{key}"] - async def _get_state_change_message(self, event: str, user: 'u.User', - arguments: Optional[Dict] = None) -> Optional[Dict]: + async def _get_state_change_message(self, event: str, user: 'u.User', **kwargs: Any + ) -> Optional[str]: tpl = self.get_config(f"state_event_formats.{event}") if len(tpl) == 0: # Empty format means they don't want the message return None displayname = await self.get_displayname(user) - tpl_args = dict(mxid=user.mxid, - username=user.mxid_localpart, - displayname=escape_html(displayname)) - tpl_args = {**tpl_args, **(arguments or {})} - message = Template(tpl).safe_substitute(tpl_args) - return { - "format": "org.matrix.custom.html", - "formatted_body": message, + tpl_args = { + "mxid": user.mxid, + "username": user.mxid_localpart, + "displayname": escape_html(displayname), + **kwargs, } + return Template(tpl).safe_substitute(tpl_args) async def name_change_matrix(self, user: 'u.User', displayname: str, prev_displayname: str, event_id: EventID) -> None: async with self.require_send_lock(self.bot.tgid): message = await self._get_state_change_message( "name_change", user, - dict(displayname=displayname, prev_displayname=prev_displayname)) + displayname=displayname, prev_displayname=prev_displayname) if not message: return response = await self.bot.client.send_message( @@ -858,7 +861,7 @@ class Portal: self.is_duplicate(response, (event_id, space)) async def get_displayname(self, user: 'u.User') -> str: - # FIXME mautrix4 + # FIXME this doesn't seem to use cache in mautrix 0.4 return (await self.main_intent.get_displayname(self.mxid, user.mxid) or user.mxid) @@ -994,13 +997,15 @@ class Portal: await self._apply_msg_format(sender, msgtype, message["m.new_content"]) @staticmethod - def _matrix_event_to_entities(event: Dict[str, Any] + def _matrix_event_to_entities(event: Union[str, TextMessageEventContent] ) -> Tuple[str, Optional[List[TypeMessageEntity]]]: try: - if event.get("format", None) == "org.matrix.custom.html": - message, entities = formatter.matrix_to_telegram(event.get("formatted_body", "")) + if isinstance(event, str): + message, entities = formatter.matrix_to_telegram(event) + elif isinstance(event, TextMessageEventContent) and event.format == Format.HTML: + message, entities = formatter.matrix_to_telegram(event.formatted_body) else: - message, entities = formatter.matrix_text_to_telegram(event.get("body", "")) + message, entities = formatter.matrix_text_to_telegram(event.body) except KeyError: message, entities = None, None return message, entities @@ -1403,8 +1408,7 @@ class Portal: self.bot.add_chat(self.tgid, self.peer_type) levels = await self.main_intent.get_power_levels(self.mxid) - bot_level = self._get_bot_level(levels) - if bot_level == 100: + if levels.get_user_level(self.main_intent.mxid) == 100: levels = self._get_base_power_levels(levels, entity) await self.main_intent.set_power_levels(self.mxid, levels) await self.handle_matrix_power_levels(source, levels["users"], {}) @@ -1441,22 +1445,17 @@ class Portal: return None if self.get_config("inline_images") and (evt.message or evt.fwd_from or evt.reply_to_msg_id): - text, html, relates_to = await formatter.telegram_to_matrix( + content = await formatter.telegram_to_matrix( evt, source, self.main_intent, prefix_html=f"Inline Telegram photo
", prefix_text="Inline image: ") + content.external_url = self.get_external_url(evt) await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to, - timestamp=evt.date, - external_url=self.get_external_url(evt)) - info = { - "h": largest_size.h, - "w": largest_size.w, - "size": len(largest_size.bytes) if ( - isinstance(largest_size, PhotoCachedSize)) else largest_size.size, - "orientation": 0, - "mimetype": file.mime_type, - } + return await intent.send_message(self.mxid, content, timestamp=evt.date) + info = ImageInfo( + height=largest_size.h, width=largest_size.w, orientation=0, mimetype=file.mime_type, + size=(len(largest_size.bytes) if (isinstance(largest_size, PhotoCachedSize)) + else largest_size.size)) name = f"image{sane_mimetypes.guess_extension(file.mime_type)}" await intent.set_typing(self.mxid, is_typing=False) result = await intent.send_image(self.mxid, file.mxc, info=info, text=name, @@ -1492,7 +1491,7 @@ class Portal: @staticmethod def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: Dict, - thumb_size: TypePhotoSize) -> Tuple[Dict, str]: + thumb_size: TypePhotoSize) -> Tuple[ImageInfo, str]: document = evt.media.document name = evt.message or attrs["name"] if attrs["is_sticker"]: @@ -1508,26 +1507,21 @@ class Portal: mime_type = document.mime_type or file.mime_type else: mime_type = file.mime_type or document.mime_type - info = { - "size": file.size, - "mimetype": mime_type, - } + info = ImageInfo(size=file.size, mimetype=mime_type) if attrs["mime_type"] and not file.was_converted: file.mime_type = attrs["mime_type"] or file.mime_type if file.width and file.height: - info["w"], info["h"] = file.width, file.height + info.width, info.height = file.width, file.height elif attrs["width"] and attrs["height"]: - info["w"], info["h"] = attrs["width"], attrs["height"] + info.width, info.height = attrs["width"], attrs["height"] if file.thumbnail: - info["thumbnail_url"] = file.thumbnail.mxc - info["thumbnail_info"] = { - "mimetype": file.thumbnail.mime_type, - "h": file.thumbnail.height or thumb_size.h, - "w": file.thumbnail.width or thumb_size.w, - "size": file.thumbnail.size, - } + info.thumbnail_url = file.thumbnail.mxc + info.thumbnail_info = ThumbnailInfo(mimetype=file.thumbnail.mime_type, + height=file.thumbnail.height or thumb_size.h, + width=file.thumbnail.width or thumb_size.w, + size=file.thumbnail.size) return info, name @@ -1948,7 +1942,7 @@ class Portal: await self.update_telegram_pin() @staticmethod - def _get_level_from_participant(participant: TypeParticipant, _: Dict) -> int: + def _get_level_from_participant(participant: TypeParticipant) -> int: # TODO use the power level requirements to get better precision in channels if isinstance(participant, (ChatParticipantAdmin, ChannelParticipantAdmin)): return 50 @@ -1957,28 +1951,16 @@ class Portal: return 0 @staticmethod - def _participant_to_power_levels(levels: dict, user: Union['u.User', p.Puppet], new_level: int, + def _participant_to_power_levels(levels: PowerLevelStateEventContent, + user: Union['u.User', p.Puppet], new_level: int, bot_level: int) -> bool: new_level = min(new_level, bot_level) - default_level = levels["users_default"] if "users_default" in levels else 0 - try: - user_level = int(levels["users"][user.mxid]) - except (ValueError, KeyError): - user_level = default_level + user_level = levels.get_user_level(user.mxid) if user_level != new_level and user_level < bot_level: - levels["users"][user.mxid] = new_level + levels.users[user.mxid] = new_level return True return False - def _get_bot_level(self, levels: dict) -> int: - try: - return levels["users"][self.main_intent.mxid] - except KeyError: - try: - return levels["users_default"] - except KeyError: - return 0 - @staticmethod def _get_powerlevel_level(levels: dict) -> int: try: @@ -1989,21 +1971,21 @@ class Portal: except KeyError: return 50 - def _participants_to_power_levels(self, participants: List[TypeParticipant], levels: Dict - ) -> bool: - bot_level = self._get_bot_level(levels) - if bot_level < self._get_powerlevel_level(levels): + def _participants_to_power_levels(self, participants: List[TypeParticipant], + levels: PowerLevelStateEventContent) -> bool: + bot_level = levels.get_user_level(self.main_intent.mxid) + if bot_level < levels.get_event_level(EventType.ROOM_POWER_LEVELS): return False changed = False admin_power_level = min(75 if self.peer_type == "channel" else 50, bot_level) - if levels["events"]["m.room.power_levels"] != admin_power_level: + if levels.events[EventType.ROOM_POWER_LEVELS] != admin_power_level: changed = True - levels["events"]["m.room.power_levels"] = admin_power_level + levels.events[EventType.ROOM_POWER_LEVELS] = admin_power_level for participant in participants: puppet = p.Puppet.get(TelegramID(participant.user_id)) user = u.User.get_by_tgid(TelegramID(participant.user_id)) - new_level = self._get_level_from_participant(participant, levels) + new_level = self._get_level_from_participant(participant) if user: user.register_portal(self) diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py index 0e8b20ab..7071b2d6 100644 --- a/mautrix_telegram/util/__init__.py +++ b/mautrix_telegram/util/__init__.py @@ -1,10 +1,4 @@ -from asyncio import Future - from .file_transfer import transfer_file_to_matrix, convert_image from .format_duration import format_duration from .signed_token import sign_token, verify_token from .recursive_dict import recursive_del, recursive_set, recursive_get - - -def ignore_coro(_: Future) -> None: - pass diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py index ae353bad..39bdab4f 100644 --- a/mautrix_telegram/web/common/auth_api.py +++ b/mautrix_telegram/web/common/auth_api.py @@ -26,7 +26,7 @@ from telethon.errors import * from mautrix.bridge import OnlyLoginSelf, InvalidAccessToken from ...commands.telegram.auth import enter_password -from ...util import format_duration, ignore_coro +from ...util import format_duration from ...puppet import Puppet from ...user import User @@ -119,7 +119,7 @@ class AuthAPI(abc.ABC): existing_user = User.get_by_tgid(user_info.id) if existing_user and existing_user != user: await existing_user.log_out() - ignore_coro(asyncio.ensure_future(user.post_login(user_info), loop=self.loop)) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) if user.command_status and user.command_status["action"] == "Login": user.command_status = None diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 4c345937..793c1375 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -30,7 +30,6 @@ from mautrix.types import UserID from ...types import TelegramID from ...user import User from ...portal import Portal -from ...util import ignore_coro from ...commands.portal.util import user_has_power_level, get_initial_state from ..common import AuthAPI @@ -188,9 +187,8 @@ class ProvisioningAPI(AuthAPI): portal.photo_id = "" portal.save() - ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, - levels=levels), - loop=self.loop)) + asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels), + loop=self.loop) return web.Response(status=202, body="{}") @@ -269,7 +267,8 @@ class ProvisioningAPI(AuthAPI): require_puppeting=False, require_user=False) if err is not None: return err - elif user and not await user_has_power_level(portal.mxid, self.az.intent, user, "unbridge"): + elif user and not await user_has_power_level(portal.mxid, self.az.intent, user, + "unbridge"): return self.get_error_response(403, "not_enough_permissions", "You do not have the permissions to unbridge that room.") @@ -284,7 +283,7 @@ class ProvisioningAPI(AuthAPI): self.log.exception("Failed to disconnect chat") return self.get_error_response(500, "exception", "Failed to disconnect chat") else: - ignore_coro(asyncio.ensure_future(coro, loop=self.loop)) + asyncio.ensure_future(coro, loop=self.loop) return web.json_response({}, status=200 if sync else 202) async def get_user_info(self, request: web.Request) -> web.Response: