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)