Blacken and isort code
This commit is contained in:
@@ -1,8 +1,26 @@
|
||||
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
|
||||
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
|
||||
SECTION_MISC, SECTION_ADMIN)
|
||||
from . import portal, telegram, matrix_auth
|
||||
from .handler import (
|
||||
SECTION_ADMIN,
|
||||
SECTION_AUTH,
|
||||
SECTION_CREATING_PORTALS,
|
||||
SECTION_MISC,
|
||||
SECTION_PORTAL_MANAGEMENT,
|
||||
CommandEvent,
|
||||
CommandHandler,
|
||||
CommandProcessor,
|
||||
command_handler,
|
||||
)
|
||||
|
||||
__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
|
||||
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
|
||||
"SECTION_PORTAL_MANAGEMENT"]
|
||||
# This has to happen after the handler imports
|
||||
from . import matrix_auth, portal, telegram # isort: skip
|
||||
|
||||
__all__ = [
|
||||
"command_handler",
|
||||
"CommandHandler",
|
||||
"CommandProcessor",
|
||||
"CommandEvent",
|
||||
"SECTION_AUTH",
|
||||
"SECTION_MISC",
|
||||
"SECTION_ADMIN",
|
||||
"SECTION_CREATING_PORTALS",
|
||||
"SECTION_PORTAL_MANAGEMENT",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -15,18 +15,22 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Awaitable, Callable, NamedTuple, Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, NamedTuple
|
||||
|
||||
from telethon.errors import FloodWaitError
|
||||
|
||||
from mautrix.types import RoomID, EventID, MessageEventContent
|
||||
from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent,
|
||||
CommandHandler as BaseCommandHandler,
|
||||
CommandProcessor as BaseCommandProcessor,
|
||||
CommandHandlerFunc, command_handler as base_command_handler)
|
||||
from mautrix.bridge.commands import (
|
||||
CommandEvent as BaseCommandEvent,
|
||||
CommandHandler as BaseCommandHandler,
|
||||
CommandHandlerFunc,
|
||||
CommandProcessor as BaseCommandProcessor,
|
||||
HelpSection,
|
||||
command_handler as base_command_handler,
|
||||
)
|
||||
from mautrix.types import EventID, MessageEventContent, RoomID
|
||||
from mautrix.util.format_duration import format_duration
|
||||
|
||||
from .. import user as u, portal as po
|
||||
from .. import portal as po, user as u
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..__main__ import TelegramBridge
|
||||
@@ -52,11 +56,31 @@ class CommandEvent(BaseCommandEvent):
|
||||
sender: u.User
|
||||
portal: po.Portal
|
||||
|
||||
def __init__(self, processor: CommandProcessor, room_id: RoomID, event_id: EventID,
|
||||
sender: u.User, command: str, args: list[str], content: MessageEventContent,
|
||||
portal: po.Portal | None, is_management: bool, has_bridge_bot: bool) -> None:
|
||||
super().__init__(processor, room_id, event_id, sender, command, args, content,
|
||||
portal, is_management, has_bridge_bot)
|
||||
def __init__(
|
||||
self,
|
||||
processor: CommandProcessor,
|
||||
room_id: RoomID,
|
||||
event_id: EventID,
|
||||
sender: u.User,
|
||||
command: str,
|
||||
args: list[str],
|
||||
content: MessageEventContent,
|
||||
portal: po.Portal | None,
|
||||
is_management: bool,
|
||||
has_bridge_bot: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
processor,
|
||||
room_id,
|
||||
event_id,
|
||||
sender,
|
||||
command,
|
||||
args,
|
||||
content,
|
||||
portal,
|
||||
is_management,
|
||||
has_bridge_bot,
|
||||
)
|
||||
self.bridge = processor.bridge
|
||||
self.tgbot = processor.tgbot
|
||||
self.config = processor.config
|
||||
@@ -67,9 +91,14 @@ class CommandEvent(BaseCommandEvent):
|
||||
return self.sender.is_admin
|
||||
|
||||
async def get_help_key(self) -> HelpCacheKey:
|
||||
return HelpCacheKey(self.is_management, self.portal is not None,
|
||||
self.sender.puppet_whitelisted, self.sender.matrix_puppet_whitelisted,
|
||||
self.sender.is_admin, await self.sender.is_logged_in())
|
||||
return HelpCacheKey(
|
||||
self.is_management,
|
||||
self.portal is not None,
|
||||
self.sender.puppet_whitelisted,
|
||||
self.sender.matrix_puppet_whitelisted,
|
||||
self.sender.is_admin,
|
||||
await self.sender.is_logged_in(),
|
||||
)
|
||||
|
||||
|
||||
class CommandHandler(BaseCommandHandler):
|
||||
@@ -78,14 +107,33 @@ class CommandHandler(BaseCommandHandler):
|
||||
needs_puppeting: bool
|
||||
needs_matrix_puppeting: bool
|
||||
|
||||
def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]],
|
||||
management_only: bool, name: str, help_text: str, help_args: str,
|
||||
help_section: HelpSection, needs_auth: bool, needs_puppeting: bool,
|
||||
needs_matrix_puppeting: bool, needs_admin: bool, **kwargs) -> None:
|
||||
super().__init__(handler, management_only, name, help_text, help_args, help_section,
|
||||
needs_auth=needs_auth, needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin,
|
||||
**kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
handler: Callable[[CommandEvent], Awaitable[EventID]],
|
||||
management_only: bool,
|
||||
name: str,
|
||||
help_text: str,
|
||||
help_args: str,
|
||||
help_section: HelpSection,
|
||||
needs_auth: bool,
|
||||
needs_puppeting: bool,
|
||||
needs_matrix_puppeting: bool,
|
||||
needs_admin: bool,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
handler,
|
||||
management_only,
|
||||
name,
|
||||
help_text,
|
||||
help_args,
|
||||
help_section,
|
||||
needs_auth=needs_auth,
|
||||
needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting,
|
||||
needs_admin=needs_admin,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def get_permission_error(self, evt: CommandEvent) -> str | None:
|
||||
if self.needs_puppeting and not evt.sender.puppet_whitelisted:
|
||||
@@ -95,33 +143,51 @@ class CommandHandler(BaseCommandHandler):
|
||||
return await super().get_permission_error(evt)
|
||||
|
||||
def has_permission(self, key: HelpCacheKey) -> bool:
|
||||
return (super().has_permission(key) and
|
||||
(not self.needs_puppeting or key.puppet_whitelisted) and
|
||||
(not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted))
|
||||
return (
|
||||
super().has_permission(key)
|
||||
and (not self.needs_puppeting or key.puppet_whitelisted)
|
||||
and (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted)
|
||||
)
|
||||
|
||||
|
||||
def command_handler(_func: CommandHandlerFunc | None = None, *, needs_auth: bool = True,
|
||||
needs_puppeting: bool = True, needs_matrix_puppeting: bool = False,
|
||||
needs_admin: bool = False, management_only: bool = False,
|
||||
name: str | None = None, help_text: str = "", help_args: str = "",
|
||||
help_section: HelpSection = None) -> Callable[[CommandHandlerFunc],
|
||||
CommandHandler]:
|
||||
def command_handler(
|
||||
_func: CommandHandlerFunc | None = None,
|
||||
*,
|
||||
needs_auth: bool = True,
|
||||
needs_puppeting: bool = True,
|
||||
needs_matrix_puppeting: bool = False,
|
||||
needs_admin: bool = False,
|
||||
management_only: bool = False,
|
||||
name: str | None = None,
|
||||
help_text: str = "",
|
||||
help_args: str = "",
|
||||
help_section: HelpSection = None,
|
||||
) -> Callable[[CommandHandlerFunc], CommandHandler]:
|
||||
return base_command_handler(
|
||||
_func, _handler_class=CommandHandler, name=name, help_text=help_text, help_args=help_args,
|
||||
help_section=help_section, management_only=management_only, needs_auth=needs_auth,
|
||||
needs_admin=needs_admin, needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting)
|
||||
_func,
|
||||
_handler_class=CommandHandler,
|
||||
name=name,
|
||||
help_text=help_text,
|
||||
help_args=help_args,
|
||||
help_section=help_section,
|
||||
management_only=management_only,
|
||||
needs_auth=needs_auth,
|
||||
needs_admin=needs_admin,
|
||||
needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting,
|
||||
)
|
||||
|
||||
|
||||
class CommandProcessor(BaseCommandProcessor):
|
||||
def __init__(self, bridge: 'TelegramBridge') -> None:
|
||||
def __init__(self, bridge: "TelegramBridge") -> None:
|
||||
super().__init__(event_class=CommandEvent, bridge=bridge)
|
||||
self.tgbot = bridge.bot
|
||||
self.public_website = bridge.public_website
|
||||
|
||||
@staticmethod
|
||||
async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
|
||||
) -> Any:
|
||||
async def _run_handler(
|
||||
handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
|
||||
) -> Any:
|
||||
try:
|
||||
return await handler(evt)
|
||||
except FloodWaitError as e:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -13,33 +13,27 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from mautrix.types import EventID
|
||||
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
|
||||
from mautrix.types import EventID
|
||||
|
||||
from . import command_handler, CommandEvent, SECTION_AUTH
|
||||
from .. import puppet as pu
|
||||
from . import SECTION_AUTH, CommandEvent, command_handler
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH, help_text="Revert your Telegram account's Matrix "
|
||||
"puppet to use the default Matrix account.")
|
||||
async def logout_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
|
||||
if not puppet.is_real_user:
|
||||
return await evt.reply("You are not logged in with your Matrix account.")
|
||||
await puppet.switch_mxid(None, None)
|
||||
return await evt.reply("Reverted your Telegram account's Matrix puppet back to the default.")
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, management_only=True, needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Replace your Telegram account's Matrix puppet with your own Matrix "
|
||||
"account.")
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=True,
|
||||
needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Replace your Telegram account's Matrix puppet with your own Matrix account.",
|
||||
)
|
||||
async def login_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
|
||||
if puppet.is_real_user:
|
||||
return await evt.reply("You have already logged in with your Matrix account. "
|
||||
"Log out with `$cmdprefix+sp logout-matrix` first.")
|
||||
return await evt.reply(
|
||||
"You have already logged in with your Matrix account. "
|
||||
"Log out with `$cmdprefix+sp logout-matrix` first."
|
||||
)
|
||||
allow_matrix_login = evt.config.get("bridge.allow_matrix_login", True)
|
||||
if allow_matrix_login:
|
||||
evt.sender.command_status = {
|
||||
@@ -57,57 +51,35 @@ async def login_matrix(evt: CommandEvent) -> EventID:
|
||||
"here.\n"
|
||||
f"If you would like to log in outside of Matrix, [click here]({url}).\n\n"
|
||||
"Logging in outside of Matrix is recommended, because in-Matrix login would save "
|
||||
"your access token in the message history.")
|
||||
return await evt.reply("This bridge instance does not allow logging in inside Matrix.\n\n"
|
||||
f"Please visit [the login page]({url}) to log in.")
|
||||
"your access token in the message history."
|
||||
)
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow logging in inside Matrix.\n\n"
|
||||
f"Please visit [the login page]({url}) to log in."
|
||||
)
|
||||
elif allow_matrix_login:
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow you to log in outside of Matrix.\n\n"
|
||||
"Please send your Matrix access token here to log in.")
|
||||
"Please send your Matrix access token here to log in."
|
||||
)
|
||||
return await evt.reply("This bridge instance has been configured to not allow logging in.")
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Pings the server with the stored matrix authentication.")
|
||||
async def ping_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
|
||||
if not puppet.is_real_user:
|
||||
return await evt.reply("You are not logged in with your Matrix account.")
|
||||
try:
|
||||
await puppet.start()
|
||||
except InvalidAccessToken:
|
||||
return await evt.reply("Your access token is invalid.")
|
||||
return await evt.reply("Your Matrix login is working.")
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True, help_section=SECTION_AUTH,
|
||||
help_text="Clear the Matrix sync token stored for your custom puppet.")
|
||||
async def clear_cache_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
|
||||
if not puppet.is_real_user:
|
||||
return await evt.reply("You are not logged in with your Matrix account.")
|
||||
try:
|
||||
puppet.stop()
|
||||
puppet.next_batch = None
|
||||
await puppet.start()
|
||||
except InvalidAccessToken:
|
||||
return await evt.reply("Your access token is invalid.")
|
||||
return await evt.reply("Cleared cache successfully.")
|
||||
|
||||
|
||||
async def enter_matrix_token(evt: CommandEvent) -> EventID:
|
||||
evt.sender.command_status = None
|
||||
|
||||
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
|
||||
if puppet.is_real_user:
|
||||
return await evt.reply("You have already logged in with your Matrix account. "
|
||||
"Log out with `$cmdprefix+sp logout-matrix` first.")
|
||||
return await evt.reply(
|
||||
"You have already logged in with your Matrix account. "
|
||||
"Log out with `$cmdprefix+sp logout-matrix` first."
|
||||
)
|
||||
try:
|
||||
await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
|
||||
except OnlyLoginSelf:
|
||||
return await evt.reply("You can only log in as your own Matrix user.")
|
||||
except InvalidAccessToken:
|
||||
return await evt.reply("Failed to verify access token.")
|
||||
return await evt.reply("Replaced your Telegram account's Matrix puppet "
|
||||
f"with {puppet.custom_mxid}.")
|
||||
return await evt.reply(
|
||||
"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}."
|
||||
)
|
||||
|
||||
@@ -18,13 +18,16 @@ import asyncio
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po, puppet as pu, user as u
|
||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
from .. import SECTION_ADMIN, CommandEvent, command_handler
|
||||
|
||||
|
||||
@command_handler(needs_admin=True, needs_auth=False,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`portal`|`puppet`|`user`>",
|
||||
help_text="Clear internal bridge caches")
|
||||
@command_handler(
|
||||
needs_admin=True,
|
||||
needs_auth=False,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`portal`|`puppet`|`user`>",
|
||||
help_text="Clear internal bridge caches",
|
||||
)
|
||||
async def clear_db_cache(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
section = evt.args[0].lower()
|
||||
@@ -44,19 +47,19 @@ async def clear_db_cache(evt: CommandEvent) -> EventID:
|
||||
)
|
||||
await evt.reply("Cleared puppet cache and restarted custom puppet syncers")
|
||||
elif section == "user":
|
||||
u.User.by_mxid = {
|
||||
user.mxid: user
|
||||
for user in u.User.by_tgid.values()
|
||||
}
|
||||
u.User.by_mxid = {user.mxid: user for user in u.User.by_tgid.values()}
|
||||
await evt.reply("Cleared non-logged-in user cache")
|
||||
else:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp clear-db-cache <section>`")
|
||||
|
||||
|
||||
@command_handler(needs_admin=True, needs_auth=False,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="[_mxid_]",
|
||||
help_text="Reload and reconnect a user")
|
||||
@command_handler(
|
||||
needs_admin=True,
|
||||
needs_auth=False,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="[_mxid_]",
|
||||
help_text="Reload and reconnect a user",
|
||||
)
|
||||
async def reload_user(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) > 0:
|
||||
mxid = evt.args[0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -18,26 +18,31 @@ from __future__ import annotations
|
||||
from typing import Awaitable
|
||||
import asyncio
|
||||
|
||||
from telethon.tl.types import ChatForbidden, ChannelForbidden
|
||||
from telethon.tl.types import ChannelForbidden, ChatForbidden
|
||||
|
||||
from mautrix.types import EventID, RoomID
|
||||
|
||||
from ...types import TelegramID
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
||||
from .util import user_has_power_level, get_initial_state, warn_missing_power
|
||||
from ...types import TelegramID
|
||||
from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
|
||||
from .util import get_initial_state, user_has_power_level, warn_missing_power
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_CREATING_PORTALS,
|
||||
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.")
|
||||
@command_handler(
|
||||
needs_auth=False,
|
||||
needs_puppeting=False,
|
||||
help_section=SECTION_CREATING_PORTALS,
|
||||
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) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** "
|
||||
"`$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`")
|
||||
return await evt.reply(
|
||||
"**Usage:** `$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`"
|
||||
)
|
||||
force_use_bot = False
|
||||
if evt.args[0] == "--usebot" and evt.sender.is_admin:
|
||||
force_use_bot = True
|
||||
@@ -61,24 +66,30 @@ async def bridge(evt: CommandEvent) -> EventID:
|
||||
tgid = TelegramID(-int(tgid_str))
|
||||
peer_type = "chat"
|
||||
else:
|
||||
return await evt.reply("That doesn't seem like a prefixed Telegram chat ID.\n\n"
|
||||
"If you did not get the ID using the `/id` bot command, please "
|
||||
"prefix channel IDs with `-100` and normal group IDs with `-`.\n\n"
|
||||
"Bridging private chats to existing rooms is not allowed.")
|
||||
return await evt.reply(
|
||||
"That doesn't seem like a prefixed Telegram chat ID.\n\n"
|
||||
"If you did not get the ID using the `/id` bot command, please "
|
||||
"prefix channel IDs with `-100` and normal group IDs with `-`.\n\n"
|
||||
"Bridging private chats to existing rooms is not allowed."
|
||||
)
|
||||
|
||||
portal = await 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 filter whitelist <Telegram chat ID>` first.")
|
||||
return await evt.reply(
|
||||
"This bridge doesn't allow bridging that Telegram chat.\n"
|
||||
"If you're the bridge admin, try "
|
||||
"`$cmdprefix+sp filter whitelist <Telegram chat ID>` first."
|
||||
)
|
||||
if portal.mxid:
|
||||
has_portal_message = (
|
||||
"That Telegram chat already has a portal at "
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ")
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). "
|
||||
)
|
||||
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
|
||||
return await evt.reply(f"{has_portal_message}"
|
||||
"Additionally, you do not have the permissions to unbridge "
|
||||
"that room.")
|
||||
return await evt.reply(
|
||||
f"{has_portal_message}"
|
||||
"Additionally, you do not have the permissions to unbridge that room."
|
||||
)
|
||||
evt.sender.command_status = {
|
||||
"next": confirm_bridge,
|
||||
"action": "Room bridging",
|
||||
@@ -88,12 +99,14 @@ async def bridge(evt: CommandEvent) -> EventID:
|
||||
"peer_type": portal.peer_type,
|
||||
"force_use_bot": force_use_bot,
|
||||
}
|
||||
return await evt.reply(f"{has_portal_message}"
|
||||
"However, you have the permissions to unbridge that room.\n\n"
|
||||
"To delete that portal completely and continue bridging, use "
|
||||
"`$cmdprefix+sp delete-and-continue`. To unbridge the portal "
|
||||
"without kicking Matrix users, use `$cmdprefix+sp unbridge-and-"
|
||||
"continue`. To cancel, use `$cmdprefix+sp cancel`")
|
||||
return await evt.reply(
|
||||
f"{has_portal_message}"
|
||||
"However, you have the permissions to unbridge that room.\n\n"
|
||||
"To delete that portal completely and continue bridging, use "
|
||||
"`$cmdprefix+sp delete-and-continue`. To unbridge the portal "
|
||||
"without kicking Matrix users, use `$cmdprefix+sp unbridge-and-"
|
||||
"continue`. To cancel, use `$cmdprefix+sp cancel`"
|
||||
)
|
||||
evt.sender.command_status = {
|
||||
"next": confirm_bridge,
|
||||
"action": "Room bridging",
|
||||
@@ -102,29 +115,36 @@ async def bridge(evt: CommandEvent) -> EventID:
|
||||
"peer_type": portal.peer_type,
|
||||
"force_use_bot": force_use_bot,
|
||||
}
|
||||
return await evt.reply("That Telegram chat has no existing portal. To confirm bridging the "
|
||||
"chat to this room, use `$cmdprefix+sp continue`")
|
||||
return await evt.reply(
|
||||
"That Telegram chat has no existing portal. To confirm bridging the "
|
||||
"chat to this room, use `$cmdprefix+sp continue`"
|
||||
)
|
||||
|
||||
|
||||
async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal"
|
||||
) -> tuple[bool, Awaitable[None] | None]:
|
||||
async def cleanup_old_portal_while_bridging(
|
||||
evt: CommandEvent, portal: po.Portal
|
||||
) -> tuple[bool, Awaitable[None] | None]:
|
||||
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"
|
||||
"Continuing without touching previous Matrix room...")
|
||||
await evt.reply(
|
||||
"The portal seems to have lost its Matrix room between you"
|
||||
"calling `$cmdprefix+sp bridge` and this command.\n\n"
|
||||
"Continuing without touching previous Matrix room..."
|
||||
)
|
||||
return True, None
|
||||
elif evt.args[0] == "delete-and-continue":
|
||||
return True, portal.cleanup_portal("Portal deleted (moving to another room)", delete=False)
|
||||
elif evt.args[0] == "unbridge-and-continue":
|
||||
return True, portal.cleanup_portal("Room unbridged (portal moving to another room)",
|
||||
puppets_only=True, delete=False)
|
||||
return True, portal.cleanup_portal(
|
||||
"Room unbridged (portal moving to another room)", puppets_only=True, delete=False
|
||||
)
|
||||
else:
|
||||
await evt.reply(
|
||||
"The chat you were trying to bridge already has a Matrix portal room.\n\n"
|
||||
"Please use `$cmdprefix+sp delete-and-continue` or `$cmdprefix+sp unbridge-and-"
|
||||
"continue` to either delete or unbridge the existing room (respectively) and "
|
||||
"continue with the bridging.\n\n"
|
||||
"If you changed your mind, use `$cmdprefix+sp cancel` to cancel.")
|
||||
"If you changed your mind, use `$cmdprefix+sp cancel` to cancel."
|
||||
)
|
||||
return False, None
|
||||
|
||||
|
||||
@@ -135,9 +155,10 @@ async def confirm_bridge(evt: CommandEvent) -> EventID | None:
|
||||
bridge_to_mxid = status["bridge_to_mxid"]
|
||||
except KeyError:
|
||||
evt.sender.command_status = None
|
||||
return await evt.reply("Fatal error: tgid or peer_type missing from command_status. "
|
||||
"This shouldn't happen unless you're messing with the command "
|
||||
"handler code.")
|
||||
return await evt.reply(
|
||||
"Fatal error: tgid or peer_type missing from command_status. "
|
||||
"This shouldn't happen unless you're messing with the command handler code."
|
||||
)
|
||||
|
||||
is_logged_in = await evt.sender.is_logged_in() and not status["force_use_bot"]
|
||||
|
||||
@@ -150,32 +171,41 @@ async def confirm_bridge(evt: CommandEvent) -> EventID | None:
|
||||
await evt.reply("Cleaning up previous portal room...")
|
||||
elif portal.mxid:
|
||||
evt.sender.command_status = None
|
||||
return await evt.reply("The portal seems to have created a Matrix room between you "
|
||||
"calling `$cmdprefix+sp bridge` and this command.\n\n"
|
||||
"Please start over by calling the bridge command again.")
|
||||
return await evt.reply(
|
||||
"The portal seems to have created a Matrix room between you "
|
||||
"calling `$cmdprefix+sp bridge` and this command.\n\n"
|
||||
"Please start over by calling the bridge command again."
|
||||
)
|
||||
elif evt.args[0] != "continue":
|
||||
return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or "
|
||||
"`$cmdprefix+sp cancel` to cancel.")
|
||||
return await evt.reply(
|
||||
"Please use `$cmdprefix+sp continue` to confirm the bridging or "
|
||||
"`$cmdprefix+sp cancel` to cancel."
|
||||
)
|
||||
|
||||
evt.sender.command_status = None
|
||||
async with portal._room_create_lock:
|
||||
await _locked_confirm_bridge(evt, portal=portal, room_id=bridge_to_mxid,
|
||||
is_logged_in=is_logged_in)
|
||||
await _locked_confirm_bridge(
|
||||
evt, portal=portal, room_id=bridge_to_mxid, is_logged_in=is_logged_in
|
||||
)
|
||||
|
||||
|
||||
async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id: RoomID,
|
||||
is_logged_in: bool) -> EventID | None:
|
||||
async def _locked_confirm_bridge(
|
||||
evt: CommandEvent, portal: po.Portal, room_id: RoomID, is_logged_in: bool
|
||||
) -> EventID | None:
|
||||
user = evt.sender if is_logged_in else evt.tgbot
|
||||
try:
|
||||
entity = await user.client.get_entity(portal.peer)
|
||||
except Exception:
|
||||
evt.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
|
||||
if is_logged_in:
|
||||
return await evt.reply("Failed to get info of telegram chat. "
|
||||
"You are logged in, are you in that chat?")
|
||||
return await evt.reply(
|
||||
"Failed to get info of telegram chat. You are logged in, are you in that chat?"
|
||||
)
|
||||
else:
|
||||
return await evt.reply("Failed to get info of telegram chat. "
|
||||
"You're not logged in, is the relay bot in the chat?")
|
||||
return await evt.reply(
|
||||
"Failed to get info of telegram chat. "
|
||||
"You're not logged in, is the relay bot in the chat?"
|
||||
)
|
||||
if isinstance(entity, (ChatForbidden, ChannelForbidden)):
|
||||
if is_logged_in:
|
||||
return await evt.reply("You don't seem to be in that chat.")
|
||||
@@ -184,14 +214,14 @@ async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id
|
||||
|
||||
portal.mxid = room_id
|
||||
portal.by_mxid[portal.mxid] = portal
|
||||
(portal.title, portal.about, levels,
|
||||
portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
|
||||
(portal.title, portal.about, levels, portal.encrypted) = await get_initial_state(
|
||||
evt.az.intent, evt.room_id
|
||||
)
|
||||
portal.photo_id = ""
|
||||
await portal.save()
|
||||
await portal.update_bridge_info()
|
||||
|
||||
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
|
||||
loop=evt.loop)
|
||||
asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels))
|
||||
|
||||
await warn_missing_power(levels, evt)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -13,21 +13,26 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import Awaitable, Any
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Awaitable
|
||||
from io import StringIO
|
||||
|
||||
from ruamel.yaml import YAMLError
|
||||
|
||||
from mautrix.util.config import yaml
|
||||
from mautrix.types import EventID
|
||||
from mautrix.util.config import yaml
|
||||
|
||||
from ... import portal as po, util
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
from .. import SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="View or change per-portal settings.",
|
||||
help_args="<`help`|_subcommand_> [...]")
|
||||
@command_handler(
|
||||
needs_auth=False,
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="View or change per-portal settings.",
|
||||
help_args="<`help`|_subcommand_> [...]",
|
||||
)
|
||||
async def config(evt: CommandEvent) -> None:
|
||||
cmd = evt.args[0].lower() if len(evt.args) > 0 else "help"
|
||||
if cmd not in ("view", "defaults", "set", "unset", "add", "del"):
|
||||
@@ -67,7 +72,8 @@ async def config(evt: CommandEvent) -> None:
|
||||
|
||||
|
||||
def config_help(evt: CommandEvent) -> Awaitable[EventID]:
|
||||
return evt.reply("""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
||||
return evt.reply(
|
||||
"""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
||||
|
||||
* **help** - View this help text.
|
||||
* **view** - View the current config data.
|
||||
@@ -76,7 +82,8 @@ def config_help(evt: CommandEvent) -> Awaitable[EventID]:
|
||||
* **unset** <_key_> - Remove a config value.
|
||||
* **add** <_key_> <_value_> - Add a value to an array.
|
||||
* **del** <_key_> <_value_> - Remove a value from an array.
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
|
||||
@@ -84,18 +91,20 @@ def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
|
||||
|
||||
|
||||
def config_defaults(evt: CommandEvent) -> Awaitable[EventID]:
|
||||
value = _str_value({
|
||||
"bridge_notices": {
|
||||
"default": evt.config["bridge.bridge_notices.default"],
|
||||
"exceptions": evt.config["bridge.bridge_notices.exceptions"],
|
||||
},
|
||||
"bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
|
||||
"inline_images": evt.config["bridge.inline_images"],
|
||||
"message_formats": evt.config["bridge.message_formats"],
|
||||
"emote_format": evt.config["bridge.emote_format"],
|
||||
"state_event_formats": evt.config["bridge.state_event_formats"],
|
||||
"telegram_link_preview": evt.config["bridge.telegram_link_preview"],
|
||||
})
|
||||
value = _str_value(
|
||||
{
|
||||
"bridge_notices": {
|
||||
"default": evt.config["bridge.bridge_notices.default"],
|
||||
"exceptions": evt.config["bridge.bridge_notices.exceptions"],
|
||||
},
|
||||
"bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
|
||||
"inline_images": evt.config["bridge.inline_images"],
|
||||
"message_formats": evt.config["bridge.message_formats"],
|
||||
"emote_format": evt.config["bridge.emote_format"],
|
||||
"state_event_formats": evt.config["bridge.state_event_formats"],
|
||||
"telegram_link_preview": evt.config["bridge.telegram_link_preview"],
|
||||
}
|
||||
)
|
||||
return evt.reply(f"Bridge instance wide config:\n{value.rstrip()}")
|
||||
|
||||
|
||||
@@ -115,8 +124,7 @@ def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: Any) -> Aw
|
||||
elif util.recursive_set(portal.local_config, key, value):
|
||||
return evt.reply(f"Successfully set the value of `{key}` to {_str_value(value)}".rstrip())
|
||||
else:
|
||||
return evt.reply(f"Failed to set value of `{key}`. "
|
||||
"Does the path contain non-map types?")
|
||||
return evt.reply(f"Failed to set value of `{key}`. Does the path contain non-map types?")
|
||||
|
||||
|
||||
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[EventID]:
|
||||
@@ -128,15 +136,17 @@ def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Ev
|
||||
return evt.reply(f"`{key}` not found in config.")
|
||||
|
||||
|
||||
def config_add_del(evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
|
||||
) -> Awaitable[EventID]:
|
||||
def config_add_del(
|
||||
evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
|
||||
) -> Awaitable[EventID]:
|
||||
if not key or value is None:
|
||||
return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
|
||||
|
||||
arr = util.recursive_get(portal.local_config, key)
|
||||
if not arr:
|
||||
return evt.reply(f"`{key}` not found in config. "
|
||||
f"Maybe do `$cmdprefix+sp config set {key} []` first?")
|
||||
return evt.reply(
|
||||
f"`{key}` not found in config. Maybe do `$cmdprefix+sp config set {key} []` first?"
|
||||
)
|
||||
elif not isinstance(arr, list):
|
||||
return evt.reply("`{key}` does not seem to be an array.")
|
||||
elif cmd == "add":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -13,24 +13,30 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from ...types import TelegramID
|
||||
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
||||
from .util import user_has_power_level, get_initial_state, warn_missing_power
|
||||
from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
|
||||
from .util import get_initial_state, user_has_power_level, warn_missing_power
|
||||
|
||||
|
||||
@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 "
|
||||
"`supergroup`).")
|
||||
@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 `supergroup`)."
|
||||
),
|
||||
)
|
||||
async def create(evt: CommandEvent) -> EventID:
|
||||
type = evt.args[0] if len(evt.args) > 0 else "supergroup"
|
||||
if type not in ("chat", "group", "supergroup", "channel"):
|
||||
return await evt.reply(
|
||||
"**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
|
||||
"**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`"
|
||||
)
|
||||
|
||||
if await po.Portal.get_by_mxid(evt.room_id):
|
||||
return await evt.reply("This is already a portal room.")
|
||||
@@ -50,14 +56,23 @@ async def create(evt: CommandEvent) -> EventID:
|
||||
"group": "chat",
|
||||
}[type]
|
||||
|
||||
portal = po.Portal(tgid=TelegramID(0), tg_receiver=TelegramID(0), peer_type=type,
|
||||
mxid=evt.room_id, title=title, about=about, encrypted=encrypted)
|
||||
portal = po.Portal(
|
||||
tgid=TelegramID(0),
|
||||
tg_receiver=TelegramID(0),
|
||||
peer_type=type,
|
||||
mxid=evt.room_id,
|
||||
title=title,
|
||||
about=about,
|
||||
encrypted=encrypted,
|
||||
)
|
||||
invites, errors = await portal.get_telegram_users_in_matrix_room(evt.sender)
|
||||
if len(errors) > 0:
|
||||
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
|
||||
await evt.reply(f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
|
||||
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
|
||||
"those users.")
|
||||
await evt.reply(
|
||||
f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
|
||||
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
|
||||
"those users."
|
||||
)
|
||||
|
||||
await warn_missing_power(levels, evt)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -13,17 +13,20 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
from .. import SECTION_ADMIN, CommandEvent, command_handler
|
||||
|
||||
|
||||
@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.")
|
||||
@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) -> EventID:
|
||||
try:
|
||||
mode = evt.args[0]
|
||||
@@ -36,19 +39,26 @@ async def filter_mode(evt: CommandEvent) -> EventID:
|
||||
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 <chat ID>`.")
|
||||
return await evt.reply(
|
||||
"The bridge will now disallow bridging chats by default.\n"
|
||||
"To allow bridging a specific chat, use"
|
||||
"`!filter whitelist <chat ID>`."
|
||||
)
|
||||
else:
|
||||
return await evt.reply("The bridge will now allow bridging chats by default.\n"
|
||||
"To disallow bridging a specific chat, use"
|
||||
"`!filter blacklist <chat ID>`.")
|
||||
return await evt.reply(
|
||||
"The bridge will now allow bridging chats by default.\n"
|
||||
"To disallow bridging a specific chat, use"
|
||||
"`!filter blacklist <chat ID>`."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(name="filter", needs_admin=True,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
|
||||
help_text="Allow or disallow bridging a specific chat.")
|
||||
@command_handler(
|
||||
name="filter",
|
||||
needs_admin=True,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
|
||||
help_text="Allow or disallow bridging a specific chat.",
|
||||
)
|
||||
async def edit_filter(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
action = evt.args[0]
|
||||
@@ -67,7 +77,7 @@ async def edit_filter(evt: CommandEvent) -> EventID:
|
||||
|
||||
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.")
|
||||
return await evt.reply(f'Unknown filter mode "{mode}". Please fix the bridge config.')
|
||||
|
||||
filter_id_list = evt.config["bridge.filter.list"]
|
||||
|
||||
|
||||
@@ -15,24 +15,33 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
||||
from telethon.errors import (
|
||||
ChatAdminRequiredError,
|
||||
RPCError,
|
||||
UsernameInvalidError,
|
||||
UsernameNotModifiedError,
|
||||
UsernameOccupiedError,
|
||||
)
|
||||
from telethon.tl.functions.channels import GetFullChannelRequest
|
||||
from telethon.tl.functions.messages import GetFullChatRequest
|
||||
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
|
||||
UsernameNotModifiedError, UsernameOccupiedError, RPCError)
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC
|
||||
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.")
|
||||
@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:
|
||||
@@ -44,8 +53,9 @@ async def sync_state(evt: CommandEvent) -> EventID:
|
||||
await evt.reply("Synchronization complete")
|
||||
|
||||
|
||||
@command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False,
|
||||
help_section=SECTION_MISC)
|
||||
@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:
|
||||
@@ -70,9 +80,14 @@ async def sync_full(evt: CommandEvent) -> EventID:
|
||||
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.")
|
||||
@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:
|
||||
@@ -85,12 +100,14 @@ async def get_id(evt: CommandEvent) -> EventID:
|
||||
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
|
||||
|
||||
|
||||
invite_link_usage = ("**Usage:** `$cmdprefix+sp invite-link [--uses=<amount>] [--expire=<delta>]`"
|
||||
"\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)")
|
||||
invite_link_usage = (
|
||||
"**Usage:** `$cmdprefix+sp invite-link [--uses=<amount>] [--expire=<delta>]`"
|
||||
"\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)"
|
||||
)
|
||||
|
||||
|
||||
def _parse_flag(args: list[str]) -> tuple[str, str]:
|
||||
@@ -99,7 +116,7 @@ def _parse_flag(args: list[str]) -> tuple[str, str]:
|
||||
value_start = arg.index("=")
|
||||
if value_start:
|
||||
flag = arg[2:value_start]
|
||||
value = arg[value_start+1:]
|
||||
value = arg[value_start + 1 :]
|
||||
else:
|
||||
flag = arg[2:]
|
||||
value = args.pop(0).lower()
|
||||
@@ -114,7 +131,9 @@ def _parse_flag(args: list[str]) -> tuple[str, str]:
|
||||
return flag, value
|
||||
|
||||
|
||||
delta_regex = re.compile("([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)")
|
||||
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:
|
||||
@@ -137,9 +156,11 @@ def _parse_delta(value: str) -> timedelta | None:
|
||||
return None
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Get a Telegram invite link to the current chat.",
|
||||
help_args="[--uses=<amount>] [--expire=<time delta, e.g. 1d>]")
|
||||
@command_handler(
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Get a Telegram invite link to the current chat.",
|
||||
help_args="[--uses=<amount>] [--expire=<time delta, e.g. 1d>]",
|
||||
)
|
||||
async def invite_link(evt: CommandEvent) -> EventID:
|
||||
# TODO once we switch to Python 3.9 minimum, use argparse with exit_on_error=False
|
||||
uses = None
|
||||
@@ -176,8 +197,10 @@ async def invite_link(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("You don't have the permission to create an invite link.")
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Upgrade a normal Telegram group to a supergroup.")
|
||||
@command_handler(
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Upgrade a normal Telegram group to a supergroup.",
|
||||
)
|
||||
async def upgrade(evt: CommandEvent) -> EventID:
|
||||
portal = await po.Portal.get_by_mxid(evt.room_id)
|
||||
if not portal:
|
||||
@@ -196,10 +219,13 @@ async def upgrade(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply(e.args[0])
|
||||
|
||||
|
||||
@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.")
|
||||
@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) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp group-name <name/->`")
|
||||
@@ -211,15 +237,15 @@ async def group_name(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("Only channels and supergroups have usernames.")
|
||||
|
||||
try:
|
||||
await portal.set_telegram_username(evt.sender,
|
||||
evt.args[0] if evt.args[0] != "-" else "")
|
||||
await portal.set_telegram_username(evt.sender, evt.args[0] if evt.args[0] != "-" else "")
|
||||
if portal.username:
|
||||
return await evt.reply(f"Username of channel changed to {portal.username}.")
|
||||
else:
|
||||
return await evt.reply(f"Channel is now private.")
|
||||
except ChatAdminRequiredError:
|
||||
return await evt.reply(
|
||||
"You don't have the permission to set the username of this channel.")
|
||||
"You don't have the permission to set the username of this channel."
|
||||
)
|
||||
except UsernameNotModifiedError:
|
||||
if portal.username:
|
||||
return await evt.reply("That is already the username of this channel.")
|
||||
|
||||
@@ -17,10 +17,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from mautrix.types import RoomID, EventID
|
||||
from mautrix.types import EventID, RoomID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
from .. import SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
|
||||
from .util import user_has_power_level
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ async def _get_portal_and_check_permission(evt: CommandEvent) -> po.Portal | Non
|
||||
return portal
|
||||
|
||||
|
||||
def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str,
|
||||
completed_message: str) -> dict:
|
||||
def _get_portal_murder_function(
|
||||
action: str, room_id: str, function: Callable, command: str, completed_message: str
|
||||
) -> dict:
|
||||
async def post_confirm(confirm) -> EventID | None:
|
||||
confirm.sender.command_status = None
|
||||
if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}":
|
||||
@@ -63,40 +64,55 @@ def _get_portal_murder_function(action: str, room_id: str, function: Callable, c
|
||||
}
|
||||
|
||||
|
||||
@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.")
|
||||
@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) -> EventID | None:
|
||||
portal = await _get_portal_and_check_permission(evt)
|
||||
if not portal:
|
||||
return None
|
||||
|
||||
evt.sender.command_status = _get_portal_murder_function("Portal deletion", portal.mxid,
|
||||
portal.cleanup_and_delete, "delete",
|
||||
"Portal successfully deleted.")
|
||||
return await evt.reply("Please confirm deletion of portal "
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
|
||||
f"to Telegram chat \"{portal.title}\" "
|
||||
"by typing `$cmdprefix+sp confirm-delete`"
|
||||
"\n\n"
|
||||
"**WARNING:** If the bridge bot has the power level to do so, **this "
|
||||
"will kick ALL users** in the room. If you just want to remove the "
|
||||
"bridge, use `$cmdprefix+sp unbridge` instead.")
|
||||
evt.sender.command_status = _get_portal_murder_function(
|
||||
"Portal deletion",
|
||||
portal.mxid,
|
||||
portal.cleanup_and_delete,
|
||||
"delete",
|
||||
"Portal successfully deleted.",
|
||||
)
|
||||
return await evt.reply(
|
||||
"Please confirm deletion of portal "
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
|
||||
f'to Telegram chat "{portal.title}" '
|
||||
"by typing `$cmdprefix+sp confirm-delete`"
|
||||
"\n\n"
|
||||
"**WARNING:** If the bridge bot has the power level to do so, **this "
|
||||
"will kick ALL users** in the room. If you just want to remove the "
|
||||
"bridge, use `$cmdprefix+sp unbridge` instead."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Remove puppets from the current portal room and forget the portal.")
|
||||
@command_handler(
|
||||
needs_auth=False,
|
||||
needs_puppeting=False,
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Remove puppets from the current portal room and forget the portal.",
|
||||
)
|
||||
async def unbridge(evt: CommandEvent) -> EventID | None:
|
||||
portal = await _get_portal_and_check_permission(evt)
|
||||
if not portal:
|
||||
return None
|
||||
|
||||
evt.sender.command_status = _get_portal_murder_function("Room unbridging", portal.mxid,
|
||||
portal.unbridge, "unbridge",
|
||||
"Room successfully unbridged.")
|
||||
return await evt.reply(f"Please confirm unbridging chat \"{portal.title}\" from room "
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
|
||||
"by typing `$cmdprefix+sp confirm-unbridge`")
|
||||
evt.sender.command_status = _get_portal_murder_function(
|
||||
"Room unbridging", portal.mxid, portal.unbridge, "unbridge", "Room successfully unbridged."
|
||||
)
|
||||
return await evt.reply(
|
||||
f'Please confirm unbridging chat "{portal.title}" from room '
|
||||
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
|
||||
"by typing `$cmdprefix+sp confirm-unbridge`"
|
||||
)
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.appservice import IntentAPI
|
||||
from mautrix.types import RoomID, EventType, PowerLevelStateEventContent
|
||||
from .. import CommandEvent
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.types import EventType, PowerLevelStateEventContent, RoomID
|
||||
|
||||
from ... import user as u
|
||||
from .. import CommandEvent
|
||||
|
||||
|
||||
async def get_initial_state(
|
||||
@@ -51,14 +51,16 @@ async def get_initial_state(
|
||||
|
||||
async def warn_missing_power(levels: PowerLevelStateEventContent, evt: CommandEvent) -> None:
|
||||
if levels.get_user_level(evt.az.bot_mxid) < levels.redact:
|
||||
await evt.reply("Warning: The bot does not have privileges to redact messages on Matrix. "
|
||||
"Message deletions from Telegram will not be bridged unless you give "
|
||||
"redaction permissions to "
|
||||
f"[{evt.az.bot_mxid}](https://matrix.to/#/{evt.az.bot_mxid})")
|
||||
await evt.reply(
|
||||
"Warning: The bot does not have privileges to redact messages on Matrix. "
|
||||
"Message deletions from Telegram will not be bridged unless you give "
|
||||
f"redaction permissions to [{evt.az.bot_mxid}](https://matrix.to/#/{evt.az.bot_mxid})"
|
||||
)
|
||||
|
||||
|
||||
async def user_has_power_level(room_id: RoomID, intent: IntentAPI, sender: u.User,
|
||||
event: str) -> bool:
|
||||
async def user_has_power_level(
|
||||
room_id: RoomID, intent: IntentAPI, sender: u.User, event: str
|
||||
) -> bool:
|
||||
if sender.is_admin:
|
||||
return True
|
||||
# Make sure the state store contains the power levels.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 Tulir Asokan
|
||||
# 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
|
||||
@@ -13,22 +13,36 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from telethon.errors import (UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError,
|
||||
HashInvalidError, AuthKeyError, FirstNameInvalidError,
|
||||
AboutTooLongError)
|
||||
from __future__ import annotations
|
||||
|
||||
from telethon.errors import (
|
||||
AboutTooLongError,
|
||||
AuthKeyError,
|
||||
FirstNameInvalidError,
|
||||
HashInvalidError,
|
||||
UsernameInvalidError,
|
||||
UsernameNotModifiedError,
|
||||
UsernameOccupiedError,
|
||||
)
|
||||
from telethon.tl.functions.account import (
|
||||
GetAuthorizationsRequest,
|
||||
ResetAuthorizationRequest,
|
||||
UpdateProfileRequest,
|
||||
UpdateUsernameRequest,
|
||||
)
|
||||
from telethon.tl.types import Authorization
|
||||
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
|
||||
ResetAuthorizationRequest, UpdateProfileRequest)
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from .. import command_handler, CommandEvent, SECTION_AUTH
|
||||
from .. import SECTION_AUTH, CommandEvent, command_handler
|
||||
|
||||
|
||||
@command_handler(needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_new username_>",
|
||||
help_text="Change your Telegram username.")
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_new username_>",
|
||||
help_text="Change your Telegram username.",
|
||||
)
|
||||
async def username(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp username <new username>`")
|
||||
@@ -40,8 +54,9 @@ async def username(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
await evt.sender.client(UpdateUsernameRequest(username=new_name))
|
||||
except UsernameInvalidError:
|
||||
return await evt.reply("Invalid username. Usernames must be between 5 and 30 alphanumeric "
|
||||
"characters.")
|
||||
return await evt.reply(
|
||||
"Invalid username. Usernames must be between 5 and 30 alphanumeric characters."
|
||||
)
|
||||
except UsernameNotModifiedError:
|
||||
return await evt.reply("That is your current username.")
|
||||
except UsernameOccupiedError:
|
||||
@@ -53,10 +68,12 @@ async def username(evt: CommandEvent) -> EventID:
|
||||
await evt.reply(f"Username changed to {evt.sender.tg_username}")
|
||||
|
||||
|
||||
@command_handler(needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_new about_>",
|
||||
help_text="Change your Telegram about section.")
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_new about_>",
|
||||
help_text="Change your Telegram about section.",
|
||||
)
|
||||
async def about(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp about <new about>`")
|
||||
@@ -72,17 +89,21 @@ async def about(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("About section updated")
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, help_section=SECTION_AUTH, help_args="<_new displayname_>",
|
||||
help_text="Change your Telegram displayname.")
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_new displayname_>",
|
||||
help_text="Change your Telegram displayname.",
|
||||
)
|
||||
async def displayname(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp displayname <new displayname>`")
|
||||
if evt.sender.is_bot:
|
||||
return await evt.reply("Bots can't set their own displayname.")
|
||||
|
||||
first_name, last_name = ((evt.args[0], "")
|
||||
if len(evt.args) == 1
|
||||
else (" ".join(evt.args[:-1]), evt.args[-1]))
|
||||
first_name, last_name = (
|
||||
(evt.args[0], "") if len(evt.args) == 1 else (" ".join(evt.args[:-1]), evt.args[-1])
|
||||
)
|
||||
try:
|
||||
await evt.sender.client(UpdateProfileRequest(first_name=first_name, last_name=last_name))
|
||||
except FirstNameInvalidError:
|
||||
@@ -92,16 +113,20 @@ async def displayname(evt: CommandEvent) -> EventID:
|
||||
|
||||
|
||||
def _format_session(sess: Authorization) -> str:
|
||||
return (f"**{sess.app_name} {sess.app_version}** \n"
|
||||
f" **Platform:** {sess.device_model} {sess.platform} {sess.system_version} \n"
|
||||
f" **Active:** {sess.date_active} (created {sess.date_created}) \n"
|
||||
f" **From:** {sess.ip} - {sess.region}, {sess.country}")
|
||||
return (
|
||||
f"**{sess.app_name} {sess.app_version}** \n"
|
||||
f" **Platform:** {sess.device_model} {sess.platform} {sess.system_version} \n"
|
||||
f" **Active:** {sess.date_active} (created {sess.date_created}) \n"
|
||||
f" **From:** {sess.ip} - {sess.region}, {sess.country}"
|
||||
)
|
||||
|
||||
|
||||
@command_handler(needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<`list`|`terminate`> [_hash_]",
|
||||
help_text="View or delete other Telegram sessions.")
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<`list`|`terminate`> [_hash_]",
|
||||
help_text="View or delete other Telegram sessions.",
|
||||
)
|
||||
async def session(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp session <list|terminate> [hash]`")
|
||||
@@ -113,14 +138,18 @@ async def session(evt: CommandEvent) -> EventID:
|
||||
session_list = res.authorizations
|
||||
current = [s for s in session_list if s.current][0]
|
||||
current_text = _format_session(current)
|
||||
other_text = "\n".join(f"* {_format_session(sess)} \n"
|
||||
f" **Hash:** {sess.hash}"
|
||||
for sess in session_list if not sess.current)
|
||||
return await evt.reply(f"### Current session\n"
|
||||
f"{current_text}\n"
|
||||
f"\n"
|
||||
f"### Other active sessions\n"
|
||||
f"{other_text}")
|
||||
other_text = "\n".join(
|
||||
f"* {_format_session(sess)} \n **Hash:** {sess.hash}"
|
||||
for sess in session_list
|
||||
if not sess.current
|
||||
)
|
||||
return await evt.reply(
|
||||
f"### Current session\n"
|
||||
f"{current_text}\n"
|
||||
f"\n"
|
||||
f"### Other active sessions\n"
|
||||
f"{other_text}"
|
||||
)
|
||||
elif cmd == "terminate" and len(evt.args) > 1:
|
||||
try:
|
||||
session_hash = int(evt.args[1])
|
||||
@@ -132,8 +161,9 @@ async def session(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("Invalid session hash.")
|
||||
except AuthKeyError as e:
|
||||
if e.message == "FRESH_RESET_AUTHORISATION_FORBIDDEN":
|
||||
return await evt.reply("New sessions can't terminate other sessions. "
|
||||
"Please wait a while.")
|
||||
return await evt.reply(
|
||||
"New sessions can't terminate other sessions. Please wait a while."
|
||||
)
|
||||
raise
|
||||
if ok:
|
||||
return await evt.reply("Session terminated successfully.")
|
||||
|
||||
@@ -20,33 +20,49 @@ import asyncio
|
||||
import io
|
||||
|
||||
from telethon.errors import (
|
||||
AccessTokenExpiredError, AccessTokenInvalidError, FirstNameInvalidError, FloodWaitError,
|
||||
PasswordHashInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError,
|
||||
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
||||
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError,
|
||||
PhoneNumberInvalidError)
|
||||
AccessTokenExpiredError,
|
||||
AccessTokenInvalidError,
|
||||
FirstNameInvalidError,
|
||||
FloodWaitError,
|
||||
PasswordHashInvalidError,
|
||||
PhoneCodeExpiredError,
|
||||
PhoneCodeInvalidError,
|
||||
PhoneNumberAppSignupForbiddenError,
|
||||
PhoneNumberBannedError,
|
||||
PhoneNumberFloodError,
|
||||
PhoneNumberInvalidError,
|
||||
PhoneNumberOccupiedError,
|
||||
PhoneNumberUnoccupiedError,
|
||||
SessionPasswordNeededError,
|
||||
)
|
||||
from telethon.tl.types import User
|
||||
|
||||
from mautrix.types import (EventID, UserID, MediaMessageEventContent, ImageInfo, MessageType,
|
||||
TextMessageEventContent)
|
||||
from mautrix.types import (
|
||||
EventID,
|
||||
ImageInfo,
|
||||
MediaMessageEventContent,
|
||||
MessageType,
|
||||
TextMessageEventContent,
|
||||
UserID,
|
||||
)
|
||||
from mautrix.util.format_duration import format_duration as fmt_duration
|
||||
|
||||
from ... import user as u
|
||||
from ...commands import SECTION_AUTH, CommandEvent, command_handler
|
||||
from ...types import TelegramID
|
||||
from ...commands import command_handler, CommandEvent, SECTION_AUTH
|
||||
|
||||
try:
|
||||
import qrcode
|
||||
import PIL as _
|
||||
from telethon.tl.custom import QRLogin
|
||||
import PIL as _
|
||||
import qrcode
|
||||
except ImportError:
|
||||
qrcode = None
|
||||
QRLogin = None
|
||||
|
||||
|
||||
@command_handler(needs_auth=False,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Check if you're logged into Telegram.")
|
||||
@command_handler(
|
||||
needs_auth=False, help_section=SECTION_AUTH, help_text="Check if you're logged into Telegram."
|
||||
)
|
||||
async def ping(evt: CommandEvent) -> EventID:
|
||||
if await evt.sender.is_logged_in():
|
||||
me = await evt.sender.get_me()
|
||||
@@ -59,22 +75,30 @@ async def ping(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("You're not logged in.")
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Get the info of the message relay Telegram bot.")
|
||||
@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) -> EventID:
|
||||
if not evt.tgbot:
|
||||
return await evt.reply("Telegram message relay bot not configured.")
|
||||
info, mxid = await evt.tgbot.get_me(use_cache=False)
|
||||
return await evt.reply("Telegram message relay bot is active: "
|
||||
f"[{info.first_name}](https://matrix.to/#/{mxid}) (ID {info.id})\n\n"
|
||||
"To use the bot, simply invite it to a portal room.")
|
||||
return await evt.reply(
|
||||
"Telegram message relay bot is active: "
|
||||
f"[{info.first_name}](https://matrix.to/#/{mxid}) (ID {info.id})\n\n"
|
||||
"To use the bot, simply invite it to a portal room."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, management_only=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_args="<_phone_> <_full name_>",
|
||||
help_text="Register to Telegram")
|
||||
@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) -> EventID:
|
||||
if await evt.sender.is_logged_in():
|
||||
return await evt.reply("You are already logged in.")
|
||||
@@ -87,13 +111,19 @@ async def register(evt: CommandEvent) -> EventID:
|
||||
else:
|
||||
full_name = " ".join(evt.args[1:-1]), evt.args[-1]
|
||||
|
||||
await _request_code(evt, phone_number, {
|
||||
"next": enter_code_register,
|
||||
"action": "Register",
|
||||
"full_name": full_name,
|
||||
})
|
||||
return await evt.reply("By signing up for Telegram, you agree to "
|
||||
"the terms of service: https://telegram.org/tos")
|
||||
await _request_code(
|
||||
evt,
|
||||
phone_number,
|
||||
{
|
||||
"next": enter_code_register,
|
||||
"action": "Register",
|
||||
"full_name": full_name,
|
||||
},
|
||||
)
|
||||
return await evt.reply(
|
||||
"By signing up for Telegram, you agree to "
|
||||
"the terms of service: https://telegram.org/tos"
|
||||
)
|
||||
|
||||
|
||||
async def enter_code_register(evt: CommandEvent) -> EventID:
|
||||
@@ -107,23 +137,31 @@ async def enter_code_register(evt: CommandEvent) -> EventID:
|
||||
evt.sender.command_status = None
|
||||
return await evt.reply(f"Successfully registered to Telegram.")
|
||||
except PhoneNumberOccupiedError:
|
||||
return await evt.reply("That phone number has already been registered. "
|
||||
"You can log in with `$cmdprefix+sp login`.")
|
||||
return await evt.reply(
|
||||
"That phone number has already been registered. "
|
||||
"You can log in with `$cmdprefix+sp login`."
|
||||
)
|
||||
except FirstNameInvalidError:
|
||||
return await evt.reply("Invalid name. Please set a Matrix displayname before registering.")
|
||||
except PhoneCodeExpiredError:
|
||||
return await evt.reply(
|
||||
"Phone code expired. Try again with `$cmdprefix+sp register <phone>`.")
|
||||
"Phone code expired. Try again with `$cmdprefix+sp register <phone>`."
|
||||
)
|
||||
except PhoneCodeInvalidError:
|
||||
return await evt.reply("Invalid phone code.")
|
||||
except Exception:
|
||||
evt.log.exception("Error sending phone code")
|
||||
return await evt.reply("Unhandled exception while sending code. "
|
||||
"Check console for more details.")
|
||||
return await evt.reply(
|
||||
"Unhandled exception while sending code. Check console for more details."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
||||
help_text="Log in by scanning a QR code.")
|
||||
@command_handler(
|
||||
needs_auth=False,
|
||||
management_only=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Log in by scanning a QR code.",
|
||||
)
|
||||
async def login_qr(evt: CommandEvent) -> EventID:
|
||||
login_as = evt.sender
|
||||
if len(evt.args) > 0 and evt.sender.is_admin:
|
||||
@@ -145,9 +183,12 @@ async def login_qr(evt: CommandEvent) -> EventID:
|
||||
image.save(buffer, "PNG")
|
||||
qr = buffer.getvalue()
|
||||
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
|
||||
content = MediaMessageEventContent(body=qr_login.url, url=mxc, msgtype=MessageType.IMAGE,
|
||||
info=ImageInfo(mimetype="image/png", size=len(qr),
|
||||
width=size, height=size))
|
||||
content = MediaMessageEventContent(
|
||||
body=qr_login.url,
|
||||
url=mxc,
|
||||
msgtype=MessageType.IMAGE,
|
||||
info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size),
|
||||
)
|
||||
if qr_event_id:
|
||||
content.set_edit(qr_event_id)
|
||||
await evt.az.intent.send_message(evt.room_id, content)
|
||||
@@ -170,8 +211,9 @@ async def login_qr(evt: CommandEvent) -> EventID:
|
||||
"login_as": login_as if login_as != evt.sender else None,
|
||||
"action": "Login (password entry)",
|
||||
}
|
||||
return await evt.reply("Your account has two-factor authentication. "
|
||||
"Please send your password here.")
|
||||
return await evt.reply(
|
||||
"Your account has two-factor authentication. Please send your password here."
|
||||
)
|
||||
else:
|
||||
timeout = TextMessageEventContent(body="Login timed out", msgtype=MessageType.TEXT)
|
||||
timeout.set_edit(qr_event_id)
|
||||
@@ -180,9 +222,12 @@ async def login_qr(evt: CommandEvent) -> EventID:
|
||||
return await _finish_sign_in(evt, user, login_as=login_as)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, management_only=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Get instructions on how to log in.")
|
||||
@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) -> EventID:
|
||||
override_sender = False
|
||||
if len(evt.args) > 0 and evt.sender.is_admin:
|
||||
@@ -203,24 +248,32 @@ async def login(evt: CommandEvent) -> EventID:
|
||||
prefix = evt.config["appservice.public.external"]
|
||||
url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid, '/login')}"
|
||||
if override_sender:
|
||||
return await evt.reply(f"[Click here to log in]({url}) as "
|
||||
f"[{evt.sender.mxid}](https://matrix.to/#/{evt.sender.mxid}).")
|
||||
return await evt.reply(
|
||||
f"[Click here to log in]({url}) as "
|
||||
f"[{evt.sender.mxid}](https://matrix.to/#/{evt.sender.mxid})."
|
||||
)
|
||||
elif allow_matrix_login:
|
||||
return await evt.reply(f"[Click here to log in]({url}). Alternatively, send your phone"
|
||||
f" number (or bot auth token) here to log in.\n\n{nb}")
|
||||
return await evt.reply(
|
||||
f"[Click here to log in]({url}). Alternatively, send your phone"
|
||||
f" number (or bot auth token) here to log in.\n\n{nb}"
|
||||
)
|
||||
return await evt.reply(f"[Click here to log in]({url}).\n\n{nb}")
|
||||
elif allow_matrix_login:
|
||||
if override_sender:
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow you to log in outside of Matrix. "
|
||||
"Logging in as another user inside Matrix is not currently possible.")
|
||||
return await evt.reply("Please send your phone number (or bot auth token) here to start "
|
||||
f"the login process.\n\n{nb}")
|
||||
"Logging in as another user inside Matrix is not currently possible."
|
||||
)
|
||||
return await evt.reply(
|
||||
"Please send your phone number (or bot auth token) here to start "
|
||||
f"the login process.\n\n{nb}"
|
||||
)
|
||||
return await evt.reply("This bridge instance has been configured to not allow logging in.")
|
||||
|
||||
|
||||
async def _request_code(evt: CommandEvent, phone_number: str, next_status: dict[str, Any]
|
||||
) -> EventID:
|
||||
async def _request_code(
|
||||
evt: CommandEvent, phone_number: str, next_status: dict[str, Any]
|
||||
) -> EventID:
|
||||
ok = False
|
||||
try:
|
||||
await evt.sender.ensure_started(even_if_no_session=True)
|
||||
@@ -230,22 +283,29 @@ async def _request_code(evt: CommandEvent, phone_number: str, next_status: dict[
|
||||
except PhoneNumberAppSignupForbiddenError:
|
||||
return await evt.reply("Your phone number does not allow 3rd party apps to sign in.")
|
||||
except PhoneNumberFloodError:
|
||||
return await evt.reply("Your phone number has been temporarily blocked for flooding. "
|
||||
"The ban is usually applied for around a day.")
|
||||
return await evt.reply(
|
||||
"Your phone number has been temporarily blocked for flooding. "
|
||||
"The ban is usually applied for around a day."
|
||||
)
|
||||
except FloodWaitError as e:
|
||||
return await evt.reply("Your phone number has been temporarily blocked for flooding. "
|
||||
f"Please wait for {fmt_duration(e.seconds)} before trying again.")
|
||||
return await evt.reply(
|
||||
"Your phone number has been temporarily blocked for flooding. "
|
||||
f"Please wait for {fmt_duration(e.seconds)} before trying again."
|
||||
)
|
||||
except PhoneNumberBannedError:
|
||||
return await evt.reply("Your phone number has been banned from Telegram.")
|
||||
except PhoneNumberUnoccupiedError:
|
||||
return await evt.reply("That phone number has not been registered. "
|
||||
"Please register with `$cmdprefix+sp register <phone>`.")
|
||||
return await evt.reply(
|
||||
"That phone number has not been registered. "
|
||||
"Please register with `$cmdprefix+sp register <phone>`."
|
||||
)
|
||||
except PhoneNumberInvalidError:
|
||||
return await evt.reply("That phone number is not valid.")
|
||||
except Exception:
|
||||
evt.log.exception("Error requesting phone code")
|
||||
return await evt.reply("Unhandled exception while requesting code. "
|
||||
"Check console for more details.")
|
||||
return await evt.reply(
|
||||
"Unhandled exception while requesting code. Check console for more details."
|
||||
)
|
||||
finally:
|
||||
evt.sender.command_status = next_status if ok else None
|
||||
|
||||
@@ -255,8 +315,10 @@ async def enter_phone_or_token(evt: CommandEvent) -> EventID | None:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp enter-phone-or-token <phone-or-token>`")
|
||||
elif not evt.config.get("bridge.allow_matrix_login", True):
|
||||
return await evt.reply("This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions")
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions"
|
||||
)
|
||||
|
||||
# phone numbers don't contain colons but telegram bot auth tokens do
|
||||
if evt.args[0].find(":") > 0:
|
||||
@@ -264,13 +326,11 @@ async def enter_phone_or_token(evt: CommandEvent) -> EventID | None:
|
||||
await _sign_in(evt, bot_token=evt.args[0])
|
||||
except Exception:
|
||||
evt.log.exception("Error sending auth token")
|
||||
return await evt.reply("Unhandled exception while sending auth token. "
|
||||
"Check console for more details.")
|
||||
return await evt.reply(
|
||||
"Unhandled exception while sending auth token. Check console for more details."
|
||||
)
|
||||
else:
|
||||
await _request_code(evt, evt.args[0], {
|
||||
"next": enter_code,
|
||||
"action": "Login",
|
||||
})
|
||||
await _request_code(evt, evt.args[0], {"next": enter_code, "action": "Login"})
|
||||
return None
|
||||
|
||||
|
||||
@@ -279,14 +339,17 @@ async def enter_code(evt: CommandEvent) -> EventID | None:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp enter-code <code>`")
|
||||
elif not evt.config.get("bridge.allow_matrix_login", True):
|
||||
return await evt.reply("This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions")
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions"
|
||||
)
|
||||
try:
|
||||
await _sign_in(evt, code=evt.args[0])
|
||||
except Exception:
|
||||
evt.log.exception("Error sending phone code")
|
||||
return await evt.reply("Unhandled exception while sending code. "
|
||||
"Check console for more details.")
|
||||
return await evt.reply(
|
||||
"Unhandled exception while sending code. Check console for more details."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -295,19 +358,25 @@ async def enter_password(evt: CommandEvent) -> EventID | None:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp enter-password <password>`")
|
||||
elif not evt.config.get("bridge.allow_matrix_login", True):
|
||||
return await evt.reply("This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions")
|
||||
return await evt.reply(
|
||||
"This bridge instance does not allow in-Matrix login. "
|
||||
"Please use `$cmdprefix+sp login` to get login instructions"
|
||||
)
|
||||
try:
|
||||
await _sign_in(evt, login_as=evt.sender.command_status.get("login_as", None),
|
||||
password=" ".join(evt.args))
|
||||
await _sign_in(
|
||||
evt,
|
||||
login_as=evt.sender.command_status.get("login_as", None),
|
||||
password=" ".join(evt.args),
|
||||
)
|
||||
except AccessTokenInvalidError:
|
||||
return await evt.reply("That bot token is not valid.")
|
||||
except AccessTokenExpiredError:
|
||||
return await evt.reply("That bot token has expired.")
|
||||
except Exception:
|
||||
evt.log.exception("Error sending password")
|
||||
return await evt.reply("Unhandled exception while sending password. "
|
||||
"Check console for more details.")
|
||||
return await evt.reply(
|
||||
"Unhandled exception while sending password. Check console for more details."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -328,8 +397,9 @@ async def _sign_in(evt: CommandEvent, login_as: u.User = None, **sign_in_info) -
|
||||
"next": enter_password,
|
||||
"action": "Login (password entry)",
|
||||
}
|
||||
return await evt.reply("Your account has two-factor authentication. "
|
||||
"Please send your password here.")
|
||||
return await evt.reply(
|
||||
"Your account has two-factor authentication. Please send your password here."
|
||||
)
|
||||
|
||||
|
||||
async def _finish_sign_in(evt: CommandEvent, user: User, login_as: u.User = None) -> EventID:
|
||||
@@ -337,23 +407,24 @@ async def _finish_sign_in(evt: CommandEvent, user: User, login_as: u.User = None
|
||||
existing_user = await u.User.get_by_tgid(TelegramID(user.id))
|
||||
if existing_user and existing_user != login_as:
|
||||
await existing_user.log_out()
|
||||
await evt.reply(f"[{existing_user.displayname}]"
|
||||
f"(https://matrix.to/#/{existing_user.mxid})"
|
||||
" was logged out from the account.")
|
||||
await evt.reply(
|
||||
f"[{existing_user.displayname}] (https://matrix.to/#/{existing_user.mxid})"
|
||||
" was logged out from the account."
|
||||
)
|
||||
asyncio.ensure_future(login_as.post_login(user, first_login=True), loop=evt.loop)
|
||||
evt.sender.command_status = None
|
||||
name = f"@{user.username}" if user.username else f"+{user.phone}"
|
||||
if login_as != evt.sender:
|
||||
msg = (f"Successfully logged in [{login_as.mxid}](https://matrix.to/#/{login_as.mxid})"
|
||||
f" as {name}")
|
||||
msg = (
|
||||
f"Successfully logged in [{login_as.mxid}](https://matrix.to/#/{login_as.mxid})"
|
||||
f" as {name}"
|
||||
)
|
||||
else:
|
||||
msg = f"Successfully logged in as {name}"
|
||||
return await evt.reply(msg)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Log out from Telegram.")
|
||||
@command_handler(needs_auth=False, help_section=SECTION_AUTH, help_text="Log out from Telegram.")
|
||||
async def logout(evt: CommandEvent) -> EventID:
|
||||
if not evt.sender.tgid:
|
||||
return await evt.reply("You're not logged in")
|
||||
|
||||
@@ -16,36 +16,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
import codecs
|
||||
import base64
|
||||
import codecs
|
||||
import re
|
||||
|
||||
from aiohttp import ClientSession, InvalidURL
|
||||
|
||||
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
|
||||
UserAlreadyParticipantError, ChatIdInvalidError,
|
||||
TakeoutInitDelayError, EmoticonInvalidError)
|
||||
from telethon.tl.patched import Message
|
||||
from telethon.tl.types import (User as TLUser, TypeUpdates, MessageMediaGame, MessageMediaPoll,
|
||||
TypeInputPeer, InputMediaDice)
|
||||
from telethon.tl.types.messages import BotCallbackAnswer
|
||||
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
|
||||
GetBotCallbackAnswerRequest, SendVoteRequest)
|
||||
from telethon.errors import (
|
||||
ChatIdInvalidError,
|
||||
EmoticonInvalidError,
|
||||
InviteHashExpiredError,
|
||||
InviteHashInvalidError,
|
||||
OptionsTooMuchError,
|
||||
TakeoutInitDelayError,
|
||||
UserAlreadyParticipantError,
|
||||
)
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
from telethon.tl.functions.messages import (
|
||||
CheckChatInviteRequest,
|
||||
GetBotCallbackAnswerRequest,
|
||||
ImportChatInviteRequest,
|
||||
SendVoteRequest,
|
||||
)
|
||||
from telethon.tl.patched import Message
|
||||
from telethon.tl.types import (
|
||||
InputMediaDice,
|
||||
MessageMediaGame,
|
||||
MessageMediaPoll,
|
||||
TypeInputPeer,
|
||||
TypeUpdates,
|
||||
User as TLUser,
|
||||
)
|
||||
from telethon.tl.types.messages import BotCallbackAnswer
|
||||
|
||||
from mautrix.types import EventID, Format
|
||||
|
||||
from ... import puppet as pu, portal as po
|
||||
from ... import portal as po, puppet as pu
|
||||
from ...abstract_user import AbstractUser
|
||||
from ...commands import (
|
||||
SECTION_CREATING_PORTALS,
|
||||
SECTION_MISC,
|
||||
SECTION_PORTAL_MANAGEMENT,
|
||||
CommandEvent,
|
||||
command_handler,
|
||||
)
|
||||
from ...db import Message as DBMessage
|
||||
from ...types import TelegramID
|
||||
from ...commands import (command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS,
|
||||
SECTION_PORTAL_MANAGEMENT)
|
||||
|
||||
|
||||
@command_handler(needs_auth=False,
|
||||
help_section=SECTION_MISC, help_args="<_caption_>",
|
||||
help_text="Set a caption for the next image you send")
|
||||
@command_handler(
|
||||
needs_auth=False,
|
||||
help_section=SECTION_MISC,
|
||||
help_args="<_caption_>",
|
||||
help_text="Set a caption for the next image you send",
|
||||
)
|
||||
async def caption(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp caption <caption>`")
|
||||
@@ -55,13 +78,17 @@ async def caption(evt: CommandEvent) -> EventID:
|
||||
evt.content.formatted_body = evt.content.formatted_body.replace(prefix, "", 1)
|
||||
evt.content.body = evt.content.body.replace(prefix, "", 1)
|
||||
evt.sender.command_status = {"caption": evt.content, "action": "Caption"}
|
||||
return await evt.reply("Your next image or file will be sent with that caption. "
|
||||
"Use `$cmdprefix+sp cancel` to cancel the caption.")
|
||||
return await evt.reply(
|
||||
"Your next image or file will be sent with that caption. "
|
||||
"Use `$cmdprefix+sp cancel` to cancel the caption."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="[_-r|--remote_] <_query_>",
|
||||
help_text="Search your contacts or the Telegram servers for users.")
|
||||
@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) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] <query>`")
|
||||
@@ -79,8 +106,9 @@ async def search(evt: CommandEvent) -> EventID:
|
||||
|
||||
if not results:
|
||||
if len(query) < 5 and remote:
|
||||
return await evt.reply("No local results. "
|
||||
"Minimum length of remote query is 5 characters.")
|
||||
return await evt.reply(
|
||||
"No local results. Minimum length of remote query is 5 characters."
|
||||
)
|
||||
return await evt.reply("No results 3:")
|
||||
|
||||
reply: list[str] = []
|
||||
@@ -88,20 +116,27 @@ async def search(evt: CommandEvent) -> EventID:
|
||||
reply += ["**Results from Telegram server:**", ""]
|
||||
else:
|
||||
reply += ["**Results in contacts:**", ""]
|
||||
reply += [(f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): "
|
||||
f"{puppet.id} ({similarity}% match)")
|
||||
for puppet, similarity in results]
|
||||
reply += [
|
||||
(
|
||||
f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): "
|
||||
f"{puppet.id} ({similarity}% match)"
|
||||
)
|
||||
for puppet, similarity in results
|
||||
]
|
||||
|
||||
# TODO somehow show remote channel results when joining by alias is possible?
|
||||
|
||||
return await evt.reply("\n".join(reply))
|
||||
|
||||
|
||||
@command_handler(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.")
|
||||
@command_handler(
|
||||
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 pm(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp pm <user identifier>`")
|
||||
@@ -122,8 +157,9 @@ async def pm(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply(f"Created private chat room with {displayname}")
|
||||
|
||||
|
||||
async def _join(evt: CommandEvent, identifier: str, link_type: str
|
||||
) -> tuple[TypeUpdates | None, EventID | None]:
|
||||
async def _join(
|
||||
evt: CommandEvent, identifier: str, link_type: str
|
||||
) -> tuple[TypeUpdates | None, EventID | None]:
|
||||
if link_type == "joinchat":
|
||||
try:
|
||||
await evt.sender.client(CheckChatInviteRequest(identifier))
|
||||
@@ -142,9 +178,11 @@ async def _join(evt: CommandEvent, identifier: str, link_type: str
|
||||
return await evt.sender.client(JoinChannelRequest(channel)), None
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_CREATING_PORTALS,
|
||||
help_args="<_link_>",
|
||||
help_text="Join a chat with an invite link.")
|
||||
@command_handler(
|
||||
help_section=SECTION_CREATING_PORTALS,
|
||||
help_args="<_link_>",
|
||||
help_text="Join a chat with an invite link.",
|
||||
)
|
||||
async def join(evt: CommandEvent) -> EventID | None:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp join <invite link>`")
|
||||
@@ -157,8 +195,10 @@ async def join(evt: CommandEvent) -> EventID | None:
|
||||
except InvalidURL:
|
||||
return await evt.reply("That doesn't look like a Telegram invite link.")
|
||||
|
||||
regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)"
|
||||
r"(?:/(?P<type>joinchat|s))?/(?P<id>[^/]+)/?", flags=re.IGNORECASE)
|
||||
regex = re.compile(
|
||||
r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:/(?P<type>joinchat|s))?/(?P<id>[^/]+)/?",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
arg = regex.match(url)
|
||||
if not arg:
|
||||
return await evt.reply("That doesn't look like a Telegram invite link.")
|
||||
@@ -182,16 +222,20 @@ async def join(evt: CommandEvent) -> EventID | None:
|
||||
try:
|
||||
await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
|
||||
except ChatIdInvalidError as e:
|
||||
evt.log.trace("ChatIdInvalidError while creating portal from !tg join command: %s",
|
||||
updates.stringify())
|
||||
evt.log.trace(
|
||||
"ChatIdInvalidError while creating portal from !tg join command: %s",
|
||||
updates.stringify(),
|
||||
)
|
||||
raise e
|
||||
return await evt.reply(f"Created room for {portal.title}")
|
||||
return None
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="[`chats`|`contacts`|`me`]",
|
||||
help_text="Synchronize your chat portals, contacts and/or own info.")
|
||||
@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) -> EventID:
|
||||
if len(evt.args) > 0:
|
||||
sync_only = evt.args[0]
|
||||
@@ -220,8 +264,9 @@ class MessageIDError(ValueError):
|
||||
self.message = message
|
||||
|
||||
|
||||
async def _parse_encoded_msgid(user: AbstractUser, enc_id: str, type_name: str
|
||||
) -> tuple[TypeInputPeer, Message]:
|
||||
async def _parse_encoded_msgid(
|
||||
user: AbstractUser, enc_id: str, type_name: str
|
||||
) -> tuple[TypeInputPeer, Message]:
|
||||
try:
|
||||
enc_id += (4 - len(enc_id) % 4) * "="
|
||||
enc_id = base64.b64decode(enc_id)
|
||||
@@ -253,9 +298,9 @@ async def _parse_encoded_msgid(user: AbstractUser, enc_id: str, type_name: str
|
||||
return peer, cast(Message, msg)
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="<_play ID_>",
|
||||
help_text="Play a Telegram game.")
|
||||
@command_handler(
|
||||
help_section=SECTION_MISC, help_args="<_play ID_>", help_text="Play a Telegram game."
|
||||
)
|
||||
async def play(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) < 1:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp play <play ID>`")
|
||||
@@ -273,17 +318,22 @@ async def play(evt: CommandEvent) -> EventID:
|
||||
return await evt.reply("Invalid play ID (message doesn't look like a game)")
|
||||
|
||||
game = await evt.sender.client(
|
||||
GetBotCallbackAnswerRequest(peer=peer, msg_id=msg.id, game=True))
|
||||
GetBotCallbackAnswerRequest(peer=peer, msg_id=msg.id, game=True)
|
||||
)
|
||||
if not isinstance(game, BotCallbackAnswer):
|
||||
return await evt.reply("Game request response invalid")
|
||||
|
||||
return await evt.reply(f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
|
||||
f"{msg.media.game.description}")
|
||||
return await evt.reply(
|
||||
f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
|
||||
f"{msg.media.game.description}"
|
||||
)
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="<_poll ID_> <_choice number_>",
|
||||
help_text="Vote in a Telegram poll.")
|
||||
@command_handler(
|
||||
help_section=SECTION_MISC,
|
||||
help_args="<_poll ID_> <_choice number_>",
|
||||
help_text="Vote in a Telegram poll.",
|
||||
)
|
||||
async def vote(evt: CommandEvent) -> EventID | None:
|
||||
if len(evt.args) < 1:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp vote <poll ID> <choice number>`")
|
||||
@@ -309,17 +359,20 @@ async def vote(evt: CommandEvent) -> EventID | None:
|
||||
except ValueError:
|
||||
option_index = None
|
||||
if option_index is None:
|
||||
return await evt.reply(f"Invalid option number \"{option}\"",
|
||||
render_markdown=False, allow_html=False)
|
||||
return await evt.reply(
|
||||
f'Invalid option number "{option}"', render_markdown=False, allow_html=False
|
||||
)
|
||||
elif option_index < 0:
|
||||
return await evt.reply(f"Invalid option number {option}. "
|
||||
f"Option numbers must be positive.")
|
||||
return await evt.reply(
|
||||
f"Invalid option number {option}. Option numbers must be positive."
|
||||
)
|
||||
elif option_index >= len(msg.media.poll.answers):
|
||||
return await evt.reply(f"Invalid option number {option}. "
|
||||
f"The poll only has {len(msg.media.poll.answers)} options.")
|
||||
return await evt.reply(
|
||||
f"Invalid option number {option}. "
|
||||
f"The poll only has {len(msg.media.poll.answers)} options."
|
||||
)
|
||||
options.append(msg.media.poll.answers[option_index].option)
|
||||
options = [msg.media.poll.answers[int(option) - 1].option
|
||||
for option in evt.args[1:]]
|
||||
options = [msg.media.poll.answers[int(option) - 1].option for option in evt.args[1:]]
|
||||
try:
|
||||
await evt.sender.client(SendVoteRequest(peer=peer, msg_id=msg.id, options=options))
|
||||
except OptionsTooMuchError:
|
||||
@@ -328,9 +381,12 @@ async def vote(evt: CommandEvent) -> EventID | None:
|
||||
return await evt.mark_read()
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC, help_args="<_emoji_>",
|
||||
help_text="Roll a dice (\U0001F3B2), kick a football (\u26BD\uFE0F) or throw a "
|
||||
"dart (\U0001F3AF) or basketball (\U0001F3C0) on the Telegram servers.")
|
||||
@command_handler(
|
||||
help_section=SECTION_MISC,
|
||||
help_args="<_emoji_>",
|
||||
help_text="Roll a dice (\U0001F3B2), kick a football (\u26BD\uFE0F) or throw a "
|
||||
"dart (\U0001F3AF) or basketball (\U0001F3C0) on the Telegram servers.",
|
||||
)
|
||||
async def random(evt: CommandEvent) -> EventID:
|
||||
if not evt.is_portal:
|
||||
return await evt.reply("You can only randomize values in portal rooms")
|
||||
@@ -345,14 +401,18 @@ async def random(evt: CommandEvent) -> EventID:
|
||||
"soccer": "\u26BD",
|
||||
}.get(arg, arg)
|
||||
try:
|
||||
await evt.sender.client.send_media(await portal.get_input_entity(evt.sender),
|
||||
InputMediaDice(emoticon))
|
||||
await evt.sender.client.send_media(
|
||||
await portal.get_input_entity(evt.sender), InputMediaDice(emoticon)
|
||||
)
|
||||
except EmoticonInvalidError:
|
||||
return await evt.reply("Invalid emoji for randomization")
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT, help_args="[_limit_]",
|
||||
help_text="Backfill messages from Telegram history.")
|
||||
@command_handler(
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_args="[_limit_]",
|
||||
help_text="Backfill messages from Telegram history.",
|
||||
)
|
||||
async def backfill(evt: CommandEvent) -> None:
|
||||
if not evt.is_portal:
|
||||
await evt.reply("You can only use backfill in portal rooms")
|
||||
@@ -368,10 +428,13 @@ async def backfill(evt: CommandEvent) -> None:
|
||||
try:
|
||||
await portal.backfill(evt.sender, limit=limit)
|
||||
except TakeoutInitDelayError:
|
||||
msg = ("Please accept the data export request from a mobile device, "
|
||||
"then re-run the backfill command.")
|
||||
msg = (
|
||||
"Please accept the data export request from a mobile device, "
|
||||
"then re-run the backfill command."
|
||||
)
|
||||
if portal.peer_type == "user":
|
||||
from mautrix.appservice import IntentAPI
|
||||
|
||||
await portal.main_intent.send_notice(evt.room_id, msg)
|
||||
else:
|
||||
await evt.reply(msg)
|
||||
|
||||
Reference in New Issue
Block a user