More file splitting and new admin commands
This commit is contained in:
+20
-23
@@ -107,12 +107,12 @@ bridge:
|
|||||||
# If no channel admins have logged into the bridge, the bridge won't be able to sync the member
|
# If no channel admins have logged into the bridge, the bridge won't be able to sync the member
|
||||||
# list regardless of this setting.
|
# list regardless of this setting.
|
||||||
sync_channel_members: true
|
sync_channel_members: true
|
||||||
# Whether or not to automatically sync the Matrix room state (mostly unpuppeted displaynames)
|
|
||||||
# at startup and when creating a bridge.
|
|
||||||
sync_matrix_state: true
|
|
||||||
# The maximum number of simultaneous Telegram deletions to handle.
|
# The maximum number of simultaneous Telegram deletions to handle.
|
||||||
# A large number of simultaneous redactions could put strain on your homeserver.
|
# A large number of simultaneous redactions could put strain on your homeserver.
|
||||||
max_telegram_delete: 10
|
max_telegram_delete: 10
|
||||||
|
# Whether or not to automatically sync the Matrix room state (mostly unpuppeted displaynames)
|
||||||
|
# at startup and when creating a bridge.
|
||||||
|
sync_matrix_state: true
|
||||||
# Allow logging in within Matrix. If false, the only way to log in is using the out-of-Matrix
|
# Allow logging in within Matrix. If false, the only way to log in is using the out-of-Matrix
|
||||||
# login website (see appservice.public config section)
|
# login website (see appservice.public config section)
|
||||||
allow_matrix_login: true
|
allow_matrix_login: true
|
||||||
@@ -120,6 +120,9 @@ bridge:
|
|||||||
# Only enable this if your displayname_template has some static part that the bridge can use to
|
# Only enable this if your displayname_template has some static part that the bridge can use to
|
||||||
# reliably identify what is a plaintext highlight.
|
# reliably identify what is a plaintext highlight.
|
||||||
plaintext_highlights: false
|
plaintext_highlights: false
|
||||||
|
# Show message editing as a reply to the original message.
|
||||||
|
# If this is false, message edits are not shown at all, as Matrix does not support editing yet.
|
||||||
|
edits_as_replies: true
|
||||||
# Highlight changed/added parts in edits. Requires lxml.
|
# Highlight changed/added parts in edits. Requires lxml.
|
||||||
highlight_edits: false
|
highlight_edits: false
|
||||||
# Whether or not to make portals of publicly joinable channels/supergroups publicly joinable on Matrix.
|
# Whether or not to make portals of publicly joinable channels/supergroups publicly joinable on Matrix.
|
||||||
@@ -132,6 +135,20 @@ bridge:
|
|||||||
sync_with_custom_puppets: true
|
sync_with_custom_puppets: true
|
||||||
# Set to false to disable link previews in messages sent to Telegram.
|
# Set to false to disable link previews in messages sent to Telegram.
|
||||||
telegram_link_preview: true
|
telegram_link_preview: true
|
||||||
|
# Use inline images instead of a separate message for the caption.
|
||||||
|
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
||||||
|
inline_images: false
|
||||||
|
|
||||||
|
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
||||||
|
bot_messages_as_notices: true
|
||||||
|
bridge_notices:
|
||||||
|
# Whether or not Matrix bot messages (type m.notice) should be bridged.
|
||||||
|
default: false
|
||||||
|
# List of user IDs for whom the previous flag is flipped.
|
||||||
|
# e.g. if bridge_notices.default is false, notices from other users will not be bridged, but
|
||||||
|
# notices from users listed here will be bridged.
|
||||||
|
exceptions:
|
||||||
|
- "@importantbot:example.com"
|
||||||
|
|
||||||
# Some config options related to Telegram message deduplication.
|
# Some config options related to Telegram message deduplication.
|
||||||
# The default values are usually fine, but some debug messages/warnings might recommend you
|
# The default values are usually fine, but some debug messages/warnings might recommend you
|
||||||
@@ -143,26 +160,6 @@ bridge:
|
|||||||
# You might need to increase this on high-traffic bridge instances.
|
# You might need to increase this on high-traffic bridge instances.
|
||||||
cache_queue_length: 20
|
cache_queue_length: 20
|
||||||
|
|
||||||
# Show message editing as a reply to the original message.
|
|
||||||
# If this is false, message edits are not shown at all, as Matrix does not support editing yet.
|
|
||||||
edits_as_replies: false
|
|
||||||
bridge_notices:
|
|
||||||
# Whether or not Matrix bot messages (type m.notice) should be bridged.
|
|
||||||
default: false
|
|
||||||
# List of user IDs for whom the previous flag is flipped.
|
|
||||||
# e.g. if bridge_notices.default is false, notices from other users will not be bridged, but
|
|
||||||
# notices from users listed here will be bridged.
|
|
||||||
exceptions:
|
|
||||||
- "@importantbot:example.com"
|
|
||||||
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
|
||||||
bot_messages_as_notices: true
|
|
||||||
# Use inline images instead of a separate message for the caption.
|
|
||||||
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
|
||||||
inline_images: false
|
|
||||||
# Whether to send stickers as the new native m.sticker type or normal m.images.
|
|
||||||
# Old versions of Riot don't support the new type at all.
|
|
||||||
# Remember that proper sticker support always requires Pillow to convert webp into png.
|
|
||||||
native_stickers: true
|
|
||||||
|
|
||||||
# The formats to use when sending messages to Telegram via the relay bot.
|
# The formats to use when sending messages to Telegram via the relay bot.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from .handler import (command_handler, command_handlers as _command_handlers,
|
|||||||
CommandHandler, CommandProcessor, CommandEvent,
|
CommandHandler, CommandProcessor, CommandEvent,
|
||||||
SECTION_GENERAL, SECTION_AUTH, SECTION_CREATING_PORTALS,
|
SECTION_GENERAL, SECTION_AUTH, SECTION_CREATING_PORTALS,
|
||||||
SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN)
|
SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN)
|
||||||
from . import clean_rooms, auth, meta, telegram, portal
|
from . import portal, telegram, clean_rooms, matrix_auth, meta
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from . import command_handler, CommandEvent, SECTION_AUTH
|
||||||
|
from .. import puppet as pu
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Optional[Dict]:
|
||||||
|
puppet = pu.Puppet.get(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")
|
||||||
|
async def login_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||||
|
puppet = pu.Puppet.get(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.")
|
||||||
|
allow_matrix_login = evt.config.get("bridge.allow_matrix_login", True)
|
||||||
|
if allow_matrix_login:
|
||||||
|
evt.sender.command_status = {
|
||||||
|
"next": enter_matrix_token,
|
||||||
|
"action": "Matrix login",
|
||||||
|
}
|
||||||
|
if evt.config["appservice.public.enabled"]:
|
||||||
|
prefix = evt.config["appservice.public.external"]
|
||||||
|
token = evt.public_website.make_token(evt.sender.mxid, "/matrix-login")
|
||||||
|
url = f"{prefix}/matrix-login?token={token}"
|
||||||
|
if allow_matrix_login:
|
||||||
|
return await evt.reply(
|
||||||
|
"This bridge instance allows you to log in inside or outside Matrix.\n\n"
|
||||||
|
"If you would like to log in within Matrix, please send your Matrix access token "
|
||||||
|
"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.")
|
||||||
|
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.")
|
||||||
|
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) -> Optional[Dict]:
|
||||||
|
puppet = pu.Puppet.get(evt.sender.tgid)
|
||||||
|
if not puppet.is_real_user:
|
||||||
|
return await evt.reply("You are not logged in with your Matrix account.")
|
||||||
|
resp = await puppet.init_custom_mxid()
|
||||||
|
if resp == pu.PuppetError.InvalidAccessToken:
|
||||||
|
return await evt.reply("Your access token is invalid.")
|
||||||
|
elif resp == pu.PuppetError.Success:
|
||||||
|
return await evt.reply("Your Matrix login is working.")
|
||||||
|
return await evt.reply(f"Unknown response while checking your Matrix login: {resp}.")
|
||||||
|
|
||||||
|
|
||||||
|
async def enter_matrix_token(evt: CommandEvent) -> Dict:
|
||||||
|
evt.sender.command_status = None
|
||||||
|
|
||||||
|
puppet = pu.Puppet.get(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.")
|
||||||
|
|
||||||
|
resp = await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
|
||||||
|
if resp == pu.PuppetError.OnlyLoginSelf:
|
||||||
|
return await evt.reply("You can only log in as your own Matrix user.")
|
||||||
|
elif resp == pu.PuppetError.InvalidAccessToken:
|
||||||
|
return await evt.reply("Failed to verify access token.")
|
||||||
|
assert resp == pu.PuppetError.Success, "Encountered an unhandled PuppetError."
|
||||||
|
return await evt.reply(
|
||||||
|
f"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}.")
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
# -*- coding: future_fstrings -*-
|
|
||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
|
||||||
# Copyright (C) 2018 Tulir Asokan
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
from typing import Dict, Callable, Optional, Tuple, Coroutine, Awaitable
|
|
||||||
from io import StringIO
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
|
|
||||||
UsernameNotModifiedError, UsernameOccupiedError)
|
|
||||||
from telethon.tl.types import ChatForbidden, ChannelForbidden
|
|
||||||
from mautrix_appservice import MatrixRequestError, IntentAPI
|
|
||||||
|
|
||||||
from ..types import MatrixRoomID, TelegramID
|
|
||||||
from ..config import yaml
|
|
||||||
from ..util import ignore_coro
|
|
||||||
from .. import portal as po, user as u, util
|
|
||||||
from . import (command_handler, CommandEvent,
|
|
||||||
SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT, SECTION_MISC)
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_admin=True, needs_auth=False, name="set-pl",
|
|
||||||
help_section=SECTION_ADMIN,
|
|
||||||
help_args="<_level_> [_mxid_]",
|
|
||||||
help_text="Set a temporary power level without affecting Telegram.")
|
|
||||||
async def set_power_level(evt: CommandEvent) -> Dict:
|
|
||||||
try:
|
|
||||||
level = int(evt.args[0])
|
|
||||||
except KeyError:
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp set-power <level> [mxid]`")
|
|
||||||
except ValueError:
|
|
||||||
return await evt.reply("The level must be an integer.")
|
|
||||||
levels = await evt.az.intent.get_power_levels(evt.room_id)
|
|
||||||
mxid = evt.args[1] if len(evt.args) > 1 else evt.sender.mxid
|
|
||||||
levels["users"][mxid] = level
|
|
||||||
try:
|
|
||||||
await evt.az.intent.set_power_levels(evt.room_id, levels)
|
|
||||||
except MatrixRequestError:
|
|
||||||
evt.log.exception("Failed to set power level.")
|
|
||||||
return await evt.reply("Failed to set power level.")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@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) -> Dict:
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
return await evt.reply("This is not a portal room.")
|
|
||||||
elif not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
|
|
||||||
return await evt.reply(f"You do not have the permissions to synchronize this room.")
|
|
||||||
|
|
||||||
await portal.sync_matrix_members()
|
|
||||||
await evt.reply("Synchronization complete")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(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 id(evt: CommandEvent) -> Dict:
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
return await evt.reply("This is not a portal room.")
|
|
||||||
tgid = portal.tgid
|
|
||||||
if portal.peer_type == "chat":
|
|
||||||
tgid = -tgid
|
|
||||||
elif portal.peer_type == "channel":
|
|
||||||
tgid = f"-100{tgid}"
|
|
||||||
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
|
||||||
help_text="Get a Telegram invite link to the current chat.")
|
|
||||||
async def invite_link(evt: CommandEvent) -> Dict:
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
return await evt.reply("This is not a portal room.")
|
|
||||||
|
|
||||||
if portal.peer_type == "user":
|
|
||||||
return await evt.reply("You can't invite users to private chats.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
link = await portal.get_invite_link(evt.sender)
|
|
||||||
return await evt.reply(f"Invite link to {portal.title}: {link}")
|
|
||||||
except ValueError as e:
|
|
||||||
return await evt.reply(e.args[0])
|
|
||||||
except ChatAdminRequiredError:
|
|
||||||
return await evt.reply("You don't have the permission to create an invite link.")
|
|
||||||
|
|
||||||
|
|
||||||
async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50
|
|
||||||
) -> bool:
|
|
||||||
if sender.is_admin:
|
|
||||||
return True
|
|
||||||
# Make sure the state store contains the power levels.
|
|
||||||
try:
|
|
||||||
await intent.get_power_levels(room)
|
|
||||||
except MatrixRequestError:
|
|
||||||
return False
|
|
||||||
return intent.state_store.has_power_level(room, sender.mxid,
|
|
||||||
event=f"net.maunium.telegram.{event}",
|
|
||||||
default=default)
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
|
||||||
action: Optional[str] = None
|
|
||||||
) -> Optional[po.Portal]:
|
|
||||||
room_id = MatrixRoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
|
||||||
|
|
||||||
portal = po.Portal.get_by_mxid(room_id)
|
|
||||||
if not portal:
|
|
||||||
that_this = "This" if room_id == evt.room_id else "That"
|
|
||||||
await evt.reply(f"{that_this} is not a portal room.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, permission):
|
|
||||||
action = action or f"{permission.replace('_', ' ')}s"
|
|
||||||
await evt.reply(f"You do not have the permissions to {action} that portal.")
|
|
||||||
return None
|
|
||||||
return portal
|
|
||||||
|
|
||||||
|
|
||||||
def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str,
|
|
||||||
completed_message: str) -> Dict:
|
|
||||||
async def post_confirm(confirm) -> Optional[Dict]:
|
|
||||||
confirm.sender.command_status = None
|
|
||||||
if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}":
|
|
||||||
await function()
|
|
||||||
if confirm.room_id != room_id:
|
|
||||||
return await confirm.reply(completed_message)
|
|
||||||
else:
|
|
||||||
return await confirm.reply(f"{action} cancelled.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"next": post_confirm,
|
|
||||||
"action": action,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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) -> Optional[Dict]:
|
|
||||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
|
||||||
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.")
|
|
||||||
|
|
||||||
|
|
||||||
@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) -> Optional[Dict]:
|
|
||||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
|
||||||
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`")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
|
||||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
|
||||||
help_args="[_id_]",
|
|
||||||
help_text="Bridge the current Matrix room to the Telegram chat with the given "
|
|
||||||
"ID. The ID must be the prefixed version that you get with the `/id` "
|
|
||||||
"command of the Telegram-side bot.")
|
|
||||||
async def bridge(evt: CommandEvent) -> Dict:
|
|
||||||
if len(evt.args) == 0:
|
|
||||||
return await evt.reply("**Usage:** "
|
|
||||||
"`$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`")
|
|
||||||
room_id = MatrixRoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
|
|
||||||
that_this = "This" if room_id == evt.room_id else "That"
|
|
||||||
|
|
||||||
portal = po.Portal.get_by_mxid(room_id)
|
|
||||||
if portal:
|
|
||||||
return await evt.reply(f"{that_this} room is already a portal room.")
|
|
||||||
|
|
||||||
if not await user_has_power_level(room_id, evt.az.intent, evt.sender, "bridge"):
|
|
||||||
return await evt.reply(f"You do not have the permissions to bridge {that_this} room.")
|
|
||||||
|
|
||||||
# The /id bot command provides the prefixed ID, so we assume
|
|
||||||
tgid_str = evt.args[0]
|
|
||||||
if tgid_str.startswith("-100"):
|
|
||||||
tgid = TelegramID(int(tgid_str[4:]))
|
|
||||||
peer_type = "channel"
|
|
||||||
elif tgid_str.startswith("-"):
|
|
||||||
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.")
|
|
||||||
|
|
||||||
portal = po.Portal.get_by_tgid(tgid, peer_type=peer_type)
|
|
||||||
if not portal.allow_bridging():
|
|
||||||
return await evt.reply("This bridge doesn't allow bridging that Telegram chat.\n"
|
|
||||||
"If you're the bridge admin, try "
|
|
||||||
"`$cmdprefix+sp 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}). ")
|
|
||||||
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.")
|
|
||||||
evt.sender.command_status = {
|
|
||||||
"next": confirm_bridge,
|
|
||||||
"action": "Room bridging",
|
|
||||||
"mxid": portal.mxid,
|
|
||||||
"bridge_to_mxid": room_id,
|
|
||||||
"tgid": portal.tgid,
|
|
||||||
"peer_type": portal.peer_type,
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
"bridge_to_mxid": room_id,
|
|
||||||
"tgid": portal.tgid,
|
|
||||||
"peer_type": portal.peer_type,
|
|
||||||
}
|
|
||||||
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, Optional[Coroutine[None, 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...")
|
|
||||||
return True, None
|
|
||||||
elif evt.args[0] == "delete-and-continue":
|
|
||||||
return True, portal.cleanup_room(portal.main_intent, portal.mxid,
|
|
||||||
message="Portal deleted (moving to another room)")
|
|
||||||
elif evt.args[0] == "unbridge-and-continue":
|
|
||||||
return True, portal.cleanup_room(portal.main_intent, portal.mxid,
|
|
||||||
message="Room unbridged (portal moving to another room)",
|
|
||||||
puppets_only=True)
|
|
||||||
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.")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
|
|
||||||
async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]:
|
|
||||||
status = evt.sender.command_status
|
|
||||||
try:
|
|
||||||
portal = po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"])
|
|
||||||
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.")
|
|
||||||
if "mxid" in status:
|
|
||||||
ok, coro = await cleanup_old_portal_while_bridging(evt, portal)
|
|
||||||
if not ok:
|
|
||||||
return None
|
|
||||||
elif coro:
|
|
||||||
ignore_coro(asyncio.ensure_future(coro, loop=evt.loop))
|
|
||||||
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.")
|
|
||||||
elif evt.args[0] != "continue":
|
|
||||||
return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or "
|
|
||||||
"`$cmdprefix+sp cancel` to cancel.")
|
|
||||||
|
|
||||||
evt.sender.command_status = None
|
|
||||||
is_logged_in = await evt.sender.is_logged_in()
|
|
||||||
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?")
|
|
||||||
else:
|
|
||||||
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.")
|
|
||||||
else:
|
|
||||||
return await evt.reply("The bot doesn't seem to be in that chat.")
|
|
||||||
|
|
||||||
direct = False
|
|
||||||
|
|
||||||
portal.mxid = bridge_to_mxid
|
|
||||||
portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id)
|
|
||||||
portal.photo_id = ""
|
|
||||||
portal.save()
|
|
||||||
|
|
||||||
ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct,
|
|
||||||
levels=levels),
|
|
||||||
loop=evt.loop))
|
|
||||||
|
|
||||||
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_initial_state(intent: IntentAPI, room_id: str) -> Tuple[str, str, Dict]:
|
|
||||||
state = await intent.get_room_state(room_id)
|
|
||||||
title = None
|
|
||||||
about = None
|
|
||||||
levels = None
|
|
||||||
for event in state:
|
|
||||||
try:
|
|
||||||
if event["type"] == "m.room.name":
|
|
||||||
title = event["content"]["name"]
|
|
||||||
elif event["type"] == "m.room.topic":
|
|
||||||
about = event["content"]["topic"]
|
|
||||||
elif event["type"] == "m.room.power_levels":
|
|
||||||
levels = event["content"]
|
|
||||||
elif event["type"] == "m.room.canonical_alias":
|
|
||||||
title = title or event["content"]["alias"]
|
|
||||||
except KeyError:
|
|
||||||
# Some state event probably has empty content
|
|
||||||
pass
|
|
||||||
return title, about, levels
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_CREATING_PORTALS,
|
|
||||||
help_args="[_type_]",
|
|
||||||
help_text="Create a Telegram chat of the given type for the current Matrix room. "
|
|
||||||
"The type is either `group`, `supergroup` or `channel` (defaults to "
|
|
||||||
"`group`).")
|
|
||||||
async def create(evt: CommandEvent) -> Dict:
|
|
||||||
type = evt.args[0] if len(evt.args) > 0 else "group"
|
|
||||||
if type not in {"chat", "group", "supergroup", "channel"}:
|
|
||||||
return await evt.reply(
|
|
||||||
"**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
|
|
||||||
|
|
||||||
if po.Portal.get_by_mxid(evt.room_id):
|
|
||||||
return await evt.reply("This is already a portal room.")
|
|
||||||
|
|
||||||
if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
|
|
||||||
return await evt.reply("You do not have the permissions to bridge this room.")
|
|
||||||
|
|
||||||
title, about, levels = await get_initial_state(evt.az.intent, evt.room_id)
|
|
||||||
if not title:
|
|
||||||
return await evt.reply("Please set a title before creating a Telegram chat.")
|
|
||||||
|
|
||||||
supergroup = type == "supergroup"
|
|
||||||
type = {
|
|
||||||
"supergroup": "channel",
|
|
||||||
"channel": "channel",
|
|
||||||
"chat": "chat",
|
|
||||||
"group": "chat",
|
|
||||||
}[type]
|
|
||||||
|
|
||||||
portal = po.Portal(tgid=None, mxid=evt.room_id, title=title, about=about, peer_type=type)
|
|
||||||
try:
|
|
||||||
await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
|
|
||||||
except ValueError as e:
|
|
||||||
portal.delete()
|
|
||||||
return await evt.reply(e.args[0])
|
|
||||||
return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
|
||||||
help_text="Upgrade a normal Telegram group to a supergroup.")
|
|
||||||
async def upgrade(evt: CommandEvent) -> Dict:
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
return await evt.reply("This is not a portal room.")
|
|
||||||
elif portal.peer_type == "channel":
|
|
||||||
return await evt.reply("This is already a supergroup or a channel.")
|
|
||||||
elif portal.peer_type == "user":
|
|
||||||
return await evt.reply("You can't upgrade private chats.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
await portal.upgrade_telegram_chat(evt.sender)
|
|
||||||
return await evt.reply(f"Group upgraded to supergroup. New ID: -100{portal.tgid}")
|
|
||||||
except ChatAdminRequiredError:
|
|
||||||
return await evt.reply("You don't have the permission to upgrade this group.")
|
|
||||||
except ValueError as e:
|
|
||||||
return await evt.reply(e.args[0])
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(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"):
|
|
||||||
await config_help(evt)
|
|
||||||
return
|
|
||||||
elif cmd == "defaults":
|
|
||||||
await config_defaults(evt)
|
|
||||||
return
|
|
||||||
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
await evt.reply("This is not a portal room.")
|
|
||||||
return
|
|
||||||
elif cmd == "view":
|
|
||||||
await config_view(evt, portal)
|
|
||||||
return
|
|
||||||
|
|
||||||
key = evt.args[1] if len(evt.args) > 1 else None
|
|
||||||
value = yaml.load(" ".join(evt.args[2:])) if len(evt.args) > 2 else None
|
|
||||||
if cmd == "set":
|
|
||||||
await config_set(evt, portal, key, value)
|
|
||||||
elif cmd == "unset":
|
|
||||||
await config_unset(evt, portal, key)
|
|
||||||
elif cmd == "add" or cmd == "del":
|
|
||||||
await config_add_del(evt, portal, key, value, cmd)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
portal.save()
|
|
||||||
|
|
||||||
|
|
||||||
def config_help(evt: CommandEvent) -> Awaitable[Dict]:
|
|
||||||
return evt.reply("""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
|
||||||
|
|
||||||
* **help** - View this help text.
|
|
||||||
* **view** - View the current config data.
|
|
||||||
* **defaults** - View the default config values.
|
|
||||||
* **set** <_key_> <_value_> - Set a config value.
|
|
||||||
* **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[Dict]:
|
|
||||||
stream = StringIO()
|
|
||||||
yaml.dump(portal.local_config, stream)
|
|
||||||
return evt.reply(f"Room-specific config:\n\n```yaml\n{stream.getvalue()}```")
|
|
||||||
|
|
||||||
|
|
||||||
def config_defaults(evt: CommandEvent) -> Awaitable[Dict]:
|
|
||||||
stream = StringIO()
|
|
||||||
yaml.dump({
|
|
||||||
"edits_as_replies": evt.config["bridge.edits_as_replies"],
|
|
||||||
"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"],
|
|
||||||
"native_stickers": evt.config["bridge.native_stickers"],
|
|
||||||
"message_formats": evt.config["bridge.message_formats"],
|
|
||||||
"state_event_formats": evt.config["bridge.state_event_formats"],
|
|
||||||
"telegram_link_preview": evt.config["bridge.telegram_link_preview"],
|
|
||||||
}, stream)
|
|
||||||
return evt.reply(f"Bridge instance wide config:\n\n```yaml\n{stream.getvalue()}```")
|
|
||||||
|
|
||||||
|
|
||||||
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[Dict]:
|
|
||||||
if not key or value is None:
|
|
||||||
return evt.reply(f"**Usage:** `$cmdprefix+sp config set <key> <value>`")
|
|
||||||
elif util.recursive_set(portal.local_config, key, value):
|
|
||||||
return evt.reply(f"Successfully set the value of `{key}` to `{value}`.")
|
|
||||||
else:
|
|
||||||
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[Dict]:
|
|
||||||
if not key:
|
|
||||||
return evt.reply(f"**Usage:** `$cmdprefix+sp config unset <key>`")
|
|
||||||
elif util.recursive_del(portal.local_config, key):
|
|
||||||
return evt.reply(f"Successfully deleted `{key}` from config.")
|
|
||||||
else:
|
|
||||||
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[Dict]:
|
|
||||||
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?")
|
|
||||||
elif not isinstance(arr, list):
|
|
||||||
return evt.reply("`{key}` does not seem to be an array.")
|
|
||||||
elif cmd == "add":
|
|
||||||
if value in arr:
|
|
||||||
return evt.reply(f"The array at `{key}` already contains `{value}`.")
|
|
||||||
arr.append(value)
|
|
||||||
return evt.reply(f"Successfully added `{value}` to the array at `{key}`")
|
|
||||||
else:
|
|
||||||
if value not in arr:
|
|
||||||
return evt.reply(f"The array at `{key}` does not contain `{value}`.")
|
|
||||||
arr.remove(value)
|
|
||||||
return evt.reply(f"Successfully removed `{value}` from the array at `{key}`")
|
|
||||||
|
|
||||||
|
|
||||||
@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) -> Dict:
|
|
||||||
if len(evt.args) == 0:
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp group-name <name/->`")
|
|
||||||
|
|
||||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
|
||||||
if not portal:
|
|
||||||
return await evt.reply("This is not a portal room.")
|
|
||||||
elif portal.peer_type != "channel":
|
|
||||||
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 "")
|
|
||||||
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.")
|
|
||||||
except UsernameNotModifiedError:
|
|
||||||
if portal.username:
|
|
||||||
return await evt.reply("That is already the username of this channel.")
|
|
||||||
else:
|
|
||||||
return await evt.reply("This channel is already private")
|
|
||||||
except UsernameOccupiedError:
|
|
||||||
return await evt.reply("That username is already in use.")
|
|
||||||
except UsernameInvalidError:
|
|
||||||
return await evt.reply("Invalid username")
|
|
||||||
|
|
||||||
|
|
||||||
@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) -> Dict:
|
|
||||||
try:
|
|
||||||
mode = evt.args[0]
|
|
||||||
if mode not in ("whitelist", "blacklist"):
|
|
||||||
raise ValueError()
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp filter-mode <whitelist/blacklist>`")
|
|
||||||
|
|
||||||
evt.config["bridge.filter.mode"] = mode
|
|
||||||
evt.config.save()
|
|
||||||
po.Portal.filter_mode = mode
|
|
||||||
if mode == "whitelist":
|
|
||||||
return await evt.reply("The bridge will now disallow bridging chats by default.\n"
|
|
||||||
"To allow bridging a specific chat, use"
|
|
||||||
"`!filter whitelist <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>`.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_admin=True,
|
|
||||||
help_section=SECTION_ADMIN,
|
|
||||||
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
|
|
||||||
help_text="Allow or disallow bridging a specific chat.")
|
|
||||||
async def filter(evt: CommandEvent) -> Optional[Dict]:
|
|
||||||
try:
|
|
||||||
action = evt.args[0]
|
|
||||||
if action not in ("whitelist", "blacklist", "add", "remove"):
|
|
||||||
raise ValueError()
|
|
||||||
|
|
||||||
id_str = evt.args[1]
|
|
||||||
if id_str.startswith("-100"):
|
|
||||||
id = int(id_str[4:])
|
|
||||||
elif id_str.startswith("-"):
|
|
||||||
id = int(id_str[1:])
|
|
||||||
else:
|
|
||||||
id = int(id_str)
|
|
||||||
except (IndexError, ValueError):
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp filter <whitelist/blacklist> <chat ID>`")
|
|
||||||
|
|
||||||
mode = evt.config["bridge.filter.mode"]
|
|
||||||
if mode not in ("blacklist", "whitelist"):
|
|
||||||
return await evt.reply(f"Unknown filter mode \"{mode}\". Please fix the bridge config.")
|
|
||||||
|
|
||||||
list = evt.config["bridge.filter.list"]
|
|
||||||
|
|
||||||
if action in ("blacklist", "whitelist"):
|
|
||||||
action = "add" if mode == action else "remove"
|
|
||||||
|
|
||||||
def save() -> None:
|
|
||||||
evt.config["bridge.filter.list"] = list
|
|
||||||
evt.config.save()
|
|
||||||
po.Portal.filter_list = list
|
|
||||||
|
|
||||||
if action == "add":
|
|
||||||
if id in list:
|
|
||||||
return await evt.reply(f"That chat is already {mode}ed.")
|
|
||||||
list.append(id)
|
|
||||||
save()
|
|
||||||
return await evt.reply(f"Chat ID added to {mode}.")
|
|
||||||
elif action == "remove":
|
|
||||||
if id not in list:
|
|
||||||
return await evt.reply(f"That chat is not {mode}ed.")
|
|
||||||
list.remove(id)
|
|
||||||
save()
|
|
||||||
return await evt.reply(f"Chat ID removed from {mode}.")
|
|
||||||
return None
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import admin, bridge, config, create_chat, filter, misc, unbridge
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from mautrix_appservice import MatrixRequestError
|
||||||
|
|
||||||
|
from ... import portal as po, puppet as pu, user as u
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(needs_admin=True, needs_auth=False, name="set-pl",
|
||||||
|
help_section=SECTION_ADMIN,
|
||||||
|
help_args="<_level_> [_mxid_]",
|
||||||
|
help_text="Set a temporary power level without affecting Telegram.")
|
||||||
|
async def set_power_level(evt: CommandEvent) -> Dict:
|
||||||
|
try:
|
||||||
|
level = int(evt.args[0])
|
||||||
|
except KeyError:
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp set-pl <level> [mxid]`")
|
||||||
|
except ValueError:
|
||||||
|
return await evt.reply("The level must be an integer.")
|
||||||
|
levels = await evt.az.intent.get_power_levels(evt.room_id)
|
||||||
|
mxid = evt.args[1] if len(evt.args) > 1 else evt.sender.mxid
|
||||||
|
levels["users"][mxid] = level
|
||||||
|
try:
|
||||||
|
await evt.az.intent.set_power_levels(evt.room_id, levels)
|
||||||
|
except MatrixRequestError:
|
||||||
|
evt.log.exception("Failed to set power level.")
|
||||||
|
return await evt.reply("Failed to set power level.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Dict:
|
||||||
|
try:
|
||||||
|
section = evt.args[0].lower()
|
||||||
|
except KeyError:
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp clear-db-cache <section>`")
|
||||||
|
if section == "portal":
|
||||||
|
po.Portal.by_tgid = {}
|
||||||
|
po.Portal.by_mxid = {}
|
||||||
|
await evt.reply("Cleared portal cache")
|
||||||
|
elif section == "puppet":
|
||||||
|
pu.Puppet.cache = {}
|
||||||
|
for puppet in pu.Puppet.by_custom_mxid.values():
|
||||||
|
puppet.sync_task.cancel()
|
||||||
|
pu.Puppet.by_custom_mxid = {}
|
||||||
|
await asyncio.gather(
|
||||||
|
*[puppet.init_custom_mxid() for puppet in pu.Puppet.all_with_custom_mxid()],
|
||||||
|
loop=evt.loop)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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="[user]",
|
||||||
|
help_text="Reload and reconnect a user")
|
||||||
|
async def reload_user(evt: CommandEvent) -> Dict:
|
||||||
|
if len(evt.args) > 0:
|
||||||
|
mxid = evt.args[0]
|
||||||
|
else:
|
||||||
|
mxid = evt.sender.mxid
|
||||||
|
user = u.User.get_by_mxid(mxid, create=False)
|
||||||
|
if not user:
|
||||||
|
return await evt.reply("User not found")
|
||||||
|
puppet = pu.Puppet.get_by_custom_mxid(mxid)
|
||||||
|
if puppet:
|
||||||
|
puppet.sync_task.cancel()
|
||||||
|
await user.stop()
|
||||||
|
user.delete(delete_db=False)
|
||||||
|
user = u.User.get_by_mxid(mxid)
|
||||||
|
await user.ensure_started()
|
||||||
|
if puppet:
|
||||||
|
await puppet.init_custom_mxid()
|
||||||
|
await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})")
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Optional, Tuple, Coroutine
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from telethon.tl.types import ChatForbidden, ChannelForbidden
|
||||||
|
|
||||||
|
from ...types import MatrixRoomID, TelegramID
|
||||||
|
from ...util import ignore_coro
|
||||||
|
from ... import portal as po
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
||||||
|
from .util import user_has_power_level, get_initial_state
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Dict:
|
||||||
|
if len(evt.args) == 0:
|
||||||
|
return await evt.reply("**Usage:** "
|
||||||
|
"`$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`")
|
||||||
|
room_id = MatrixRoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
|
||||||
|
that_this = "This" if room_id == evt.room_id else "That"
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_mxid(room_id)
|
||||||
|
if portal:
|
||||||
|
return await evt.reply(f"{that_this} room is already a portal room.")
|
||||||
|
|
||||||
|
if not await user_has_power_level(room_id, evt.az.intent, evt.sender, "bridge"):
|
||||||
|
return await evt.reply(f"You do not have the permissions to bridge {that_this} room.")
|
||||||
|
|
||||||
|
# The /id bot command provides the prefixed ID, so we assume
|
||||||
|
tgid_str = evt.args[0]
|
||||||
|
if tgid_str.startswith("-100"):
|
||||||
|
tgid = TelegramID(int(tgid_str[4:]))
|
||||||
|
peer_type = "channel"
|
||||||
|
elif tgid_str.startswith("-"):
|
||||||
|
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.")
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_tgid(tgid, peer_type=peer_type)
|
||||||
|
if not portal.allow_bridging():
|
||||||
|
return await evt.reply("This bridge doesn't allow bridging that Telegram chat.\n"
|
||||||
|
"If you're the bridge admin, try "
|
||||||
|
"`$cmdprefix+sp 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}). ")
|
||||||
|
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.")
|
||||||
|
evt.sender.command_status = {
|
||||||
|
"next": confirm_bridge,
|
||||||
|
"action": "Room bridging",
|
||||||
|
"mxid": portal.mxid,
|
||||||
|
"bridge_to_mxid": room_id,
|
||||||
|
"tgid": portal.tgid,
|
||||||
|
"peer_type": portal.peer_type,
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
"bridge_to_mxid": room_id,
|
||||||
|
"tgid": portal.tgid,
|
||||||
|
"peer_type": portal.peer_type,
|
||||||
|
}
|
||||||
|
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, Optional[Coroutine[None, 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...")
|
||||||
|
return True, None
|
||||||
|
elif evt.args[0] == "delete-and-continue":
|
||||||
|
return True, portal.cleanup_room(portal.main_intent, portal.mxid,
|
||||||
|
message="Portal deleted (moving to another room)")
|
||||||
|
elif evt.args[0] == "unbridge-and-continue":
|
||||||
|
return True, portal.cleanup_room(portal.main_intent, portal.mxid,
|
||||||
|
message="Room unbridged (portal moving to another room)",
|
||||||
|
puppets_only=True)
|
||||||
|
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.")
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]:
|
||||||
|
status = evt.sender.command_status
|
||||||
|
try:
|
||||||
|
portal = po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"])
|
||||||
|
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.")
|
||||||
|
if "mxid" in status:
|
||||||
|
ok, coro = await cleanup_old_portal_while_bridging(evt, portal)
|
||||||
|
if not ok:
|
||||||
|
return None
|
||||||
|
elif coro:
|
||||||
|
ignore_coro(asyncio.ensure_future(coro, loop=evt.loop))
|
||||||
|
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.")
|
||||||
|
elif evt.args[0] != "continue":
|
||||||
|
return await evt.reply("Please use `$cmdprefix+sp continue` to confirm the bridging or "
|
||||||
|
"`$cmdprefix+sp cancel` to cancel.")
|
||||||
|
|
||||||
|
evt.sender.command_status = None
|
||||||
|
is_logged_in = await evt.sender.is_logged_in()
|
||||||
|
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?")
|
||||||
|
else:
|
||||||
|
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.")
|
||||||
|
else:
|
||||||
|
return await evt.reply("The bot doesn't seem to be in that chat.")
|
||||||
|
|
||||||
|
direct = False
|
||||||
|
|
||||||
|
portal.mxid = bridge_to_mxid
|
||||||
|
portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id)
|
||||||
|
portal.photo_id = ""
|
||||||
|
portal.save()
|
||||||
|
|
||||||
|
ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct,
|
||||||
|
levels=levels),
|
||||||
|
loop=evt.loop))
|
||||||
|
|
||||||
|
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Awaitable
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from ...config import yaml
|
||||||
|
from ... import portal as po, user as u, util
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||||
|
|
||||||
|
@command_handler(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"):
|
||||||
|
await config_help(evt)
|
||||||
|
return
|
||||||
|
elif cmd == "defaults":
|
||||||
|
await config_defaults(evt)
|
||||||
|
return
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
await evt.reply("This is not a portal room.")
|
||||||
|
return
|
||||||
|
elif cmd == "view":
|
||||||
|
await config_view(evt, portal)
|
||||||
|
return
|
||||||
|
|
||||||
|
key = evt.args[1] if len(evt.args) > 1 else None
|
||||||
|
value = yaml.load(" ".join(evt.args[2:])) if len(evt.args) > 2 else None
|
||||||
|
if cmd == "set":
|
||||||
|
await config_set(evt, portal, key, value)
|
||||||
|
elif cmd == "unset":
|
||||||
|
await config_unset(evt, portal, key)
|
||||||
|
elif cmd == "add" or cmd == "del":
|
||||||
|
await config_add_del(evt, portal, key, value, cmd)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
portal.save()
|
||||||
|
|
||||||
|
|
||||||
|
def config_help(evt: CommandEvent) -> Awaitable[Dict]:
|
||||||
|
return evt.reply("""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
||||||
|
|
||||||
|
* **help** - View this help text.
|
||||||
|
* **view** - View the current config data.
|
||||||
|
* **defaults** - View the default config values.
|
||||||
|
* **set** <_key_> <_value_> - Set a config value.
|
||||||
|
* **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[Dict]:
|
||||||
|
stream = StringIO()
|
||||||
|
yaml.dump(portal.local_config, stream)
|
||||||
|
return evt.reply(f"Room-specific config:\n\n```yaml\n{stream.getvalue()}```")
|
||||||
|
|
||||||
|
|
||||||
|
def config_defaults(evt: CommandEvent) -> Awaitable[Dict]:
|
||||||
|
stream = StringIO()
|
||||||
|
yaml.dump({
|
||||||
|
"edits_as_replies": evt.config["bridge.edits_as_replies"],
|
||||||
|
"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"],
|
||||||
|
"state_event_formats": evt.config["bridge.state_event_formats"],
|
||||||
|
"telegram_link_preview": evt.config["bridge.telegram_link_preview"],
|
||||||
|
}, stream)
|
||||||
|
return evt.reply(f"Bridge instance wide config:\n\n```yaml\n{stream.getvalue()}```")
|
||||||
|
|
||||||
|
|
||||||
|
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[Dict]:
|
||||||
|
if not key or value is None:
|
||||||
|
return evt.reply(f"**Usage:** `$cmdprefix+sp config set <key> <value>`")
|
||||||
|
elif util.recursive_set(portal.local_config, key, value):
|
||||||
|
return evt.reply(f"Successfully set the value of `{key}` to `{value}`.")
|
||||||
|
else:
|
||||||
|
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[Dict]:
|
||||||
|
if not key:
|
||||||
|
return evt.reply(f"**Usage:** `$cmdprefix+sp config unset <key>`")
|
||||||
|
elif util.recursive_del(portal.local_config, key):
|
||||||
|
return evt.reply(f"Successfully deleted `{key}` from config.")
|
||||||
|
else:
|
||||||
|
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[Dict]:
|
||||||
|
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?")
|
||||||
|
elif not isinstance(arr, list):
|
||||||
|
return evt.reply("`{key}` does not seem to be an array.")
|
||||||
|
elif cmd == "add":
|
||||||
|
if value in arr:
|
||||||
|
return evt.reply(f"The array at `{key}` already contains `{value}`.")
|
||||||
|
arr.append(value)
|
||||||
|
return evt.reply(f"Successfully added `{value}` to the array at `{key}`")
|
||||||
|
else:
|
||||||
|
if value not in arr:
|
||||||
|
return evt.reply(f"The array at `{key}` does not contain `{value}`.")
|
||||||
|
arr.remove(value)
|
||||||
|
return evt.reply(f"Successfully removed `{value}` from the array at `{key}`")
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from ... import portal as po
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
||||||
|
from .util import user_has_power_level, get_initial_state
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(help_section=SECTION_CREATING_PORTALS,
|
||||||
|
help_args="[_type_]",
|
||||||
|
help_text="Create a Telegram chat of the given type for the current Matrix room. "
|
||||||
|
"The type is either `group`, `supergroup` or `channel` (defaults to "
|
||||||
|
"`group`).")
|
||||||
|
async def create(evt: CommandEvent) -> Dict:
|
||||||
|
type = evt.args[0] if len(evt.args) > 0 else "group"
|
||||||
|
if type not in {"chat", "group", "supergroup", "channel"}:
|
||||||
|
return await evt.reply(
|
||||||
|
"**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`")
|
||||||
|
|
||||||
|
if po.Portal.get_by_mxid(evt.room_id):
|
||||||
|
return await evt.reply("This is already a portal room.")
|
||||||
|
|
||||||
|
if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
|
||||||
|
return await evt.reply("You do not have the permissions to bridge this room.")
|
||||||
|
|
||||||
|
title, about, levels = await get_initial_state(evt.az.intent, evt.room_id)
|
||||||
|
if not title:
|
||||||
|
return await evt.reply("Please set a title before creating a Telegram chat.")
|
||||||
|
|
||||||
|
supergroup = type == "supergroup"
|
||||||
|
type = {
|
||||||
|
"supergroup": "channel",
|
||||||
|
"channel": "channel",
|
||||||
|
"chat": "chat",
|
||||||
|
"group": "chat",
|
||||||
|
}[type]
|
||||||
|
|
||||||
|
portal = po.Portal(tgid=None, mxid=evt.room_id, title=title, about=about, peer_type=type)
|
||||||
|
try:
|
||||||
|
await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
|
||||||
|
except ValueError as e:
|
||||||
|
portal.delete()
|
||||||
|
return await evt.reply(e.args[0])
|
||||||
|
return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from ... import portal as po
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Dict:
|
||||||
|
try:
|
||||||
|
mode = evt.args[0]
|
||||||
|
if mode not in ("whitelist", "blacklist"):
|
||||||
|
raise ValueError()
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp filter-mode <whitelist/blacklist>`")
|
||||||
|
|
||||||
|
evt.config["bridge.filter.mode"] = mode
|
||||||
|
evt.config.save()
|
||||||
|
po.Portal.filter_mode = mode
|
||||||
|
if mode == "whitelist":
|
||||||
|
return await evt.reply("The bridge will now disallow bridging chats by default.\n"
|
||||||
|
"To allow bridging a specific chat, use"
|
||||||
|
"`!filter whitelist <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>`.")
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(needs_admin=True,
|
||||||
|
help_section=SECTION_ADMIN,
|
||||||
|
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
|
||||||
|
help_text="Allow or disallow bridging a specific chat.")
|
||||||
|
async def filter(evt: CommandEvent) -> Optional[Dict]:
|
||||||
|
try:
|
||||||
|
action = evt.args[0]
|
||||||
|
if action not in ("whitelist", "blacklist", "add", "remove"):
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
id_str = evt.args[1]
|
||||||
|
if id_str.startswith("-100"):
|
||||||
|
id = int(id_str[4:])
|
||||||
|
elif id_str.startswith("-"):
|
||||||
|
id = int(id_str[1:])
|
||||||
|
else:
|
||||||
|
id = int(id_str)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp filter <whitelist/blacklist> <chat ID>`")
|
||||||
|
|
||||||
|
mode = evt.config["bridge.filter.mode"]
|
||||||
|
if mode not in ("blacklist", "whitelist"):
|
||||||
|
return await evt.reply(f"Unknown filter mode \"{mode}\". Please fix the bridge config.")
|
||||||
|
|
||||||
|
list = evt.config["bridge.filter.list"]
|
||||||
|
|
||||||
|
if action in ("blacklist", "whitelist"):
|
||||||
|
action = "add" if mode == action else "remove"
|
||||||
|
|
||||||
|
def save() -> None:
|
||||||
|
evt.config["bridge.filter.list"] = list
|
||||||
|
evt.config.save()
|
||||||
|
po.Portal.filter_list = list
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
if id in list:
|
||||||
|
return await evt.reply(f"That chat is already {mode}ed.")
|
||||||
|
list.append(id)
|
||||||
|
save()
|
||||||
|
return await evt.reply(f"Chat ID added to {mode}.")
|
||||||
|
elif action == "remove":
|
||||||
|
if id not in list:
|
||||||
|
return await evt.reply(f"That chat is not {mode}ed.")
|
||||||
|
list.remove(id)
|
||||||
|
save()
|
||||||
|
return await evt.reply(f"Chat ID removed from {mode}.")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
|
||||||
|
UsernameNotModifiedError, UsernameOccupiedError)
|
||||||
|
|
||||||
|
from ... import portal as po
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC
|
||||||
|
from .util import user_has_power_level
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False,
|
||||||
|
help_section=SECTION_MISC,
|
||||||
|
help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.")
|
||||||
|
async def sync_state(evt: CommandEvent) -> Dict:
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
return await evt.reply("This is not a portal room.")
|
||||||
|
elif not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
|
||||||
|
return await evt.reply(f"You do not have the permissions to synchronize this room.")
|
||||||
|
|
||||||
|
await portal.sync_matrix_members()
|
||||||
|
await evt.reply("Synchronization complete")
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(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 id(evt: CommandEvent) -> Dict:
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
return await evt.reply("This is not a portal room.")
|
||||||
|
tgid = portal.tgid
|
||||||
|
if portal.peer_type == "chat":
|
||||||
|
tgid = -tgid
|
||||||
|
elif portal.peer_type == "channel":
|
||||||
|
tgid = f"-100{tgid}"
|
||||||
|
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||||
|
help_text="Get a Telegram invite link to the current chat.")
|
||||||
|
async def invite_link(evt: CommandEvent) -> Dict:
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
return await evt.reply("This is not a portal room.")
|
||||||
|
|
||||||
|
if portal.peer_type == "user":
|
||||||
|
return await evt.reply("You can't invite users to private chats.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
link = await portal.get_invite_link(evt.sender)
|
||||||
|
return await evt.reply(f"Invite link to {portal.title}: {link}")
|
||||||
|
except ValueError as e:
|
||||||
|
return await evt.reply(e.args[0])
|
||||||
|
except ChatAdminRequiredError:
|
||||||
|
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.")
|
||||||
|
async def upgrade(evt: CommandEvent) -> Dict:
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
return await evt.reply("This is not a portal room.")
|
||||||
|
elif portal.peer_type == "channel":
|
||||||
|
return await evt.reply("This is already a supergroup or a channel.")
|
||||||
|
elif portal.peer_type == "user":
|
||||||
|
return await evt.reply("You can't upgrade private chats.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await portal.upgrade_telegram_chat(evt.sender)
|
||||||
|
return await evt.reply(f"Group upgraded to supergroup. New ID: -100{portal.tgid}")
|
||||||
|
except ChatAdminRequiredError:
|
||||||
|
return await evt.reply("You don't have the permission to upgrade this group.")
|
||||||
|
except ValueError as e:
|
||||||
|
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.")
|
||||||
|
async def group_name(evt: CommandEvent) -> Dict:
|
||||||
|
if len(evt.args) == 0:
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp group-name <name/->`")
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
return await evt.reply("This is not a portal room.")
|
||||||
|
elif portal.peer_type != "channel":
|
||||||
|
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 "")
|
||||||
|
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.")
|
||||||
|
except UsernameNotModifiedError:
|
||||||
|
if portal.username:
|
||||||
|
return await evt.reply("That is already the username of this channel.")
|
||||||
|
else:
|
||||||
|
return await evt.reply("This channel is already private")
|
||||||
|
except UsernameOccupiedError:
|
||||||
|
return await evt.reply("That username is already in use.")
|
||||||
|
except UsernameInvalidError:
|
||||||
|
return await evt.reply("Invalid username")
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Callable, Optional
|
||||||
|
|
||||||
|
from ...types import MatrixRoomID
|
||||||
|
from ... import portal as po
|
||||||
|
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||||
|
from .util import user_has_power_level, get_initial_state
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||||
|
action: Optional[str] = None
|
||||||
|
) -> Optional[po.Portal]:
|
||||||
|
room_id = MatrixRoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_mxid(room_id)
|
||||||
|
if not portal:
|
||||||
|
that_this = "This" if room_id == evt.room_id else "That"
|
||||||
|
await evt.reply(f"{that_this} is not a portal room.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, permission):
|
||||||
|
action = action or f"{permission.replace('_', ' ')}s"
|
||||||
|
await evt.reply(f"You do not have the permissions to {action} that portal.")
|
||||||
|
return None
|
||||||
|
return portal
|
||||||
|
|
||||||
|
|
||||||
|
def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str,
|
||||||
|
completed_message: str) -> Dict:
|
||||||
|
async def post_confirm(confirm) -> Optional[Dict]:
|
||||||
|
confirm.sender.command_status = None
|
||||||
|
if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}":
|
||||||
|
await function()
|
||||||
|
if confirm.room_id != room_id:
|
||||||
|
return await confirm.reply(completed_message)
|
||||||
|
else:
|
||||||
|
return await confirm.reply(f"{action} cancelled.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"next": post_confirm,
|
||||||
|
"action": action,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Optional[Dict]:
|
||||||
|
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||||
|
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.")
|
||||||
|
|
||||||
|
|
||||||
|
@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) -> Optional[Dict]:
|
||||||
|
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||||
|
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`")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
from mautrix_appservice import MatrixRequestError, IntentAPI
|
||||||
|
|
||||||
|
from ... import user as u
|
||||||
|
|
||||||
|
|
||||||
|
async def get_initial_state(intent: IntentAPI, room_id: str) -> Tuple[str, str, Dict]:
|
||||||
|
state = await intent.get_room_state(room_id)
|
||||||
|
title = None
|
||||||
|
about = None
|
||||||
|
levels = None
|
||||||
|
for event in state:
|
||||||
|
try:
|
||||||
|
if event["type"] == "m.room.name":
|
||||||
|
title = event["content"]["name"]
|
||||||
|
elif event["type"] == "m.room.topic":
|
||||||
|
about = event["content"]["topic"]
|
||||||
|
elif event["type"] == "m.room.power_levels":
|
||||||
|
levels = event["content"]
|
||||||
|
elif event["type"] == "m.room.canonical_alias":
|
||||||
|
title = title or event["content"]["alias"]
|
||||||
|
except KeyError:
|
||||||
|
# Some state event probably has empty content
|
||||||
|
pass
|
||||||
|
return title, about, levels
|
||||||
|
|
||||||
|
|
||||||
|
async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50
|
||||||
|
) -> bool:
|
||||||
|
if sender.is_admin:
|
||||||
|
return True
|
||||||
|
# Make sure the state store contains the power levels.
|
||||||
|
try:
|
||||||
|
await intent.get_power_levels(room)
|
||||||
|
except MatrixRequestError:
|
||||||
|
return False
|
||||||
|
return intent.state_store.has_power_level(room, sender.mxid,
|
||||||
|
event=f"net.maunium.telegram.{event}",
|
||||||
|
default=default)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import account, auth, misc
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# -*- coding: future_fstrings -*-
|
||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2018 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from telethon.errors import UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError
|
||||||
|
from telethon.tl.functions.account import UpdateUsernameRequest
|
||||||
|
|
||||||
|
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(needs_auth=True,
|
||||||
|
help_section=SECTION_AUTH,
|
||||||
|
help_text="Change your Telegram username")
|
||||||
|
async def username(evt: CommandEvent) -> Optional[Dict]:
|
||||||
|
if len(evt.args) == 0:
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp username <new username>`")
|
||||||
|
if evt.sender.is_bot:
|
||||||
|
return await evt.reply("Bots can't set their own username.")
|
||||||
|
new_name = evt.args[0]
|
||||||
|
if new_name == "-":
|
||||||
|
new_name = ""
|
||||||
|
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.")
|
||||||
|
except UsernameNotModifiedError:
|
||||||
|
return await evt.reply("That is your current username.")
|
||||||
|
except UsernameOccupiedError:
|
||||||
|
return await evt.reply("That username is already in use.")
|
||||||
|
await evt.sender.update_info()
|
||||||
|
if not evt.sender.username:
|
||||||
|
await evt.reply("Username removed")
|
||||||
|
else:
|
||||||
|
await evt.reply(f"Username changed to {evt.sender.username}")
|
||||||
@@ -21,13 +21,11 @@ from telethon.errors import (
|
|||||||
AccessTokenExpiredError, AccessTokenInvalidError, FirstNameInvalidError, FloodWaitError,
|
AccessTokenExpiredError, AccessTokenInvalidError, FirstNameInvalidError, FloodWaitError,
|
||||||
PasswordHashInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError,
|
PasswordHashInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError,
|
||||||
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
||||||
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError,
|
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError)
|
||||||
UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError)
|
|
||||||
from telethon.tl.functions.account import UpdateUsernameRequest
|
|
||||||
|
|
||||||
from . import command_handler, CommandEvent, SECTION_AUTH
|
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
||||||
from .. import puppet as pu, user as u
|
from mautrix_telegram import puppet as pu, user as u
|
||||||
from ..util import format_duration, ignore_coro
|
from mautrix_telegram.util import format_duration, ignore_coro
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False,
|
@command_handler(needs_auth=False,
|
||||||
@@ -56,87 +54,6 @@ async def ping_bot(evt: CommandEvent) -> Optional[Dict]:
|
|||||||
"To use the bot, simply invite it to a portal room.")
|
"To use the bot, simply invite it to a portal room.")
|
||||||
|
|
||||||
|
|
||||||
@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) -> Optional[Dict]:
|
|
||||||
puppet = pu.Puppet.get(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")
|
|
||||||
async def login_matrix(evt: CommandEvent) -> Optional[Dict]:
|
|
||||||
puppet = pu.Puppet.get(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.")
|
|
||||||
allow_matrix_login = evt.config.get("bridge.allow_matrix_login", True)
|
|
||||||
if allow_matrix_login:
|
|
||||||
evt.sender.command_status = {
|
|
||||||
"next": enter_matrix_token,
|
|
||||||
"action": "Matrix login",
|
|
||||||
}
|
|
||||||
if evt.config["appservice.public.enabled"]:
|
|
||||||
prefix = evt.config["appservice.public.external"]
|
|
||||||
token = evt.public_website.make_token(evt.sender.mxid, "/matrix-login")
|
|
||||||
url = f"{prefix}/matrix-login?token={token}"
|
|
||||||
if allow_matrix_login:
|
|
||||||
return await evt.reply(
|
|
||||||
"This bridge instance allows you to log in inside or outside Matrix.\n\n"
|
|
||||||
"If you would like to log in within Matrix, please send your Matrix access token "
|
|
||||||
"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.")
|
|
||||||
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.")
|
|
||||||
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) -> Optional[Dict]:
|
|
||||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
|
||||||
if not puppet.is_real_user:
|
|
||||||
return await evt.reply("You are not logged in with your Matrix account.")
|
|
||||||
resp = await puppet.init_custom_mxid()
|
|
||||||
if resp == pu.PuppetError.InvalidAccessToken:
|
|
||||||
return await evt.reply("Your access token is invalid.")
|
|
||||||
elif resp == pu.PuppetError.Success:
|
|
||||||
return await evt.reply("Your Matrix login is working.")
|
|
||||||
return await evt.reply(f"Unknown response while checking your Matrix login: {resp}.")
|
|
||||||
|
|
||||||
|
|
||||||
async def enter_matrix_token(evt: CommandEvent) -> Dict:
|
|
||||||
evt.sender.command_status = None
|
|
||||||
|
|
||||||
puppet = pu.Puppet.get(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.")
|
|
||||||
|
|
||||||
resp = await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
|
|
||||||
if resp == pu.PuppetError.OnlyLoginSelf:
|
|
||||||
return await evt.reply("You can only log in as your own Matrix user.")
|
|
||||||
elif resp == pu.PuppetError.InvalidAccessToken:
|
|
||||||
return await evt.reply("Failed to verify access token.")
|
|
||||||
assert resp == pu.PuppetError.Success, "Encountered an unhandled PuppetError."
|
|
||||||
return await evt.reply(
|
|
||||||
f"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, management_only=True,
|
@command_handler(needs_auth=False, management_only=True,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
help_args="<_phone_> <_full name_>",
|
help_args="<_phone_> <_full name_>",
|
||||||
@@ -375,30 +292,3 @@ async def logout(evt: CommandEvent) -> Optional[Dict]:
|
|||||||
if await evt.sender.log_out():
|
if await evt.sender.log_out():
|
||||||
return await evt.reply("Logged out successfully.")
|
return await evt.reply("Logged out successfully.")
|
||||||
return await evt.reply("Failed to log out.")
|
return await evt.reply("Failed to log out.")
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=True,
|
|
||||||
help_section=SECTION_AUTH,
|
|
||||||
help_text="Change your Telegram username")
|
|
||||||
async def username(evt: CommandEvent) -> Optional[Dict]:
|
|
||||||
if len(evt.args) == 0:
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp username <new username>`")
|
|
||||||
if evt.sender.is_bot:
|
|
||||||
return await evt.reply("Bots can't set their own username.")
|
|
||||||
new_name = evt.args[0]
|
|
||||||
if new_name == "-":
|
|
||||||
new_name = ""
|
|
||||||
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.")
|
|
||||||
except UsernameNotModifiedError:
|
|
||||||
return await evt.reply("That is your current username.")
|
|
||||||
except UsernameOccupiedError:
|
|
||||||
return await evt.reply("That username is already in use.")
|
|
||||||
await evt.sender.update_info()
|
|
||||||
if not evt.sender.username:
|
|
||||||
await evt.reply("Username removed")
|
|
||||||
else:
|
|
||||||
await evt.reply(f"Username changed to {evt.sender.username}")
|
|
||||||
@@ -27,10 +27,10 @@ from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatIn
|
|||||||
GetBotCallbackAnswerRequest)
|
GetBotCallbackAnswerRequest)
|
||||||
from telethon.tl.functions.channels import JoinChannelRequest
|
from telethon.tl.functions.channels import JoinChannelRequest
|
||||||
|
|
||||||
from .. import puppet as pu, portal as po
|
from mautrix_telegram import puppet as pu, portal as po
|
||||||
from ..db import Message as DBMessage
|
from mautrix_telegram.db import Message as DBMessage
|
||||||
from ..types import TelegramID
|
from mautrix_telegram.types import TelegramID
|
||||||
from . import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_MISC,
|
@command_handler(help_section=SECTION_MISC,
|
||||||
@@ -71,14 +71,13 @@ async def search(evt: CommandEvent) -> Optional[Dict]:
|
|||||||
return await evt.reply("\n".join(reply))
|
return await evt.reply("\n".join(reply))
|
||||||
|
|
||||||
|
|
||||||
@command_handler(name="pm",
|
@command_handler(help_section=SECTION_CREATING_PORTALS,
|
||||||
help_section=SECTION_CREATING_PORTALS,
|
|
||||||
help_args="<_identifier_>",
|
help_args="<_identifier_>",
|
||||||
help_text="Open a private chat with the given Telegram user. The identifier is "
|
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. "
|
"either the internal user ID, the username or the phone number. "
|
||||||
"**N.B.** The phone numbers you start chats with must already be in "
|
"**N.B.** The phone numbers you start chats with must already be in "
|
||||||
"your contacts.")
|
"your contacts.")
|
||||||
async def private_message(evt: CommandEvent) -> Optional[Dict]:
|
async def pm(evt: CommandEvent) -> Optional[Dict]:
|
||||||
if len(evt.args) == 0:
|
if len(evt.args) == 0:
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp pm <user identifier>`")
|
return await evt.reply("**Usage:** `$cmdprefix+sp pm <user identifier>`")
|
||||||
|
|
||||||
@@ -209,7 +209,6 @@ class Config(DictWithRecursion):
|
|||||||
copy("bridge.inline_images")
|
copy("bridge.inline_images")
|
||||||
copy("bridge.plaintext_highlights")
|
copy("bridge.plaintext_highlights")
|
||||||
copy("bridge.public_portals")
|
copy("bridge.public_portals")
|
||||||
copy("bridge.native_stickers")
|
|
||||||
copy("bridge.catch_up")
|
copy("bridge.catch_up")
|
||||||
copy("bridge.sync_with_custom_puppets")
|
copy("bridge.sync_with_custom_puppets")
|
||||||
copy("bridge.telegram_link_preview")
|
copy("bridge.telegram_link_preview")
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Puppet(Base):
|
|||||||
return cls._select_one_or_none(cls.c.id == tgid)
|
return cls._select_one_or_none(cls.c.id == tgid)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_custom_mxid(cls, mxid: MatrixRoomID) -> Optional['Puppet']:
|
def get_by_custom_mxid(cls, mxid: MatrixUserID) -> Optional['Puppet']:
|
||||||
return cls._select_one_or_none(cls.c.custom_mxid == mxid)
|
return cls._select_one_or_none(cls.c.custom_mxid == mxid)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class User(Base):
|
|||||||
return cls._select_one_or_none(cls.c.tgid == tgid)
|
return cls._select_one_or_none(cls.c.tgid == tgid)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['User']:
|
def get_by_mxid(cls, mxid: MatrixUserID) -> Optional['User']:
|
||||||
return cls._select_one_or_none(cls.c.mxid == mxid)
|
return cls._select_one_or_none(cls.c.mxid == mxid)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -79,8 +79,9 @@ class User(Base):
|
|||||||
@contacts.setter
|
@contacts.setter
|
||||||
def contacts(self, puppets: Iterable[TelegramID]) -> None:
|
def contacts(self, puppets: Iterable[TelegramID]) -> None:
|
||||||
self.db.execute(Contact.t.delete().where(Contact.c.user == self.tgid))
|
self.db.execute(Contact.t.delete().where(Contact.c.user == self.tgid))
|
||||||
self.db.execute(Contact.t.insert(), [{"user": self.tgid, "contact": tgid}
|
if puppets:
|
||||||
for tgid in puppets])
|
self.db.execute(Contact.t.insert(), [{"user": self.tgid, "contact": tgid}
|
||||||
|
for tgid in puppets])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
|
def portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
|
||||||
@@ -92,12 +93,18 @@ class User(Base):
|
|||||||
@portals.setter
|
@portals.setter
|
||||||
def portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
|
def portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
|
||||||
self.db.execute(UserPortal.t.delete().where(UserPortal.c.user == self.tgid))
|
self.db.execute(UserPortal.t.delete().where(UserPortal.c.user == self.tgid))
|
||||||
self.db.execute(UserPortal.t.insert(),
|
if portals:
|
||||||
[{
|
self.db.execute(UserPortal.t.insert(),
|
||||||
"user": self.tgid,
|
[{
|
||||||
"portal": tgid,
|
"user": self.tgid,
|
||||||
"portal_receiver": tg_receiver
|
"portal": tgid,
|
||||||
} for tgid, tg_receiver in portals])
|
"portal_receiver": tg_receiver
|
||||||
|
} for tgid, tg_receiver in portals])
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
super().delete()
|
||||||
|
self.portals = None
|
||||||
|
self.contacts = None
|
||||||
|
|
||||||
|
|
||||||
class UserPortal(Base):
|
class UserPortal(Base):
|
||||||
|
|||||||
@@ -1409,7 +1409,7 @@ class Portal:
|
|||||||
"external_url": self.get_external_url(evt)
|
"external_url": self.get_external_url(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs["is_sticker"] and self.get_config("native_stickers"):
|
if attrs["is_sticker"]:
|
||||||
return await intent.send_sticker(**kwargs)
|
return await intent.send_sticker(**kwargs)
|
||||||
|
|
||||||
mime_type = info["mimetype"]
|
mime_type = info["mimetype"]
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from mautrix_appservice import AppService, IntentAPI, IntentError, MatrixRequest
|
|||||||
|
|
||||||
from .types import MatrixUserID, TelegramID
|
from .types import MatrixUserID, TelegramID
|
||||||
from .db import Puppet as DBPuppet
|
from .db import Puppet as DBPuppet
|
||||||
from .util import ignore_coro
|
|
||||||
from . import util
|
from . import util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -82,6 +81,7 @@ class Puppet:
|
|||||||
|
|
||||||
self.default_mxid_intent = self.az.intent.user(self.default_mxid)
|
self.default_mxid_intent = self.az.intent.user(self.default_mxid)
|
||||||
self.intent = self._fresh_intent() # type: IntentAPI
|
self.intent = self._fresh_intent() # type: IntentAPI
|
||||||
|
self.sync_task = None # type: Optional[asyncio.Future]
|
||||||
|
|
||||||
self.cache[id] = self
|
self.cache[id] = self
|
||||||
if self.custom_mxid:
|
if self.custom_mxid:
|
||||||
@@ -154,7 +154,7 @@ class Puppet:
|
|||||||
return PuppetError.OnlyLoginSelf
|
return PuppetError.OnlyLoginSelf
|
||||||
return PuppetError.InvalidAccessToken
|
return PuppetError.InvalidAccessToken
|
||||||
if config["bridge.sync_with_custom_puppets"]:
|
if config["bridge.sync_with_custom_puppets"]:
|
||||||
ignore_coro(asyncio.ensure_future(self.sync(), loop=self.loop))
|
self.sync_task = asyncio.ensure_future(self.sync(), loop=self.loop)
|
||||||
return PuppetError.Success
|
return PuppetError.Success
|
||||||
|
|
||||||
async def leave_rooms_with_default_user(self) -> None:
|
async def leave_rooms_with_default_user(self) -> None:
|
||||||
@@ -236,6 +236,8 @@ class Puppet:
|
|||||||
async def sync(self) -> None:
|
async def sync(self) -> None:
|
||||||
try:
|
try:
|
||||||
await self._sync()
|
await self._sync()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.log.info("Syncing cancelled")
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Fatal error syncing")
|
self.log.exception("Fatal error syncing")
|
||||||
|
|
||||||
|
|||||||
@@ -136,13 +136,13 @@ class User(AbstractUser):
|
|||||||
self.db_instance.update(tgid=self.tgid, tg_username=self.username, tg_phone=self.phone,
|
self.db_instance.update(tgid=self.tgid, tg_username=self.username, tg_phone=self.phone,
|
||||||
saved_contacts=self.saved_contacts)
|
saved_contacts=self.saved_contacts)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self, delete_db: bool = True) -> None:
|
||||||
try:
|
try:
|
||||||
del self.by_mxid[self.mxid]
|
del self.by_mxid[self.mxid]
|
||||||
del self.by_tgid[self.tgid]
|
del self.by_tgid[self.tgid]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
if self._db_instance:
|
if delete_db and self._db_instance:
|
||||||
self._db_instance.delete()
|
self._db_instance.delete()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -316,7 +316,7 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
async def needs_relaybot(self, portal: po.Portal) -> bool:
|
async def needs_relaybot(self, portal: po.Portal) -> bool:
|
||||||
return not await self.is_logged_in() or (
|
return not await self.is_logged_in() or (
|
||||||
self.is_bot and portal.tgid_full not in self.portals)
|
(portal.has_bot or self.bot) and portal.tgid_full not in self.portals)
|
||||||
|
|
||||||
def _hash_contacts(self) -> int:
|
def _hash_contacts(self) -> int:
|
||||||
acc = 0
|
acc = 0
|
||||||
@@ -328,7 +328,7 @@ class User(AbstractUser):
|
|||||||
response = await self.client(GetContactsRequest(hash=self._hash_contacts()))
|
response = await self.client(GetContactsRequest(hash=self._hash_contacts()))
|
||||||
if isinstance(response, ContactsNotModified):
|
if isinstance(response, ContactsNotModified):
|
||||||
return
|
return
|
||||||
self.log.debug("Updating contacts...")
|
self.log.debug(f"Updating contacts of {self.name}...")
|
||||||
self.contacts = []
|
self.contacts = []
|
||||||
self.saved_contacts = response.saved_count
|
self.saved_contacts = response.saved_count
|
||||||
for user in response.users:
|
for user in response.users:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import logging
|
|||||||
|
|
||||||
from telethon.errors import *
|
from telethon.errors import *
|
||||||
|
|
||||||
from ...commands.auth import enter_password
|
from ...commands.telegram.auth import enter_password
|
||||||
from ...util import format_duration, ignore_coro
|
from ...util import format_duration, ignore_coro
|
||||||
from ...puppet import Puppet, PuppetError
|
from ...puppet import Puppet, PuppetError
|
||||||
from ...user import User
|
from ...user import User
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from ...types import MatrixUserID, TelegramID
|
|||||||
from ...user import User
|
from ...user import User
|
||||||
from ...portal import Portal
|
from ...portal import Portal
|
||||||
from ...util import ignore_coro
|
from ...util import ignore_coro
|
||||||
from ...commands.portal import user_has_power_level, get_initial_state
|
from ...commands.portal.util import user_has_power_level, get_initial_state
|
||||||
from ..common import AuthAPI
|
from ..common import AuthAPI
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
Reference in New Issue
Block a user