From 7a373fa556fc38a8629e1ec15f493ca3396ef3c5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 19 May 2018 19:34:35 +0300 Subject: [PATCH] Add option to filter telegram chats from being bridged. Fixes #41 --- example-config.yaml | 10 +++- mautrix_telegram/bot.py | 3 ++ mautrix_telegram/commands/meta.py | 4 ++ mautrix_telegram/commands/portal.py | 71 +++++++++++++++++++++++++++++ mautrix_telegram/config.py | 3 ++ mautrix_telegram/portal.py | 35 +++++++++++++- 6 files changed, 123 insertions(+), 3 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index b5db9a43..94c2982f 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -109,13 +109,21 @@ bridge: public_portals: true # Whether to send stickers as the new native m.sticker type or normal m.images. # Old versions of Riot don't support the new type at all. - # # Remember that proper sticker support always requires Pillow to convert webp into png. native_stickers: true # Whether or not to fetch and handle Telegram updates at startup from the time the bridge was down. # WARNING: Probably buggy, might get stuck in infinite loop. catch_up: false + filter: + # Filter mode to use. Either "blacklist" or "whitelist". + # If the mode is "blacklist", the listed chats will never be bridged. An empty blacklist disables the filter. + # If the mode is "whitelist", only the listed chats can be bridged. + # Direct chats are not affected. + mode: blacklist + # The list of group/channel IDs to filter. + list: [] + # The prefix for commands. Only required in non-management rooms. command_prefix: "!tg" diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py index f933e770..bf57b72a 100644 --- a/mautrix_telegram/bot.py +++ b/mautrix_telegram/bot.py @@ -145,6 +145,9 @@ class Bot(AbstractUser): if not config["bridge.relaybot.authless_portals"]: return await reply("This bridge doesn't allow portal creation from Telegram.") + if not portal.allow_bridging(): + return await reply("This bridge doesn't allow bridging this chat.") + await portal.create_matrix_room(self) if portal.mxid: if portal.username: diff --git a/mautrix_telegram/commands/meta.py b/mautrix_telegram/commands/meta.py index f8d1fe30..130b5b1b 100644 --- a/mautrix_telegram/commands/meta.py +++ b/mautrix_telegram/commands/meta.py @@ -80,5 +80,9 @@ def help(evt): **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 +**filter-mode** <`whitelist`|`blacklist`> - Change whether the bridge will allow or disallow + bridging rooms by default. """ return evt.reply(management_status + help) diff --git a/mautrix_telegram/commands/portal.py b/mautrix_telegram/commands/portal.py index 82e83f9f..f759c025 100644 --- a/mautrix_telegram/commands/portal.py +++ b/mautrix_telegram/commands/portal.py @@ -161,6 +161,10 @@ async def bridge(evt: CommandEvent): "Bridging private chats to existing rooms is not allowed.") 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" + "`$cmdprefix+sp whitelist ` first.") if portal.mxid: has_portal_message = ( "That Telegram chat already has a portal at " @@ -353,3 +357,70 @@ async def group_name(evt: CommandEvent): return await evt.reply("That username is already in use.") except UsernameInvalidError: return await evt.reply("Invalid username") + + +@command_handler(needs_admin=True) +async def filter_mode(evt: CommandEvent): + try: + mode = evt.args[0] + if mode not in ("whitelist", "blacklist"): + raise ValueError() + except (IndexError, ValueError): + return await evt.reply("**Usage:** `$cmdprefix+sp filter-mode `") + + evt.config["bridge.filter.mode"] = mode + evt.config.save() + po.Portal.filter_mode = mode + if mode == "whitelist": + return await evt.reply("The bridge will now disallow bridging chats by default.\n" + "To allow bridging a specific chat, use" + "`!filter whitelist `.") + else: + return await evt.reply("The bridge will now allow bridging chats by default.\n" + "To disallow bridging a specific chat, use" + "`!filter blacklist `.") + + +@command_handler(needs_admin=True) +async def filter(evt: CommandEvent): + try: + action = evt.args[0] + if action not in ("whitelist", "blacklist", "add", "remove"): + raise ValueError() + + id = evt.args[1] + if id.startswith("-100"): + id = int(id[4:]) + elif id.startswith("-"): + id = int(id[1:]) + else: + id = int(id) + except (IndexError, ValueError): + return await evt.reply("**Usage:** `$cmdprefix+sp filter `") + + mode = evt.config["bridge.filter.mode"] + if mode not in ("blacklist", "whitelist"): + return await evt.reply(f"Unknown filter mode \"{mode}\". Please fix the bridge config.") + + list = evt.config["bridge.filter.list"] + + if action in ("blacklist", "whitelist"): + action = "add" if mode == action else "remove" + + def save(): + evt.config["bridge.filter.list"] = list + evt.config.save() + po.Portal.filter_list = list + + if action == "add": + if id in list: + return await evt.reply(f"That chat is already {mode}ed.") + list.append(id) + save() + return await evt.reply(f"Chat ID added to {mode}.") + elif action == "remove": + if id not in list: + return await evt.reply(f"That chat is not {mode}ed.") + list.remove(id) + save() + return await evt.reply(f"Chat ID removed from {mode}.") diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 7e220c67..d4b391af 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -181,6 +181,9 @@ class Config(DictWithRecursion): copy("bridge.native_stickers") copy("bridge.catch_up") + copy("bridge.filter.mode") + copy("bridge.filter.list") + copy("bridge.command_prefix") migrate_permissions = ("bridge.permissions" not in self diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index c3846ac2..6331b8d1 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -48,6 +48,8 @@ class Portal: az = None bot = None loop = None + filter_mode = None + filter_list = None bridge_notices = False alias_template = None mx_alias_regex = None @@ -117,6 +119,26 @@ class Portal: self._main_intent = puppet.intent if direct else self.az.intent return self._main_intent + # endregion + # region Filtering + + def allow_bridging(self, tgid=None): + tgid = tgid or self.tgid + if self.peer_type == "user": + self.log.debug(f"!!!!!!!!!!!!!!!!!!!!!!!!!!!! allow_bridging(User {tgid}) -> True") + return True + elif self.filter_mode == "whitelist": + self.log.debug( + f"!!!!!!!!!!!!!!!!!!!!!!!!!!!! allow_bridging(Chat {tgid}) -> {tgid in self.filter_list} (whitelist={self.filter_list})") + return tgid in self.filter_list + elif self.filter_mode == "blacklist": + self.log.debug( + f"!!!!!!!!!!!!!!!!!!!!!!!!!!!! allow_bridging(Chat {tgid}) -> {tgid not in self.filter_list} (blacklist={self.filter_list})") + return tgid not in self.filter_list + else: + self.log.debug("!!!!!!!!!!!!!!??????????????? Unknown filter mode", self.filter_mode) + return True + # endregion # region Deduplication @@ -233,6 +255,9 @@ class Portal: if self.mxid: return self.mxid + if not self.allow_bridging(): + return None + if not entity: entity = await user.client.get_entity(self.peer) self.log.debug("Fetched data: %s", entity) @@ -336,6 +361,10 @@ class Portal: await puppet.intent.ensure_joined(self.mxid) await puppet.update_info(source, entity) + user = u.User.get_by_tgid(entity.id) + if user: + await self.invite_to_matrix(user.mxid) + # We can't trust the member list if any of the following cases is true: # * There are close to 10 000 users, because Telegram might not be sending all members. # * The member sync count is limited, because then we might ignore some members. @@ -371,7 +400,7 @@ class Portal: user = u.User.get_by_tgid(user_id) if user: user.register_portal(self) - await self.main_intent.invite(self.mxid, user.mxid) + await self.invite_to_matrix(user.mxid) async def delete_telegram_user(self, user_id, sender): puppet = p.Puppet.get(user_id) @@ -1592,7 +1621,9 @@ def init(context): global config Portal.az, Portal.db, config, Portal.loop, Portal.bot = context Portal.bridge_notices = config["bridge.bridge_notices"] + Portal.filter_mode = config["bridge.filter.mode"] + Portal.filter_list = config["bridge.filter.list"] Portal.alias_template = config.get("bridge.alias_template", "telegram_{groupname}") - Portal.hs_domain = config["homeserver"]["domain"] + Portal.hs_domain = config["homeserver.domain"] localpart = Portal.alias_template.format(groupname="(.+)") Portal.mx_alias_regex = re.compile(f"#{localpart}:{Portal.hs_domain}")