diff --git a/README.md b/README.md index 49fc828a..10a3bd4c 100644 --- a/README.md +++ b/README.md @@ -105,18 +105,17 @@ does not do this automatically.~~ * [ ] Public channel username changes * [x] Initial chat metadata * [x] Supergroup upgrade -* Initiating chats +* Misc * [x] Automatic portal creation for groups/channels at startup * [x] Automatic portal creation for groups/channels when receiving invite/message * [ ] Private chat creation by inviting Telegram user to new room - * [ ] Searching for Telegram users using management commands -* Misc * [ ] Use optional bot to relay messages for unauthenticated Matrix users * [ ] Joining public channels/supergroups using room aliases * Commands * [x] Logging in and out (`login` + code entering, `logout`) * [ ] Registering (`register`) - * [ ] Searching for users (`search`) - * [ ] Starting private chats (`pm`) + * [x] Searching for users (`search`) + * [x] Starting private chats (`pm`) + * [x] Joining chats with invite links (`join`) * [ ] Creating a Telegram chat for an existing Matrix room (`create`) * [ ] Upgrading the chat of a portal room into a supergroup (`upgrade`) diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py index 81f449b7..fc072f4b 100644 --- a/mautrix_appservice/intent_api.py +++ b/mautrix_appservice/intent_api.py @@ -187,9 +187,10 @@ class IntentAPI: # region Room actions def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False, - invitees=()): + invitees=(), initial_state=[]): self._ensure_registered() - return self.client.create_room(alias, is_public, name, topic, is_direct, invitees) + return self.client.create_room(alias, is_public, name, topic, is_direct, invitees, + initial_state) def invite(self, room_id, user_id): self._ensure_joined(room_id) diff --git a/mautrix_telegram/commands.py b/mautrix_telegram/commands.py index 06808067..0f09a55e 100644 --- a/mautrix_telegram/commands.py +++ b/mautrix_telegram/commands.py @@ -16,6 +16,11 @@ from contextlib import contextmanager import markdown from telethon.errors import * +from telethon.tl.types import * +from telethon.tl.functions.contacts import SearchRequest +from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest +from telethon.tl.functions.channels import JoinChannelRequest +from . import puppet as pu, portal as po command_handlers = {} @@ -37,7 +42,12 @@ class CommandHandler: def handle(self, room, sender, command, args, is_management, is_portal): with self.handler(sender, room, command, args, is_management, is_portal) as handle_command: - handle_command(self, sender, args) + try: + handle_command(self, sender, args) + except: + self.reply("Fatal error while handling command. Check logs for more details.") + self.log.exception(f"Fatal error handling command " + f"'$cmdprefix {command} {''.join(args)}' from {sender.mxid}") @contextmanager def handler(self, sender, room, command, args, is_management, is_portal): @@ -62,9 +72,9 @@ class CommandHandler: raise AttributeError("the reply function can only be used from within" "the `CommandHandler.run` context manager") - message = message.replace("$cmdprefix", self.command_prefix) message = message.replace("$cmdprefix+sp ", "" if self._is_management else f"{self.command_prefix} ") + message = message.replace("$cmdprefix", self.command_prefix) html = None if render_markdown: html = markdown.markdown(message, safe_mode="escape" if allow_html else False) @@ -165,7 +175,7 @@ class CommandHandler: return self.reply("Incorrect password.") except: self.log.exception() - return self.reply("Unhandled exception while sending password." + return self.reply("Unhandled exception while sending password. " "Check console for more details.") @command_handler @@ -181,11 +191,83 @@ class CommandHandler: @command_handler def search(self, sender, args): - self.reply("Not yet implemented.") + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] ") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + force_remote = False + if args[0] in {"-r", "--remote"}: + args.pop(0) + query = " ".join(args) + if len(query) < 5: + return self.reply("Minimum length of query for remote search is 5 characters.") + found = sender.client(SearchRequest(q=query, limit=10)) + print(found) + # reply = ["**People:**", ""] + reply = ["**Results from Telegram server:**", ""] + for result in found.users: + puppet = pu.Puppet.get(result.id) + puppet.update_info(sender, result) + reply.append( + f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): {puppet.id}") + # reply.extend(("", "**Chats:**", "")) + # for result in found.chats: + # reply.append(f"* {result.title}") + return self.reply("\n".join(reply)) @command_handler def pm(self, sender, args): - self.reply("Not yet implemented.") + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp pm ") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + + user = sender.client.get_entity(args[0]) + if not user: + return self.reply("User not found.") + elif not isinstance(user, User): + return self.reply("That doesn't seem to be a user.") + print(user) + + def _strip_prefix(self, value, prefixes): + for prefix in prefixes: + if value.startswith(prefix): + return value[len(prefix):] + return value + + @command_handler + def join(self, sender, args): + if len(args) == 0: + return self.reply("**Usage:** `$cmdprefix+sp join ") + elif not sender.tgid: + return self.reply("This command requires you to be logged in.") + + regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)") + arg = regex.match(args[0]) + if not arg: + return self.reply("That doesn't look like a Telegram invite link.") + arg = arg.group(1) + if arg.startswith("joinchat/"): + invite_hash = arg[len("joinchat/"):] + try: + check = sender.client(CheckChatInviteRequest(invite_hash)) + print(check) + except InviteHashInvalidError: + return self.reply("Invalid invite link.") + except InviteHashExpiredError: + return self.reply("Invite link expired.") + try: + updates = sender.client(ImportChatInviteRequest(invite_hash)) + except UserAlreadyParticipantError: + return self.reply("You are already in that chat.") + else: + channel = sender.client.get_entity(arg) + if not channel: + return self.reply("Channel/supergroup not found.") + updates = sender.client(JoinChannelRequest(channel)) + for chat in updates.chats: + portal = po.Portal.get_by_entity(chat) + portal.create_room(sender, chat, [sender.mxid]) @command_handler def create(self, sender, args): @@ -235,9 +317,12 @@ _**Telegram actions**: commands for using the bridge to interact with Telegram._ **logout** - Log out from Telegram. **ping** - Check if you're logged into Telegram. **search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users. -**pm** <_id_> - Open a private chat with the given Telegram user ID. -**create** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix room. - If the room ID is not specified, a chat for the current room is created. +**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** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix + room. If the room ID is not specified, a chat for the + current room is created. **upgrade** - Upgrade a normal Telegram group to a supergroup. """ return self.reply(management_status + help) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index e06426cc..332c91a8 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -98,28 +98,25 @@ class Portal: puppet = p.Puppet.get(self.tgid) if direct else None intent = puppet.intent if direct else self.az.intent - power_level_requirement = 0 if self.peer_type == "chat" else 50 - initial_power_levels = { - "ban": 100, - "events": { - "m.room.name": power_level_requirement, - "m.room.avatar": power_level_requirement, - "m.room.topic": 50, - "m.room.power_levels": 50, - "invite": power_level_requirement, - }, - "users_default": 0, - } - # TODO set room alias if public channel. - room = intent.create_room(invitees=invites, name=title, is_direct=direct, - initial_state=[initial_power_levels]) + room = intent.create_room(invitees=invites, name=title, is_direct=direct) if not room: raise Exception(f"Failed to create room for {self.tgid}") self.mxid = room["room_id"] self.by_mxid[self.mxid] = self self.save() + + power_level_requirement = 0 if self.peer_type == "chat" else 50 + levels = self.main_intent.get_power_levels(self.mxid) + levels["ban"] = 100 + levels["invite"] = 50 + levels["events"]["m.room.name"] = power_level_requirement + levels["events"]["m.room.avatar"] = power_level_requirement + levels["events"]["m.room.topic"] = 50 if self.peer_type == "channel" else 100 + levels["events"]["m.room.power_levels"] = 95 + self.main_intent.set_power_levels(self.mxid, levels) + if not direct: self.update_info(user, entity) users, participants = self.get_users(user, entity) @@ -397,11 +394,11 @@ class Portal: def handle_telegram_action(self, source, sender, action): if not self.mxid: - create_and_exit = [MessageActionChatCreate, MessageActionChannelCreate] - create_and_continue = [MessageActionChatAddUser, MessageActionChatJoinedByLink] + create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate) + create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink) if isinstance(action, create_and_exit + create_and_continue): self.create_room(source, invites=[source.mxid]) - if isinstance(action, create_and_exit): + if not isinstance(action, create_and_continue): return if isinstance(action, MessageActionChatEditTitle): diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py index 086532cb..5cba5ce9 100644 --- a/mautrix_telegram/puppet.py +++ b/mautrix_telegram/puppet.py @@ -29,7 +29,8 @@ class Puppet: def __init__(self, id=None, username=None, displayname=None, photo_id=None): self.id = id - self.localpart = config.get("bridge.username_template", "telegram_{userid}").format(userid=self.id) + self.localpart = config.get("bridge.username_template", "telegram_{userid}").format( + userid=self.id) hs = config["homeserver"]["domain"] self.mxid = f"@{self.localpart}:{hs}" self.username = username @@ -75,7 +76,8 @@ class Puppet: if not format: return name - return config.get("bridge.displayname_template", "{displayname} (Telegram)").format(displayname=name) + return config.get("bridge.displayname_template", "{displayname} (Telegram)").format( + displayname=name) def update_info(self, source, info): changed = False diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 1ae1d4c6..0fe6c18b 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -97,9 +97,11 @@ class User: self.connected = False if self.tgid: try: - del self.tgid[self.tgid] + del self.by_tgid[self.tgid] except KeyError: pass + self.tgid = None + self.save() return self.client.log_out() def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True):