# mautrix-telegram - A Matrix-Telegram puppeting bridge # Copyright (C) 2021 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 __future__ import annotations from datetime import datetime, timedelta import re from telethon.errors import ( ChatAdminRequiredError, RPCError, UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError, ) from telethon.helpers import add_surrogate from telethon.tl.functions.channels import GetFullChannelRequest from telethon.tl.functions.messages import GetExportedChatInvitesRequest, GetFullChatRequest from telethon.tl.types import ( ChatInviteExported, InputMessageEntityMentionName, InputUserSelf, MessageEntityMention, TypeInputPeer, TypeInputUser, ) from telethon.tl.types.messages import ExportedChatInvites from mautrix.types import EventID from ... import formatter as fmt, portal as po, puppet as pu from .. import SECTION_MISC, SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler 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) -> EventID: portal = await po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") elif not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"): return await evt.reply(f"You do not have the permissions to synchronize this room.") await portal.main_intent.get_joined_members(portal.mxid) await evt.reply("Synchronization complete") @command_handler( needs_admin=False, needs_puppeting=False, needs_auth=False, help_section=SECTION_MISC ) async def sync_full(evt: CommandEvent) -> EventID: portal = await po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") if len(evt.args) > 0 and evt.args[0] == "--usebot" and evt.sender.is_admin: src = evt.tgbot else: src = evt.tgbot if await evt.sender.needs_relaybot(portal) else evt.sender try: if portal.peer_type == "channel": res = await src.client(GetFullChannelRequest(portal.peer)) elif portal.peer_type == "chat": res = await src.client(GetFullChatRequest(portal.tgid)) else: return await evt.reply("This is not a channel or chat portal.") except (ValueError, RPCError): return await evt.reply("Failed to get portal info from Telegram.") await portal.update_matrix_room(src, res.full_chat) return await evt.reply("Portal synced successfully.") @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) -> EventID: portal = await po.Portal.get_by_mxid(evt.room_id) if not portal: return await evt.reply("This is not a portal room.") tgid = portal.tgid if portal.peer_type == "chat": tgid = -tgid elif portal.peer_type == "channel": tgid = f"-100{tgid}" await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.") invite_link_usage = ( "**Usage:** `$cmdprefix+sp invite-link " "[--uses=] [--expire=] [--request-needed] -- [title]`" "\n\n" "* `--uses`: the number of times the invite link can be used." " Defaults to unlimited.\n" "* `--expire`: the duration after which the link will expire." " A number suffixed with d(ay), h(our), m(inute) or s(econd)\n" "* `--request-needed`: should the link require admins to approve joins?\n" "* `title`: a description of the link (only shown to admins)." ) def _parse_flag(args: list[str]) -> tuple[str, str]: arg = args.pop(0).lower() if arg == "--": return "", "" value = "" if arg.startswith("--"): value_start = arg.find("=") if value_start > 0: flag = arg[2:value_start] value = arg[value_start + 1 :] else: flag = arg[2:] if arg not in ("request", "request-needed"): value = args.pop(0).lower() elif arg.startswith("-"): flag = arg[1] if len(arg) > 3 and arg[2] == "=": value = arg[3:] elif arg != "r": value = args.pop(0).lower() else: raise ValueError("invalid flag") return flag, value delta_regex = re.compile( "([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)" ) def _parse_delta(value: str) -> timedelta | None: match = delta_regex.fullmatch(value) if not match: return None number = int(match.group(1)) unit = match.group(2)[0] if unit == "w": return timedelta(weeks=number) elif unit == "d": return timedelta(days=number) elif unit == "h": return timedelta(hours=number) elif unit == "m": return timedelta(minutes=number) elif unit == "s": return timedelta(seconds=number) else: return None @command_handler( help_section=SECTION_PORTAL_MANAGEMENT, help_text="Get a Telegram invite link to the current chat.", help_args="[--uses=] [--expire=