Update to mautrix-python 0.8.0.beta3

* Cross-server double puppeting is now possible
* End-to-bridge encryption no longer requires login_shared_secret,
  but the homeserver must support MSC2778 (Synapse 1.21+)
This commit is contained in:
Tulir Asokan
2020-10-14 18:56:23 +03:00
parent fdc58ce450
commit 524f60ab48
8 changed files with 88 additions and 63 deletions
@@ -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 ###
+22 -29
View File
@@ -22,14 +22,21 @@ from mautrix.types import RoomID, EventID, MessageEventContent
from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent, from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent,
CommandHandler as BaseCommandHandler, CommandHandler as BaseCommandHandler,
CommandProcessor as BaseCommandProcessor, 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 ..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_AUTH = HelpSection("Authentication", 10, "")
SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "") SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "")
@@ -40,12 +47,13 @@ SECTION_ADMIN = HelpSection("Administration", 50, "")
class CommandEvent(BaseCommandEvent): class CommandEvent(BaseCommandEvent):
sender: u.User sender: u.User
portal: po.Portal
def __init__(self, processor: 'CommandProcessor', room_id: RoomID, event_id: EventID, def __init__(self, processor: 'CommandProcessor', room_id: RoomID, event_id: EventID,
sender: u.User, command: str, args: List[str], content: MessageEventContent, 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, 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.bridge = processor.bridge
self.tgbot = processor.tgbot self.tgbot = processor.tgbot
self.config = processor.config self.config = processor.config
@@ -56,19 +64,16 @@ class CommandEvent(BaseCommandEvent):
return self.sender.is_admin return self.sender.is_admin
async def get_help_key(self) -> HelpCacheKey: async def get_help_key(self) -> HelpCacheKey:
return HelpCacheKey(self.is_management, self.is_portal, self.sender.puppet_whitelisted, return HelpCacheKey(self.is_management, self.portal is not None,
self.sender.matrix_puppet_whitelisted, self.sender.is_admin, self.sender.puppet_whitelisted, self.sender.matrix_puppet_whitelisted,
await self.sender.is_logged_in()) self.sender.is_admin, await self.sender.is_logged_in())
class CommandHandler(BaseCommandHandler): class CommandHandler(BaseCommandHandler):
name: str name: str
management_only: bool
needs_auth: bool
needs_puppeting: bool needs_puppeting: bool
needs_matrix_puppeting: bool needs_matrix_puppeting: bool
needs_admin: bool
def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]], def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]],
management_only: bool, name: str, help_text: str, help_args: str, 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) needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin)
async def get_permission_error(self, evt: CommandEvent) -> Optional[str]: async def get_permission_error(self, evt: CommandEvent) -> Optional[str]:
if self.management_only and not evt.is_management: if self.needs_puppeting and not evt.sender.puppet_whitelisted:
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:
return "This command requires puppeting privileges." return "This command requires puppeting privileges."
elif self.needs_matrix_puppeting and not evt.sender.matrix_puppet_whitelisted: elif self.needs_matrix_puppeting and not evt.sender.matrix_puppet_whitelisted:
return "This command requires Matrix puppeting privileges." return "This command requires Matrix puppeting privileges."
elif self.needs_admin and not evt.sender.is_admin: return await super().get_permission_error(evt)
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
def has_permission(self, key: HelpCacheKey) -> bool: 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_puppeting or key.puppet_whitelisted) and
(not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted) and (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted))
(not self.needs_admin or key.is_admin) and
(not self.needs_auth or key.is_logged_in))
def command_handler(_func: Optional[CommandHandlerFunc] = None, *, needs_auth: bool = True, 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: def __init__(self, context: c.Context) -> None:
super().__init__(event_class=CommandEvent, bridge=context.bridge) super().__init__(event_class=CommandEvent, bridge=context.bridge)
self.tgbot = context.bot 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.public_website = context.public_website
self.command_prefix = self.config["bridge.command_prefix"]
@staticmethod @staticmethod
async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
-22
View File
@@ -15,34 +15,12 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import asyncio import asyncio
from mautrix.errors import MatrixRequestError
from mautrix.types import EventID from mautrix.types import EventID
from ... import portal as po, puppet as pu, user as u from ... import portal as po, puppet as pu, user as u
from .. import command_handler, CommandEvent, SECTION_ADMIN 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 <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:
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, @command_handler(needs_admin=True, needs_auth=False,
help_section=SECTION_ADMIN, help_section=SECTION_ADMIN,
help_args="<`portal`|`puppet`|`user`>", help_args="<`portal`|`puppet`|`user`>",
+8 -1
View File
@@ -105,7 +105,14 @@ class Config(BaseBridgeConfig):
copy("bridge.public_portals") copy("bridge.public_portals")
copy("bridge.sync_with_custom_puppets") copy("bridge.sync_with_custom_puppets")
copy("bridge.sync_direct_chat_list") 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.telegram_link_preview")
copy("bridge.inline_images") copy("bridge.inline_images")
copy("bridge.image_as_file_size") copy("bridge.image_as_file_size")
+2 -1
View File
@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Iterable 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 sqlalchemy.sql import expression, func
from mautrix.types import UserID, SyncToken from mautrix.types import UserID, SyncToken
@@ -31,6 +31,7 @@ class Puppet(Base):
custom_mxid: UserID = Column(String, nullable=True) custom_mxid: UserID = Column(String, nullable=True)
access_token: str = Column(String, nullable=True) access_token: str = Column(String, nullable=True)
next_batch: SyncToken = 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: str = Column(String, nullable=True)
displayname_source: TelegramID = Column(Integer, nullable=True) displayname_source: TelegramID = Column(Integer, nullable=True)
username: str = Column(String, nullable=True) username: str = Column(String, nullable=True)
+9 -2
View File
@@ -173,12 +173,19 @@ bridge:
# Note that updating the m.direct event is not atomic (except with mautrix-asmux) # Note that updating the m.direct event is not atomic (except with mautrix-asmux)
# and is therefore prone to race conditions. # and is therefore prone to race conditions.
sync_direct_chat_list: false 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 # If set, custom puppets will be enabled automatically for local users
# instead of users having to find an access token and run `login-matrix` # instead of users having to find an access token and run `login-matrix`
# manually. # 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. # 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. # Use inline images instead of a separate message for the caption.
+14 -6
View File
@@ -21,6 +21,7 @@ import logging
from telethon.tl.types import (UserProfilePhoto, User, UpdateUserName, PeerUser, TypeInputPeer, from telethon.tl.types import (UserProfilePhoto, User, UpdateUserName, PeerUser, TypeInputPeer,
InputPeerPhotoFileLocation, UserProfilePhotoEmpty, TypeInputUser) InputPeerPhotoFileLocation, UserProfilePhotoEmpty, TypeInputUser)
from yarl import URL
from mautrix.appservice import AppService, IntentAPI from mautrix.appservice import AppService, IntentAPI
from mautrix.errors import MatrixRequestError from mautrix.errors import MatrixRequestError
@@ -57,6 +58,7 @@ class Puppet(BasePuppet):
access_token: Optional[str] access_token: Optional[str]
custom_mxid: Optional[UserID] custom_mxid: Optional[UserID]
_next_batch: Optional[SyncToken] _next_batch: Optional[SyncToken]
base_url: Optional[URL]
default_mxid: UserID default_mxid: UserID
username: Optional[str] username: Optional[str]
@@ -79,6 +81,7 @@ class Puppet(BasePuppet):
access_token: Optional[str] = None, access_token: Optional[str] = None,
custom_mxid: Optional[UserID] = None, custom_mxid: Optional[UserID] = None,
next_batch: Optional[SyncToken] = None, next_batch: Optional[SyncToken] = None,
base_url: Optional[str] = None,
username: Optional[str] = None, username: Optional[str] = None,
displayname: Optional[str] = None, displayname: Optional[str] = None,
displayname_source: Optional[TelegramID] = None, displayname_source: Optional[TelegramID] = None,
@@ -91,6 +94,7 @@ class Puppet(BasePuppet):
self.access_token = access_token self.access_token = access_token
self.custom_mxid = custom_mxid self.custom_mxid = custom_mxid
self._next_batch = next_batch 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.default_mxid = self.get_mxid_from_id(self.id)
self.username = username self.username = username
@@ -161,7 +165,7 @@ class Puppet(BasePuppet):
custom_mxid=self.custom_mxid, username=self.username, is_bot=self.is_bot, custom_mxid=self.custom_mxid, username=self.username, is_bot=self.is_bot,
displayname=self.displayname, displayname_source=self.displayname_source, displayname=self.displayname, displayname_source=self.displayname_source,
photo_id=self.photo_id, matrix_registered=self.is_registered, 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: def new_db_instance(self) -> DBPuppet:
return DBPuppet(id=self.id, **self._fields) return DBPuppet(id=self.id, **self._fields)
@@ -172,9 +176,9 @@ class Puppet(BasePuppet):
@classmethod @classmethod
def from_db(cls, db_puppet: DBPuppet) -> 'Puppet': def from_db(cls, db_puppet: DBPuppet) -> 'Puppet':
return Puppet(db_puppet.id, db_puppet.access_token, db_puppet.custom_mxid, 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.next_batch, db_puppet.base_url, db_puppet.username,
db_puppet.displayname_source, db_puppet.photo_id, db_puppet.is_bot, db_puppet.displayname, db_puppet.displayname_source, db_puppet.photo_id,
db_puppet.matrix_registered, db_puppet.disable_updates, db_puppet.is_bot, db_puppet.matrix_registered, db_puppet.disable_updates,
db_instance=db_puppet) db_instance=db_puppet)
# endregion # endregion
@@ -446,8 +450,12 @@ def init(context: 'Context') -> Iterable[Awaitable[Any]]:
Puppet.displayname_template = SimpleTemplate(config["bridge.displayname_template"], Puppet.displayname_template = SimpleTemplate(config["bridge.displayname_template"],
"displayname") "displayname")
secret = config["bridge.login_shared_secret"] Puppet.sync_with_custom_puppets = config["bridge.sync_with_custom_puppets"]
Puppet.login_shared_secret = secret.encode("utf-8") if secret else None 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" Puppet.login_device_name = "Telegram Bridge"
return (puppet.try_start() for puppet in Puppet.all_with_custom_mxid()) return (puppet.try_start() for puppet in Puppet.all_with_custom_mxid())
+3 -2
View File
@@ -3,7 +3,8 @@ alembic>=1,<2
ruamel.yaml>=0.15.35,<0.17 ruamel.yaml>=0.15.35,<0.17
python-magic>=0.4,<0.5 python-magic>=0.4,<0.5
commonmark>=0.8,<0.10 commonmark>=0.8,<0.10
aiohttp>=3,<4 aiohttp>=3,<3.7
mautrix>=0.7.13,<0.8 yarl<1.6
mautrix==0.8.0.beta3
telethon>=1.16,<1.17 telethon>=1.16,<1.17
telethon-session-sqlalchemy>=0.2.14,<0.3 telethon-session-sqlalchemy>=0.2.14,<0.3