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