diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py index fc0894eb..7e376fa4 100644 --- a/mautrix_telegram/commands/__init__.py +++ b/mautrix_telegram/commands/__init__.py @@ -1,2 +1,5 @@ -from .handler import command_handler, CommandHandler, CommandEvent +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 clean_rooms, auth, meta, telegram, portal diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 77a711ea..192355eb 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -14,17 +14,20 @@ # # 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 telethon.errors import * -from . import command_handler +from . import command_handler, CommandEvent, SECTION_AUTH from .. import puppet as pu from ..util import format_duration -@command_handler(needs_auth=False) -async def ping(evt): +@command_handler(needs_auth=False, + help_section=SECTION_AUTH, + help_text="Check if you're logged into Telegram.") +async def ping(evt: CommandEvent): me = await evt.sender.client.get_me() if await evt.sender.is_logged_in() else None if me: return await evt.reply(f"You're logged in as @{me.username}") @@ -32,8 +35,10 @@ async def ping(evt): return await evt.reply("You're not logged in.") -@command_handler(needs_auth=False, needs_puppeting=False) -async def ping_bot(evt): +@command_handler(needs_auth=False, needs_puppeting=False, + help_section=SECTION_AUTH, + help_text="Get the info of the message relay Telegram bot.") +async def ping_bot(evt: CommandEvent): if not evt.tgbot: return await evt.reply("Telegram message relay bot not configured.") bot_info = await evt.tgbot.client.get_me() @@ -44,13 +49,11 @@ async def ping_bot(evt): "To use the bot, simply invite it to a portal room.") -@command_handler(needs_auth=False, management_only=True) -def register(evt): - return evt.reply("Not yet implemented.") - - -@command_handler(needs_auth=False, management_only=True) -async def register(evt): +@command_handler(needs_auth=False, management_only=True, + help_section=SECTION_AUTH, + help_args="<_phone_> <_full name_>", + help_text="Register to Telegram") +async def register(evt: CommandEvent): if await evt.sender.is_logged_in(): return await evt.reply("You are already logged in.") elif len(evt.args) < 1: @@ -69,7 +72,7 @@ async def register(evt): }) -async def enter_code_register(evt): +async def enter_code_register(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp `") try: @@ -95,8 +98,10 @@ async def enter_code_register(evt): "Check console for more details.") -@command_handler(needs_auth=False, management_only=True) -async def login(evt): +@command_handler(needs_auth=False, management_only=True, + help_section=SECTION_AUTH, + help_text="Get instructions on how to log in.") +async def login(evt: CommandEvent): if await evt.sender.is_logged_in(): return await evt.reply("You are already logged in.") @@ -126,7 +131,7 @@ async def login(evt): return await evt.reply("This bridge instance has been configured to not allow logging in.") -async def request_code(evt, phone_number, next_status): +async def request_code(evt: CommandEvent, phone_number: str, next_status: Dict[str, str]): ok = False try: await evt.sender.ensure_started(even_if_no_session=True) @@ -158,7 +163,7 @@ async def request_code(evt, phone_number, next_status): @command_handler(needs_auth=False) -async def enter_phone(evt): +async def enter_phone(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp enter-phone `") elif not evt.config.get("bridge.allow_matrix_login", True): @@ -173,7 +178,7 @@ async def enter_phone(evt): @command_handler(needs_auth=False) -async def enter_code(evt): +async def enter_code(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp enter-code `") elif not evt.config.get("bridge.allow_matrix_login", True): @@ -203,7 +208,7 @@ async def enter_code(evt): @command_handler(needs_auth=False) -async def enter_password(evt): +async def enter_password(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp enter-password `") elif not evt.config.get("bridge.allow_matrix_login", True): @@ -223,10 +228,10 @@ async def enter_password(evt): "Check console for more details.") -@command_handler(needs_auth=False) -async def logout(evt): - if not await evt.sender.is_logged_in(): - return await evt.reply("You're not logged in.") +@command_handler(needs_auth=True, + help_section=SECTION_AUTH, + help_text="Log out from Telegram.") +async def logout(evt: CommandEvent): if await evt.sender.log_out(): return await evt.reply("Logged out successfully.") return await evt.reply("Failed to log out.") diff --git a/mautrix_telegram/commands/clean_rooms.py b/mautrix_telegram/commands/clean_rooms.py index 71ef2a91..4c713f4a 100644 --- a/mautrix_telegram/commands/clean_rooms.py +++ b/mautrix_telegram/commands/clean_rooms.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from mautrix_appservice import MatrixRequestError -from . import command_handler +from . import command_handler, CommandEvent, SECTION_ADMIN from .. import puppet as pu, portal as po @@ -52,12 +52,10 @@ async def _find_rooms(intent): return management_rooms, unidentified_rooms, portals, empty_portals -@command_handler(needs_admin=True, needs_auth=False, name="clean-rooms") -async def clean_rooms(evt): - if not evt.is_management: - return await evt.reply("`clean-rooms` is a particularly spammy command. Please don't " - "run it in non-management rooms.") - +@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): management_rooms, unidentified_rooms, portals, empty_portals = await _find_rooms(evt.az.intent) reply = ["#### Management rooms (M)"] diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index dbfbfcad..7bf4323d 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -14,45 +14,38 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import List, Dict, Callable, Optional +from collections import namedtuple import markdown import logging from telethon.errors import FloodWaitError from ..util import format_duration +from ..context import Context +from .. import user as u -command_handlers = {} +command_handlers = {} # type: Dict[str, CommandHandler] +HelpSection = namedtuple("HelpSection", "name order description") -def command_handler(needs_auth=True, management_only=False, needs_puppeting=True, - needs_admin=False, name=None): - def decorator(func): - async def wrapper(evt): - if management_only and not evt.is_management: - return await evt.reply(f"`{evt.command}` is a restricted command:" - "you may only run it in management rooms.") - elif needs_auth and not await evt.sender.is_logged_in(): - return await evt.reply("This command requires you to be logged in.") - elif needs_puppeting and not evt.sender.puppet_whitelisted: - return await evt.reply("This command requires puppeting privileges.") - elif needs_admin and not evt.sender.is_admin: - return await evt.reply("This command requires administrator privileges.") - return await func(evt) - - command_handlers[name or func.__name__.replace("_", "-")] = wrapper - return wrapper - - return decorator +SECTION_GENERAL = HelpSection("General", 0, "") +SECTION_AUTH = HelpSection("Authentication", 10, "") +SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "") +SECTION_PORTAL_MANAGEMENT = HelpSection("Portal management", 30, "") +SECTION_MISC = HelpSection("Miscellaneous", 40, "") +SECTION_ADMIN = HelpSection("Administration", 50, "") class CommandEvent: - def __init__(self, handler, room, sender, command, args, is_management, is_portal): - self.az = handler.az - self.log = handler.log - self.loop = handler.loop - self.tgbot = handler.tgbot - self.config = handler.config - self.command_prefix = handler.command_prefix + def __init__(self, processor: "CommandProcessor", room: str, sender: u.User, command: str, + args: List[str], is_management: bool, is_portal: bool): + self.az = processor.az + self.log = processor.log + self.loop = processor.loop + self.tgbot = processor.tgbot + self.config = processor.config + self.command_prefix = processor.command_prefix self.room_id = room self.sender = sender self.command = command @@ -60,7 +53,7 @@ class CommandEvent: self.is_management = is_management self.is_portal = is_portal - def reply(self, message, allow_html=False, render_markdown=True): + def reply(self, message: str, allow_html: bool = False, render_markdown: bool = True): message = message.replace("$cmdprefix+sp ", "" if self.is_management else f"{self.command_prefix} ") message = message.replace("$cmdprefix", self.command_prefix) @@ -73,17 +66,78 @@ class CommandEvent: class CommandHandler: + def __init__(self, handler: Callable[[CommandEvent], None], + needs_auth: bool, needs_puppeting: bool, needs_admin: bool, management_only: bool, + name: str, help_text: str, help_args: str, help_section: HelpSection): + self._handler = handler + self.needs_auth = needs_auth + self.needs_puppeting = needs_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 + + async def get_permission_error(self, evt: CommandEvent) -> Optional[str]: + 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.") + elif self.needs_puppeting and not evt.sender.puppet_whitelisted: + return "This command requires puppeting privileges." + elif self.needs_admin and not evt.sender.is_admin: + return "This command requires administrator privileges." + elif self.needs_auth and not await evt.sender.is_logged_in(): + return "This command requires you to be logged in." + return None + + def has_permission(self, is_management: bool, puppet_whitelisted: bool, is_admin: bool, + is_logged_in: bool) -> bool: + return ((not self.management_only or is_management) and + (not self.needs_puppeting or 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): + 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: + return bool(self.help_section) and bool(self._help_text) + + @property + def help(self) -> str: + return f"**{self.name}** {self._help_args} - {self._help_text}" + + +def command_handler(_func: Optional[Callable[[CommandEvent], None]] = None, *, needs_auth=True, + needs_puppeting=True, needs_admin=False, management_only=False, + name=None, help_text="", help_args="", help_section=None): + input_name = name + + def decorator(func: Callable[[CommandEvent], None]): + name = input_name or func.__name__.replace("_", "-") + handler = CommandHandler(func, needs_auth, needs_puppeting, needs_admin, management_only, + name, help_text, help_args, help_section) + command_handlers[handler.name] = handler + return handler + + return decorator if _func is None else decorator(_func) + + +class CommandProcessor: log = logging.getLogger("mau.commands") - def __init__(self, context): + def __init__(self, context: Context): self.az, self.db, self.config, self.loop, self.tgbot = context self.command_prefix = self.config["bridge.command_prefix"] - # region Utility functions for handling commands - - async def handle(self, room, sender, command, args, is_management, is_portal): - evt = CommandEvent(self, room, sender, command, args, - is_management, is_portal) + async def handle(self, room: str, sender: u.User, command: str, args: List[str], + is_management: bool, is_portal: bool): + evt = CommandEvent(self, room, sender, command, args, is_management, is_portal) orig_command = command command = command.lower() try: @@ -100,7 +154,7 @@ class CommandHandler: except FloodWaitError as e: return await evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}") except Exception: - self.log.exception("Fatal error handling command " + self.log.exception("Unhandled error while handling command " f"{evt.command} {' '.join(args)} from {sender.mxid}") - return await evt.reply("Fatal error while handling command. " + return await evt.reply("Unhandled error while handling command. " "Check logs for more details.") diff --git a/mautrix_telegram/commands/meta.py b/mautrix_telegram/commands/meta.py index fc4ef4be..a3d36a4b 100644 --- a/mautrix_telegram/commands/meta.py +++ b/mautrix_telegram/commands/meta.py @@ -14,11 +14,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from . import command_handler +from . import command_handler, CommandEvent, _command_handlers, SECTION_GENERAL -@command_handler(needs_auth=False, needs_puppeting=False) -def cancel(evt): +@command_handler(needs_auth=False, needs_puppeting=False, + help_section=SECTION_GENERAL, + help_text="Cancel an ongoing action (such as login)") +def cancel(evt: CommandEvent): if evt.sender.command_status: action = evt.sender.command_status["action"] evt.sender.command_status = None @@ -28,78 +30,39 @@ def cancel(evt): @command_handler(needs_auth=False, needs_puppeting=False) -def unknown_command(evt): +def unknown_command(evt: CommandEvent): return evt.reply("Unknown command. Try `$cmdprefix+sp help` for help.") -@command_handler(needs_auth=False, needs_puppeting=False) -def help(evt): +help_cache = {} + + +async def _get_help_text(evt: CommandEvent): + cache_key = (evt.is_management, evt.sender.puppet_whitelisted, evt.sender.is_admin, + await evt.sender.is_logged_in()) + if cache_key not in help_cache: + help = {} + for handler in _command_handlers.values(): + if handler.has_help and handler.has_permission(*cache_key): + help.setdefault(handler.help_section, []) + help[handler.help_section].append(handler.help + " ") + help = sorted(help.items(), key=lambda item: item[0].order) + help = ["#### {}\n{}\n".format(key.name, "\n".join(value)) for key, value in help] + help_cache[cache_key] = "\n".join(help) + return help_cache[cache_key] + + +def _get_management_status(evt: CommandEvent): if evt.is_management: - management_status = ("This is a management room: prefixing commands " - "with `$cmdprefix` is not required.\n") + return "This is a management room: prefixing commands with `$cmdprefix` is not required." elif evt.is_portal: - management_status = ("**This is a portal room**: you must always " - "prefix commands with `$cmdprefix`.\n" - "Management commands will not be sent to Telegram.") - else: - management_status = ("**This is not a management room**: you must " - "prefix commands with `$cmdprefix`.\n") - help = None - if not evt.sender.puppet_whitelisted: - help = """\n -#### Generic bridge commands -**help** - Show this help message. -**cancel** - Cancel an ongoing action (such as login). -**ping-bot** - Get info of the message relay Telegram bot. -**invite-link** - Get a Telegram invite link to the current chat. -**delete-portal** - 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. -**unbridge** - Remove puppets from the current portal room and forget the portal. -**bridge** [_id_] - 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. + 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`." -""" - help = help or """\n -#### Generic bridge commands -**help** - Show this help message. -**cancel** - Cancel an ongoing action (such as login). -#### Authentication -**login** - Request an authentication code. -**logout** - Log out from Telegram. -**ping** - Check if you're logged into Telegram. - -#### Miscellaneous things -**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users. -**sync** [`chats`|`contacts`|`me`] - Synchronize your chat portals, contacts and/or own info. -**ping-bot** - Get info of the message relay Telegram bot. -**set-pl** <_level_> [_mxid_] - Set a temporary power level without affecting Telegram. - -#### Initiating chats -**pm** <_identifier_> - Open a private chat with the given Telegram user. The identifier is either - the internal user ID, the username or the phone number. -**join** <_link_> - Join a chat with an invite link. -**create** [_type_] - Create a Telegram chat of the given type for the current Matrix room. The - type is either `group`, `supergroup` or `channel` (defaults to `group`). - -#### Portal management -**upgrade** - Upgrade a normal Telegram group to a supergroup. -**invite-link** - Get a Telegram invite link to the current chat. -**delete-portal** - 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. -**unbridge** - Remove puppets from the current portal room and forget the portal. -**bridge** [_id_] - 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. -**group-name** <_name_|`-`> - Change the username of a supergroup/channel. To disable, use a dash - (`-`) as the name. -**clean-rooms** - Clean up unused portal/management rooms. - -**filter** <`whitelist`|`blacklist`> <_chat ID_> - Allow or disallow bridging a specific chat. -**filter-mode** <`whitelist`|`blacklist`> - Change whether the bridge will allow or disallow - bridging rooms by default. -""" - return evt.reply(management_status + help) +@command_handler(needs_auth=False, needs_puppeting=False, + help_section=SECTION_GENERAL, + help_text="Show this help message.") +async def help(evt: CommandEvent): + return await evt.reply(_get_management_status(evt) + "\n" + await _get_help_text(evt)) diff --git a/mautrix_telegram/commands/portal.py b/mautrix_telegram/commands/portal.py index 8b9eee00..29d6aade 100644 --- a/mautrix_telegram/commands/portal.py +++ b/mautrix_telegram/commands/portal.py @@ -14,17 +14,21 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional, Callable import asyncio from telethon.errors import * from telethon.tl.types import ChatForbidden, ChannelForbidden from mautrix_appservice import MatrixRequestError -from .. import portal as po -from . import command_handler, CommandEvent +from .. import portal as po, user as u +from . import command_handler, CommandEvent, SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT -@command_handler(needs_admin=True, needs_auth=False, name="set-pl") +@command_handler(needs_admin=True, needs_auth=False, name="set-pl", + 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): try: level = int(evt.args[0]) @@ -42,7 +46,8 @@ async def set_power_level(evt: CommandEvent): return await evt.reply("Failed to set power level.") -@command_handler() +@command_handler(help_section=SECTION_PORTAL_MANAGEMENT, + help_text="Get a Telegram invite link to the current chat.") async def invite_link(evt: CommandEvent): portal = po.Portal.get_by_mxid(evt.room_id) if not portal: @@ -60,7 +65,7 @@ async def invite_link(evt: CommandEvent): return await evt.reply("You don't have the permission to create an invite link.") -async def _has_access_to(room, intent, sender, event, default=50): +async def _has_access_to(room: str, intent, sender: u.User, event: str, default: int = 50): if sender.is_admin: return True # Make sure the state store contains the power levels. @@ -73,7 +78,8 @@ async def _has_access_to(room, intent, sender, event, default=50): default=default) -async def _get_portal_and_check_permission(evt, permission, action=None): +async def _get_portal_and_check_permission(evt: CommandEvent, permission: str, + action: Optional[str] = None): room_id = evt.args[0] if len(evt.args) > 0 else evt.room_id portal = po.Portal.get_by_mxid(room_id) @@ -87,7 +93,8 @@ async def _get_portal_and_check_permission(evt, permission, action=None): return portal, True -def _get_portal_murder_function(action, room_id, function, command, completed_message): +def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str, + completed_message: str): async def post_confirm(confirm): confirm.sender.command_status = None if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}": @@ -103,7 +110,11 @@ def _get_portal_murder_function(action, room_id, function, command, completed_me } -@command_handler(needs_auth=False, needs_puppeting=False) +@command_handler(needs_auth=False, needs_puppeting=False, + help_section=SECTION_PORTAL_MANAGEMENT, + 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): portal, ok = await _get_portal_and_check_permission(evt, "delete_portal") if not ok: @@ -122,7 +133,9 @@ async def delete_portal(evt: CommandEvent): "bridge, use `$cmdprefix+sp unbridge` instead.") -@command_handler(needs_auth=False) +@command_handler(needs_auth=False, + help_section=SECTION_PORTAL_MANAGEMENT, + help_text="Remove puppets from the current portal room and forget the portal.") async def unbridge(evt: CommandEvent): portal, ok = await _get_portal_and_check_permission(evt, "unbridge_room") if not ok: @@ -136,7 +149,12 @@ async def unbridge(evt: CommandEvent): "by typing `$cmdprefix+sp confirm-unbridge`") -@command_handler(needs_auth=False) +@command_handler(needs_auth=False, + help_section=SECTION_PORTAL_MANAGEMENT, + help_args="[_id_]", + 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): if len(evt.args) == 0: return await evt.reply("**Usage:** " @@ -168,7 +186,7 @@ async def bridge(evt: CommandEvent): portal = po.Portal.get_by_tgid(tgid, peer_type=peer_type) if not portal.allow_bridging(): return await evt.reply("This bridge doesn't allow bridging that Telegram chat.\n" - "If you're the bridge admin, try" + "If you're the bridge admin, try " "`$cmdprefix+sp whitelist ` first.") if portal.mxid: has_portal_message = ( @@ -203,7 +221,7 @@ async def bridge(evt: CommandEvent): "chat to this room, use `$cmdprefix+sp continue`") -async def cleanup_old_portal_while_bridging(evt, portal): +async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: po.Portal): 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" @@ -253,7 +271,7 @@ async def confirm_bridge(evt: CommandEvent): "`$cmdprefix+sp cancel` to cancel.") is_logged_in = await evt.sender.is_logged_in() - user = evt.sender if is_logged_in else evt.tgbot + user = evt.sender if is_logged_in else evt.tgbot try: entity = await user.client.get_entity(portal.peer) except Exception: @@ -300,7 +318,11 @@ async def _get_initial_state(evt: CommandEvent): return title, about, levels -@command_handler() +@command_handler(help_section=SECTION_CREATING_PORTALS, + help_args="[_type_]", + 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): type = evt.args[0] if len(evt.args) > 0 else "group" if type not in {"chat", "group", "supergroup", "channel"}: @@ -331,7 +353,8 @@ async def create(evt: CommandEvent): return await evt.reply(f"Telegram chat created. ID: {portal.tgid}") -@command_handler() +@command_handler(help_section=SECTION_PORTAL_MANAGEMENT, + help_text="Upgrade a normal Telegram group to a supergroup.") async def upgrade(evt: CommandEvent): portal = po.Portal.get_by_mxid(evt.room_id) if not portal: @@ -350,7 +373,10 @@ async def upgrade(evt: CommandEvent): return await evt.reply(e.args[0]) -@command_handler() +@command_handler(help_section=SECTION_PORTAL_MANAGEMENT, + 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): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp group-name `") @@ -382,7 +408,11 @@ async def group_name(evt: CommandEvent): return await evt.reply("Invalid username") -@command_handler(needs_admin=True) +@command_handler(needs_admin=True, + help_section=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): try: mode = evt.args[0] @@ -404,7 +434,10 @@ async def filter_mode(evt: CommandEvent): "`!filter blacklist `.") -@command_handler(needs_admin=True) +@command_handler(needs_admin=True, + help_section=SECTION_ADMIN, + help_args="<`whitelist`|`blacklist`> <_chat ID_>", + help_text="Allow or disallow bridging a specific chat.") async def filter(evt: CommandEvent): try: action = evt.args[0] diff --git a/mautrix_telegram/commands/telegram.py b/mautrix_telegram/commands/telegram.py index ff23d030..75221c21 100644 --- a/mautrix_telegram/commands/telegram.py +++ b/mautrix_telegram/commands/telegram.py @@ -20,11 +20,13 @@ from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInv from telethon.tl.functions.channels import JoinChannelRequest from .. import puppet as pu, portal as po -from . import command_handler +from . import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS -@command_handler() -async def search(evt): +@command_handler(help_section=SECTION_MISC, + help_args="[_-r|--remote_] <_query_>", + help_text="Search your contacts or the Telegram servers for users.") +async def search(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] `") @@ -59,8 +61,14 @@ async def search(evt): return await evt.reply("\n".join(reply)) -@command_handler(name="pm") -async def private_message(evt): +@command_handler(name="pm", + help_section=SECTION_CREATING_PORTALS, + help_args="<_identifier_>", + help_text="Open a private chat with the given Telegram user. The identifier is " + "either the internal user ID, the username or the phone number. " + "**N.B.** The phone numbers you start chats with must already be in " + "your contacts.") +async def private_message(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp pm `") @@ -79,7 +87,7 @@ async def private_message(evt): f"{pu.Puppet.get_displayname(user, False)}") -async def _join(evt, arg): +async def _join(evt: CommandEvent, arg: str): if arg.startswith("joinchat/"): invite_hash = arg[len("joinchat/"):] try: @@ -99,8 +107,10 @@ async def _join(evt, arg): return await evt.sender.client(JoinChannelRequest(channel)), None -@command_handler() -async def join(evt): +@command_handler(help_section=SECTION_CREATING_PORTALS, + help_args="<_link_>", + help_text="Join a chat with an invite link.") +async def join(evt: CommandEvent): if len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp join `") @@ -124,8 +134,10 @@ async def join(evt): return await evt.reply(f"Created room for {portal.title}") -@command_handler() -async def sync(evt): +@command_handler(help_section=SECTION_MISC, + help_args="[`chats`|`contacts`|`me`]", + help_text="Synchronize your chat portals, contacts and/or own info.") +async def sync(evt: CommandEvent): if len(evt.args) > 0: sync_only = evt.args[0] if sync_only not in ("chats", "contacts", "me"): diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 97c3dc8c..dc9e3f75 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -23,7 +23,7 @@ from mautrix_appservice import MatrixRequestError, IntentError from .user import User from .portal import Portal from .puppet import Puppet -from .commands import CommandHandler +from .commands import CommandProcessor class MatrixHandler: @@ -31,7 +31,7 @@ class MatrixHandler: def __init__(self, context): self.az, self.db, self.config, _, self.tgbot = context - self.commands = CommandHandler(context) + self.commands = CommandProcessor(context) self.az.matrix_event_handler(self.handle_event)