diff --git a/alembic/versions/888275d58e57_add_double_puppet_base_url_to_puppet_.py b/alembic/versions/888275d58e57_add_double_puppet_base_url_to_puppet_.py new file mode 100644 index 00000000..8b49d7e7 --- /dev/null +++ b/alembic/versions/888275d58e57_add_double_puppet_base_url_to_puppet_.py @@ -0,0 +1,30 @@ +"""Add double puppet base URL to puppet table + +Revision ID: 888275d58e57 +Revises: a328bf4f0932 +Create Date: 2020-10-14 18:52:00.730666 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '888275d58e57' +down_revision = 'a328bf4f0932' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('puppet', schema=None) as batch_op: + batch_op.add_column(sa.Column('base_url', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('puppet', schema=None) as batch_op: + batch_op.drop_column('base_url') + # ### end Alembic commands ### diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 569d2ca2..a6741d9c 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -22,14 +22,21 @@ from mautrix.types import RoomID, EventID, MessageEventContent from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent, CommandHandler as BaseCommandHandler, CommandProcessor as BaseCommandProcessor, - CommandHandlerFunc, command_handler as base_command_handler) + CommandHandlerFunc, command_handler as base_command_handler, + HelpCacheKey as BaseHelpCacheKey) from ..util import format_duration -from .. import user as u, context as c +from .. import user as u, context as c, portal as po + + +class HelpCacheKey(BaseHelpCacheKey, NamedTuple): + is_management: bool + is_portal: bool + puppet_whitelisted: bool + matrix_puppet_whitelisted: bool + is_admin: bool + is_logged_in: bool -HelpCacheKey = NamedTuple('HelpCacheKey', - is_management=bool, is_portal=bool, puppet_whitelisted=bool, - matrix_puppet_whitelisted=bool, is_admin=bool, is_logged_in=bool) SECTION_AUTH = HelpSection("Authentication", 10, "") SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "") @@ -40,12 +47,13 @@ SECTION_ADMIN = HelpSection("Administration", 50, "") class CommandEvent(BaseCommandEvent): sender: u.User + portal: po.Portal def __init__(self, processor: 'CommandProcessor', room_id: RoomID, event_id: EventID, sender: u.User, command: str, args: List[str], content: MessageEventContent, - is_management: bool, is_portal: bool) -> None: + portal: Optional['po.Portal'], is_management: bool, has_bridge_bot: bool) -> None: super().__init__(processor, room_id, event_id, sender, command, args, content, - is_management, is_portal) + portal, is_management, has_bridge_bot) self.bridge = processor.bridge self.tgbot = processor.tgbot self.config = processor.config @@ -56,19 +64,16 @@ class CommandEvent(BaseCommandEvent): return self.sender.is_admin async def get_help_key(self) -> HelpCacheKey: - return HelpCacheKey(self.is_management, self.is_portal, self.sender.puppet_whitelisted, - self.sender.matrix_puppet_whitelisted, self.sender.is_admin, - await self.sender.is_logged_in()) + return HelpCacheKey(self.is_management, self.portal is not None, + self.sender.puppet_whitelisted, self.sender.matrix_puppet_whitelisted, + self.sender.is_admin, await self.sender.is_logged_in()) class CommandHandler(BaseCommandHandler): name: str - management_only: bool - needs_auth: bool needs_puppeting: bool needs_matrix_puppeting: bool - needs_admin: bool def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]], management_only: bool, name: str, help_text: str, help_args: str, @@ -79,25 +84,16 @@ class CommandHandler(BaseCommandHandler): needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin) async def get_permission_error(self, evt: CommandEvent) -> Optional[str]: - if self.management_only and not evt.is_management: - return (f"`{evt.command}` is a restricted command: " - "you may only run it in management rooms.") - elif self.needs_puppeting and not evt.sender.puppet_whitelisted: + if self.needs_puppeting and not evt.sender.puppet_whitelisted: return "This command requires puppeting privileges." elif self.needs_matrix_puppeting and not evt.sender.matrix_puppet_whitelisted: return "This command requires Matrix puppeting privileges." - elif self.needs_admin and not evt.sender.is_admin: - return "This command requires administrator privileges." - elif self.needs_auth and not await evt.sender.is_logged_in(): - return "This command requires you to be logged in." - return None + return await super().get_permission_error(evt) def has_permission(self, key: HelpCacheKey) -> bool: - return ((not self.management_only or key.is_management) and + return (super().has_permission(key) and (not self.needs_puppeting or key.puppet_whitelisted) and - (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted) and - (not self.needs_admin or key.is_admin) and - (not self.needs_auth or key.is_logged_in)) + (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted)) def command_handler(_func: Optional[CommandHandlerFunc] = None, *, needs_auth: bool = True, @@ -117,10 +113,7 @@ class CommandProcessor(BaseCommandProcessor): def __init__(self, context: c.Context) -> None: super().__init__(event_class=CommandEvent, bridge=context.bridge) self.tgbot = context.bot - self.bridge = context.bridge - self.az, self.config, self.loop, self.tgbot = context.core self.public_website = context.public_website - self.command_prefix = self.config["bridge.command_prefix"] @staticmethod async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent diff --git a/mautrix_telegram/commands/portal/admin.py b/mautrix_telegram/commands/portal/admin.py index 820f95b1..1020f101 100644 --- a/mautrix_telegram/commands/portal/admin.py +++ b/mautrix_telegram/commands/portal/admin.py @@ -15,34 +15,12 @@ # along with this program. If not, see . import asyncio -from mautrix.errors import MatrixRequestError from mautrix.types import EventID 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) -> EventID: - try: - level = int(evt.args[0]) - except (KeyError, IndexError): - return await evt.reply("**Usage:** `$cmdprefix+sp set-pl [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: - return 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.") - - @command_handler(needs_admin=True, needs_auth=False, help_section=SECTION_ADMIN, help_args="<`portal`|`puppet`|`user`>", diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index a66819bf..1aa31b92 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -105,7 +105,14 @@ class Config(BaseBridgeConfig): copy("bridge.public_portals") copy("bridge.sync_with_custom_puppets") copy("bridge.sync_direct_chat_list") - copy("bridge.login_shared_secret") + copy("bridge.double_puppet_server_map") + copy("bridge.double_puppet_allow_discovery") + if "bridge.login_shared_secret" in self: + base["bridge.login_shared_secret_map"] = { + base["homeserver.domain"]: self["bridge.login_shared_secret"] + } + else: + copy("bridge.login_shared_secret_map") copy("bridge.telegram_link_preview") copy("bridge.inline_images") copy("bridge.image_as_file_size") diff --git a/mautrix_telegram/db/puppet.py b/mautrix_telegram/db/puppet.py index a93c3acb..3c258fdc 100644 --- a/mautrix_telegram/db/puppet.py +++ b/mautrix_telegram/db/puppet.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from typing import Optional, Iterable -from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy import Column, Integer, String, Text, Boolean from sqlalchemy.sql import expression, func from mautrix.types import UserID, SyncToken @@ -31,6 +31,7 @@ class Puppet(Base): custom_mxid: UserID = Column(String, nullable=True) access_token: str = Column(String, nullable=True) next_batch: SyncToken = Column(String, nullable=True) + base_url: str = Column(Text, nullable=True) displayname: str = Column(String, nullable=True) displayname_source: TelegramID = Column(Integer, nullable=True) username: str = Column(String, nullable=True) diff --git a/mautrix_telegram/example-config.yaml b/mautrix_telegram/example-config.yaml index 04135408..13ce8b95 100644 --- a/mautrix_telegram/example-config.yaml +++ b/mautrix_telegram/example-config.yaml @@ -173,12 +173,19 @@ bridge: # Note that updating the m.direct event is not atomic (except with mautrix-asmux) # and is therefore prone to race conditions. sync_direct_chat_list: false - # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth + # Servers to always allow double puppeting from + double_puppet_server_map: + example.com: https://example.com + # Allow using double puppeting from any server with a valid client .well-known file. + double_puppet_allow_discovery: false + # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # # If set, custom puppets will be enabled automatically for local users # instead of users having to find an access token and run `login-matrix` # manually. - login_shared_secret: null + # If using this for other servers than the bridge's server, + # you must also set the URL in the double_puppet_server_map. + login_shared_secret_map: {} # Set to false to disable link previews in messages sent to Telegram. telegram_link_preview: true # Use inline images instead of a separate message for the caption. diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py index cc6e5cf0..449e3ff9 100644 --- a/mautrix_telegram/puppet.py +++ b/mautrix_telegram/puppet.py @@ -21,6 +21,7 @@ import logging from telethon.tl.types import (UserProfilePhoto, User, UpdateUserName, PeerUser, TypeInputPeer, InputPeerPhotoFileLocation, UserProfilePhotoEmpty, TypeInputUser) +from yarl import URL from mautrix.appservice import AppService, IntentAPI from mautrix.errors import MatrixRequestError @@ -57,6 +58,7 @@ class Puppet(BasePuppet): access_token: Optional[str] custom_mxid: Optional[UserID] _next_batch: Optional[SyncToken] + base_url: Optional[URL] default_mxid: UserID username: Optional[str] @@ -79,6 +81,7 @@ class Puppet(BasePuppet): access_token: Optional[str] = None, custom_mxid: Optional[UserID] = None, next_batch: Optional[SyncToken] = None, + base_url: Optional[str] = None, username: Optional[str] = None, displayname: Optional[str] = None, displayname_source: Optional[TelegramID] = None, @@ -91,6 +94,7 @@ class Puppet(BasePuppet): self.access_token = access_token self.custom_mxid = custom_mxid self._next_batch = next_batch + self.base_url = URL(base_url) if base_url else None self.default_mxid = self.get_mxid_from_id(self.id) self.username = username @@ -161,7 +165,7 @@ class Puppet(BasePuppet): custom_mxid=self.custom_mxid, username=self.username, is_bot=self.is_bot, displayname=self.displayname, displayname_source=self.displayname_source, photo_id=self.photo_id, matrix_registered=self.is_registered, - disable_updates=self.disable_updates) + disable_updates=self.disable_updates, base_url=self.base_url) def new_db_instance(self) -> DBPuppet: return DBPuppet(id=self.id, **self._fields) @@ -172,9 +176,9 @@ class Puppet(BasePuppet): @classmethod def from_db(cls, db_puppet: DBPuppet) -> 'Puppet': return Puppet(db_puppet.id, db_puppet.access_token, db_puppet.custom_mxid, - db_puppet.next_batch, db_puppet.username, db_puppet.displayname, - db_puppet.displayname_source, db_puppet.photo_id, db_puppet.is_bot, - db_puppet.matrix_registered, db_puppet.disable_updates, + db_puppet.next_batch, db_puppet.base_url, db_puppet.username, + db_puppet.displayname, db_puppet.displayname_source, db_puppet.photo_id, + db_puppet.is_bot, db_puppet.matrix_registered, db_puppet.disable_updates, db_instance=db_puppet) # endregion @@ -446,8 +450,12 @@ def init(context: 'Context') -> Iterable[Awaitable[Any]]: Puppet.displayname_template = SimpleTemplate(config["bridge.displayname_template"], "displayname") - secret = config["bridge.login_shared_secret"] - Puppet.login_shared_secret = secret.encode("utf-8") if secret else None + Puppet.sync_with_custom_puppets = config["bridge.sync_with_custom_puppets"] + Puppet.homeserver_url_map = {server: URL(url) for server, url + in config["bridge.double_puppet_server_map"].items()} + Puppet.allow_discover_url = config["bridge.double_puppet_allow_discovery"] + Puppet.login_shared_secret_map = {server: secret.encode("utf-8") for server, secret + in config["bridge.login_shared_secret_map"].items()} Puppet.login_device_name = "Telegram Bridge" return (puppet.try_start() for puppet in Puppet.all_with_custom_mxid()) diff --git a/requirements.txt b/requirements.txt index efef0c54..f88635ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,8 @@ alembic>=1,<2 ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 -aiohttp>=3,<4 -mautrix>=0.7.13,<0.8 +aiohttp>=3,<3.7 +yarl<1.6 +mautrix==0.8.0.beta3 telethon>=1.16,<1.17 telethon-session-sqlalchemy>=0.2.14,<0.3