From c79d4421588ba428120600e863a92c9ccfd0d915 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Mar 2020 22:01:23 +0200 Subject: [PATCH 01/14] Add initial Matrix end-to-bridge encryption support --- ...fc8a72b_add_encrypted_field_for_portals.py | 27 +++++ mautrix_telegram/config.py | 2 + mautrix_telegram/db/portal.py | 3 +- mautrix_telegram/e2ee.py | 112 ++++++++++++++++++ mautrix_telegram/example-config.yaml | 11 ++ mautrix_telegram/matrix.py | 30 ++++- mautrix_telegram/portal/base.py | 20 ++-- mautrix_telegram/portal/metadata.py | 2 +- mautrix_telegram/portal/telegram.py | 30 +++-- optional-requirements.txt | 3 + requirements.txt | 2 +- 11 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 alembic/versions/24f31fc8a72b_add_encrypted_field_for_portals.py create mode 100644 mautrix_telegram/e2ee.py diff --git a/alembic/versions/24f31fc8a72b_add_encrypted_field_for_portals.py b/alembic/versions/24f31fc8a72b_add_encrypted_field_for_portals.py new file mode 100644 index 00000000..8cd1ba59 --- /dev/null +++ b/alembic/versions/24f31fc8a72b_add_encrypted_field_for_portals.py @@ -0,0 +1,27 @@ +"""Add encrypted field for portals + +Revision ID: 24f31fc8a72b +Revises: a7c04a56041b +Create Date: 2020-03-28 20:14:29.046699 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "24f31fc8a72b" +down_revision = "a7c04a56041b" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("portal") as batch_op: + batch_op.add_column(sa.Column("encrypted", sa.Boolean(), nullable=False, + server_default=sa.sql.expression.false())) + + +def downgrade(): + with op.batch_alter_table("portal") as batch_op: + batch_op.drop_column("encrypted") diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index a1ca0182..904f3a43 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -118,6 +118,8 @@ class Config(BaseBridgeConfig): copy("bridge.federate_rooms") copy("bridge.animated_sticker.target") copy("bridge.animated_sticker.args") + copy("bridge.encryption.allow") + copy("bridge.encryption.default") copy("bridge.initial_power_level_overrides.group") copy("bridge.initial_power_level_overrides.user") diff --git a/mautrix_telegram/db/portal.py b/mautrix_telegram/db/portal.py index bae8c76a..8112e9ad 100644 --- a/mautrix_telegram/db/portal.py +++ b/mautrix_telegram/db/portal.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from typing import Optional -from sqlalchemy import Column, Integer, String, Boolean, Text, func +from sqlalchemy import Column, Integer, String, Boolean, Text, func, sql from mautrix.types import RoomID from mautrix.util.db import Base @@ -34,6 +34,7 @@ class Portal(Base): # Matrix portal information mxid: RoomID = Column(String, unique=True, nullable=True) + encrypted: bool = Column(Boolean, nullable=False, server_default=sql.expression.false()) config: str = Column(Text, nullable=True) diff --git a/mautrix_telegram/e2ee.py b/mautrix_telegram/e2ee.py new file mode 100644 index 00000000..cb9d6895 --- /dev/null +++ b/mautrix_telegram/e2ee.py @@ -0,0 +1,112 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2019 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 . +from typing import Tuple, Union +import logging +import asyncio +import hashlib +import hmac + +from nio import AsyncClient, Event as NioEvent, GroupEncryptionError, LoginError + +from mautrix.appservice import AppService +from mautrix.types import (Filter, RoomFilter, EventFilter, RoomEventFilter, StateFilter, + EventType, RoomID, Serializable, JSON, MessageEvent, Event) + +from .context import Context + + +class EncryptionManager: + loop: asyncio.AbstractEventLoop + log: logging.Logger = logging.getLogger("mau.e2ee") + client: AsyncClient + az: AppService + + login_shared_secret: bytes + + sync_task: asyncio.Task + + def __init__(self, context: 'Context') -> None: + self.loop = context.loop + self.az = context.az + self.config = context.config + lss: str = self.config["bridge.login_shared_secret"] + if not lss: + raise ValueError("login_shared_secret must be set to enable encryption") + self.login_shared_secret = lss.encode("utf-8") + self.client = AsyncClient(homeserver=self.config["homeserver.address"], + user=self.az.bot_mxid, device_id="Telegram bridge", + store_path="nio_store") + + async def encrypt(self, room_id: RoomID, event_type: EventType, + content: Union[Serializable, JSON]) -> Tuple[EventType, JSON]: + serialized = content.serialize() if isinstance(content, Serializable) else content + type_str = str(event_type) + retries = 0 + while True: + try: + type_str, encrypted = self.client.encrypt(room_id, type_str, serialized) + break + except GroupEncryptionError: + if retries > 3: + self.log.error("Got GroupEncryptionError again, giving up") + raise + retries += 1 + self.log.debug("Got GroupEncryptionError, sharing group session and trying again") + await self.client.share_group_session(room_id, ignore_unverified_devices=True) + event_type = EventType.find(type_str) + try: + encrypted["m.relates_to"] = serialized["m.relates_to"] + except KeyError: + pass + return event_type, encrypted + + def decrypt(self, event: MessageEvent) -> MessageEvent: + serialized = event.serialize() + event = self.client.decrypt_event(NioEvent.parse_encrypted_event(serialized)) + try: + event.source["content"]["m.relates_to"] = serialized["content"]["m.relates_to"] + except KeyError: + pass + return Event.deserialize(event.source) + + async def start(self) -> None: + self.log.debug("Logging in with bridge bot user") + password = hmac.new(self.login_shared_secret, self.az.bot_mxid.encode("utf-8"), + hashlib.sha512).hexdigest() + resp = await self.client.login(password, device_name="Telegram bridge") + if isinstance(resp, LoginError): + raise resp + self.sync_task = self.loop.create_task(self.client.sync_forever( + timeout=30000, sync_filter=self._filter.serialize())) + self.log.info("End-to-bridge encryption support is enabled") + + def stop(self) -> None: + self.sync_task.cancel() + + @property + def _filter(self) -> Filter: + all_events = EventType.find("*") + return Filter( + account_data=EventFilter(types=[all_events]), + presence=EventFilter(not_types=[all_events]), + room=RoomFilter( + include_leave=False, + state=StateFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), + timeline=RoomEventFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), + account_data=RoomEventFilter(not_types=[all_events]), + ephemeral=RoomEventFilter(not_types=[all_events]), + ), + ) diff --git a/mautrix_telegram/example-config.yaml b/mautrix_telegram/example-config.yaml index 4ab42797..26c59793 100644 --- a/mautrix_telegram/example-config.yaml +++ b/mautrix_telegram/example-config.yaml @@ -191,6 +191,17 @@ bridge: height: 256 background: "020202" # only for gif fps: 30 # only for webm + # End-to-bridge encryption support options. These require matrix-nio to be installed with pip + # and login_shared_secret to be configured in order to get a device for the bridge bot. + # + # Additionally, https://github.com/matrix-org/synapse/pull/5758 is required if using a normal + # application service. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + default: false # Overrides for base power levels. initial_power_level_overrides: diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 27d6f164..3163e679 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -13,14 +13,14 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Set, Tuple, Union, Iterable, TYPE_CHECKING +from typing import Dict, Set, Tuple, Union, Iterable, Optional, TYPE_CHECKING from mautrix.bridge import BaseMatrixHandler from mautrix.types import (Event, EventType, RoomID, UserID, EventID, ReceiptEvent, ReceiptType, ReceiptEventContent, PresenceEvent, PresenceState, TypingEvent, MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent, RoomAvatarStateEventContent, RoomTopicStateEventContent, - MemberStateEventContent) + MemberStateEventContent, EncryptedEvent) from mautrix.errors import MatrixError from . import user as u, portal as po, puppet as pu, commands as com @@ -37,6 +37,11 @@ except ImportError: Histogram = None EVENT_TIME = None +try: + from .e2ee import EncryptionManager +except ImportError: + EncryptionManager = None + RoomMetaStateEventContent = Union[RoomNameStateEventContent, RoomAvatarStateEventContent, RoomTopicStateEventContent] @@ -44,14 +49,26 @@ RoomMetaStateEventContent = Union[RoomNameStateEventContent, RoomAvatarStateEven class MatrixHandler(BaseMatrixHandler): bot: 'Bot' commands: 'com.CommandProcessor' + e2ee: Optional[EncryptionManager] previously_typing: Dict[RoomID, Set[UserID]] def __init__(self, context: 'Context') -> None: super(MatrixHandler, self).__init__(context.az, context.config, loop=context.loop, command_processor=com.CommandProcessor(context)) + self.e2ee = None + if self.config["bridge.encryption.allow"]: + if EncryptionManager: + self.e2ee = EncryptionManager(context) + else: + self.log.warning("Encryption enabled in config, but dependencies not installed.") self.bot = context.bot self.previously_typing = {} + async def init_as_bot(self) -> None: + await super().init_as_bot() + if self.e2ee: + await self.e2ee.start() + async def get_user(self, user_id: UserID) -> 'u.User': return await u.User.get_by_mxid(user_id).ensure_started() @@ -355,7 +372,7 @@ class MatrixHandler(BaseMatrixHandler): self.previously_typing[room_id] = now_typing def filter_matrix_event(self, evt: Event) -> bool: - if not isinstance(evt, (RedactionEvent, MessageEvent, StateEvent)): + if not isinstance(evt, (RedactionEvent, MessageEvent, StateEvent, EncryptedEvent)): return True return evt.sender and (evt.sender == self.az.bot_mxid or pu.Puppet.get_id_from_mxid(evt.sender) is not None) @@ -372,6 +389,8 @@ class MatrixHandler(BaseMatrixHandler): async def handle_event(self, evt: Event) -> None: if evt.type == EventType.ROOM_REDACTION: await self.handle_redaction(evt) + elif evt.type == EventType.ROOM_ENCRYPTED and self.e2ee: + await self.int_handle_event(self.e2ee.decrypt(evt)) async def handle_state_event(self, evt: StateEvent) -> None: if evt.type == EventType.ROOM_POWER_LEVELS: @@ -387,6 +406,11 @@ class MatrixHandler(BaseMatrixHandler): await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events) elif evt.type == EventType.ROOM_TOMBSTONE: await self.handle_room_upgrade(evt.room_id, evt.sender, evt.content.replacement_room) + elif evt.type == EventType.ROOM_ENCRYPTION: + portal = po.Portal.get_by_mxid(evt.room_id) + if portal: + portal.encrypted = True + portal.save() async def log_event_handle_duration(self, evt: Event, duration: float) -> None: if EVENT_TIME: diff --git a/mautrix_telegram/portal/base.py b/mautrix_telegram/portal/base.py index c1332b0e..1d2d6d71 100644 --- a/mautrix_telegram/portal/base.py +++ b/mautrix_telegram/portal/base.py @@ -44,6 +44,7 @@ if TYPE_CHECKING: from ..bot import Bot from ..abstract_user import AbstractUser from ..config import Config + from ..matrix import MatrixHandler from . import Portal TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant] @@ -58,6 +59,7 @@ class BasePortal(ABC): az: AppService = None bot: 'Bot' = None loop: asyncio.AbstractEventLoop = None + matrix: 'MatrixHandler' = None # Config cache filter_mode: str = None @@ -85,6 +87,7 @@ class BasePortal(ABC): about: Optional[str] photo_id: Optional[str] local_config: Dict[str, Any] + encrypted: bool deleted: bool backfilling: bool backfill_leave: Optional[Set[IntentAPI]] @@ -102,7 +105,8 @@ class BasePortal(ABC): mxid: Optional[RoomID] = None, username: Optional[str] = None, megagroup: Optional[bool] = False, title: Optional[str] = None, about: Optional[str] = None, photo_id: Optional[str] = None, - local_config: Optional[str] = None, db_instance: DBPortal = None) -> None: + local_config: Optional[str] = None, encrypted: Optional[bool] = False, + db_instance: DBPortal = None) -> None: self.mxid = mxid self.tgid = tgid self.tg_receiver = tg_receiver or tgid @@ -113,6 +117,7 @@ class BasePortal(ABC): self.about = about self.photo_id = photo_id self.local_config = json.loads(local_config or "{}") + self.encrypted = encrypted self._db_instance = db_instance self._main_intent = None self.deleted = False @@ -328,12 +333,12 @@ class BasePortal(ABC): return DBPortal(tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type, mxid=self.mxid, username=self.username, megagroup=self.megagroup, title=self.title, about=self.about, photo_id=self.photo_id, - config=json.dumps(self.local_config)) + config=json.dumps(self.local_config), encrypted=self.encrypted) def save(self) -> None: self.db_instance.edit(mxid=self.mxid, username=self.username, title=self.title, about=self.about, photo_id=self.photo_id, megagroup=self.megagroup, - config=json.dumps(self.local_config)) + config=json.dumps(self.local_config), encrypted=self.encrypted) def delete(self) -> None: try: @@ -352,10 +357,10 @@ class BasePortal(ABC): @classmethod def from_db(cls, db_portal: DBPortal) -> 'Portal': return cls(tgid=db_portal.tgid, tg_receiver=db_portal.tg_receiver, - peer_type=db_portal.peer_type, mxid=db_portal.mxid, - username=db_portal.username, megagroup=db_portal.megagroup, - title=db_portal.title, about=db_portal.about, photo_id=db_portal.photo_id, - local_config=db_portal.config, db_instance=db_portal) + peer_type=db_portal.peer_type, mxid=db_portal.mxid, username=db_portal.username, + megagroup=db_portal.megagroup, title=db_portal.title, about=db_portal.about, + photo_id=db_portal.photo_id, local_config=db_portal.config, + encrypted=db_portal.encrypted, db_instance=db_portal) # endregion # region Class instance lookup @@ -506,6 +511,7 @@ class BasePortal(ABC): def init(context: Context) -> None: global config BasePortal.az, config, BasePortal.loop, BasePortal.bot = context.core + BasePortal.matrix = context.mx BasePortal.max_initial_member_sync = config["bridge.max_initial_member_sync"] BasePortal.sync_channel_members = config["bridge.sync_channel_members"] BasePortal.sync_matrix_state = config["bridge.sync_matrix_state"] diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 7c6ef837..4d4d491f 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -362,7 +362,7 @@ class PortalMetadata(BasePortal, ABC): levels.kick = overrides.get("kick", 50) levels.redact = overrides.get("redact", 50) levels.invite = overrides.get("invite", 50 if dbr.invite_users else 0) - levels.events[EventType.ROOM_ENCRYPTED] = 99 + levels.events[EventType.ROOM_ENCRYPTION] = 99 levels.events[EventType.ROOM_TOMBSTONE] = 99 levels.events[EventType.ROOM_NAME] = 50 if dbr.change_info else 0 levels.events[EventType.ROOM_AVATAR] = 50 if dbr.change_info else 0 diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index 35c992bb..61f655fd 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -38,7 +38,7 @@ from telethon.tl.types import ( from mautrix.appservice import IntentAPI from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType, EventType, MediaMessageEventContent, TextMessageEventContent, - LocationMessageEventContent, Format) + LocationMessageEventContent, Format, MessageEventContent) from ..types import TelegramID from ..db import Message as DBMessage, TelegramFile as DBTelegramFile @@ -71,6 +71,12 @@ class PortalTelegram(BasePortal, ABC): return f"https://t.me/c/{self.tgid}/{evt.id}" return None + async def _send_message(self, intent: IntentAPI, content: MessageEventContent, + event_type: EventType = EventType.ROOM_MESSAGE, **kwargs) -> EventID: + if self.encrypted and self.matrix.e2ee: + event_type, content = await self.matrix.e2ee.encrypt(self.mxid, event_type, content) + return await intent.send_message_event(self.mxid, event_type, content, **kwargs) + async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: Dict = None) -> Optional[EventID]: loc, largest_size = self._get_largest_photo_size(evt.media.photo) @@ -85,7 +91,7 @@ class PortalTelegram(BasePortal, ABC): prefix_text="Inline image: ") content.external_url = self._get_external_url(evt) await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_message(self.mxid, content, timestamp=evt.date) + return await self._send_message(intent, content, timestamp=evt.date) info = ImageInfo( height=largest_size.h, width=largest_size.w, orientation=0, mimetype=file.mime_type, size=(len(largest_size.bytes) if (isinstance(largest_size, PhotoCachedSize)) @@ -95,12 +101,12 @@ class PortalTelegram(BasePortal, ABC): content = MediaMessageEventContent(url=file.mxc, msgtype=MessageType.IMAGE, info=info, body=name, relates_to=relates_to, external_url=self._get_external_url(evt)) - result = await intent.send_message(self.mxid, content, timestamp=evt.date) + result = await self._send_message(intent, content, timestamp=evt.date) if evt.message: caption_content = await formatter.telegram_to_matrix(evt, source, self.main_intent, no_reply_fallback=True) caption_content.external_url = content.external_url - result = await intent.send_message(self.mxid, caption_content, timestamp=evt.date) + result = await self._send_message(intent, caption_content, timestamp=evt.date) return result @staticmethod @@ -168,6 +174,7 @@ class PortalTelegram(BasePortal, ABC): if document.size > config["bridge.max_document_size"] * 1000 ** 2: name = attrs.name or "" caption = f"\n{evt.message}" if evt.message else "" + # TODO encrypt return await intent.send_notice(self.mxid, f"Too large file {name}{caption}") thumb_loc, thumb_size = self._get_largest_photo_size(document) @@ -199,7 +206,7 @@ class PortalTelegram(BasePortal, ABC): "audio/": MessageType.AUDIO, "image/": MessageType.IMAGE, }.get(info.mimetype[:6], MessageType.FILE)) - return await intent.send_message_event(self.mxid, event_type, content, timestamp=evt.date) + return await self._send_message(intent, content, event_type=event_type, timestamp=evt.date) def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: dict = None) -> Awaitable[EventID]: @@ -218,7 +225,7 @@ class PortalTelegram(BasePortal, ABC): content["format"] = str(Format.HTML) content["formatted_body"] = f"Location: {body}" - return intent.send_message(self.mxid, content, timestamp=evt.date) + return self._send_message(intent, content, timestamp=evt.date) async def handle_telegram_text(self, source: 'AbstractUser', intent: IntentAPI, is_bot: bool, evt: Message) -> EventID: @@ -228,7 +235,7 @@ class PortalTelegram(BasePortal, ABC): if is_bot and self.get_config("bot_messages_as_notices"): content.msgtype = MessageType.NOTICE await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_message(self.mxid, content, timestamp=evt.date) + return await self._send_message(intent, content, timestamp=evt.date) async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: dict = None) -> EventID: @@ -241,7 +248,7 @@ class PortalTelegram(BasePortal, ABC): content.external_url = self._get_external_url(evt) content["net.maunium.telegram.unsupported"] = True await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_message(self.mxid, content, timestamp=evt.date) + return await self._send_message(intent, content, timestamp=evt.date) async def handle_telegram_poll(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: RelatesTo) -> EventID: @@ -267,7 +274,7 @@ class PortalTelegram(BasePortal, ABC): relates_to=relates_to, external_url=self._get_external_url(evt)) await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_message(self.mxid, content, timestamp=evt.date) + return await self._send_message(intent, content, timestamp=evt.date) @staticmethod def _int_to_bytes(i: int) -> bytes: @@ -309,7 +316,7 @@ class PortalTelegram(BasePortal, ABC): content["net.maunium.telegram.game"] = play_id await intent.set_typing(self.mxid, is_typing=False) - return await intent.send_message(self.mxid, content, timestamp=evt.date) + return await self._send_message(intent, content, timestamp=evt.date) async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message ) -> None: @@ -353,7 +360,7 @@ class PortalTelegram(BasePortal, ABC): intent = sender.intent_for(self) if sender else self.main_intent await intent.set_typing(self.mxid, is_typing=False) - event_id = await intent.send_message(self.mxid, content) + event_id = await self._send_message(intent, content) prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) or editing_msg DBMessage(mxid=event_id, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id), @@ -522,6 +529,7 @@ class PortalTelegram(BasePortal, ABC): elif isinstance(action, MessageActionChatMigrateTo): self.peer_type = "channel" self._migrate_and_save_telegram(TelegramID(action.channel_id)) + # TODO encrypt await sender.intent_for(self).send_emote(self.mxid, "upgraded this group to a supergroup.") elif isinstance(action, MessageActionGameScore): diff --git a/optional-requirements.txt b/optional-requirements.txt index 0c404596..61f51981 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -18,3 +18,6 @@ prometheus_client>=0.6,<0.8 #/postgres psycopg2-binary>=2,<3 + +#/e2be +matrix-nio[e2e]>=0.9,<0.10 diff --git a/requirements.txt b/requirements.txt index 4941f95a..e841e270 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta4 +mautrix==0.5.0.beta5 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From 6ab3106b38d2b6c56aadbfe5b9a14a1b9a42b79d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Mar 2020 22:43:10 +0200 Subject: [PATCH 02/14] Add libolm to docker image --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28d739a5..43c78ee3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,9 @@ RUN apk add --no-cache \ su-exec \ netcat-openbsd \ # lottieconverter - zlib libpng + zlib libpng \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ + olm-dev COPY requirements.txt /opt/mautrix-telegram/requirements.txt COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt @@ -67,7 +69,7 @@ RUN apk add --virtual .build-deps \ && apk del .build-deps COPY . /opt/mautrix-telegram -RUN apk add git && pip3 install .[speedups,hq_thumbnails,metrics] && apk del git +RUN apk add git && pip3 install .[speedups,hq_thumbnails,metrics,e2be] && apk del git COPY --from=lottieconverter /usr/lib/librlottie* /usr/lib/ COPY --from=lottieconverter /build/LottieConverter/dist/Debug/GNU-Linux/lottieconverter /usr/local/bin/lottieconverter From 260c1612a6470c27f3dab98786b95f4eeb3559fd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Mar 2020 23:09:08 +0200 Subject: [PATCH 03/14] Install matrix-nio dependencies from alpine packages when available --- Dockerfile | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 43c78ee3..f171b2cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,22 +20,22 @@ RUN apk add --no-cache libpng libpng-dev zlib zlib-dev \ FROM docker.io/alpine:3.11 +RUN echo "@edge_main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories +RUN echo "@edge_testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories +RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories + RUN apk add --no-cache \ py3-virtualenv \ py3-pillow \ py3-aiohttp \ py3-magic \ py3-sqlalchemy \ + py3-alembic@edge_testing \ py3-psycopg2 \ py3-ruamel.yaml \ + py3-commonmark@edge_testing \ # Indirect dependencies py3-idna \ - #commonmark - py3-future \ - #alembic - py3-mako \ - py3-dateutil \ - py3-markupsafe \ #moviepy py3-decorator \ py3-tqdm \ @@ -54,8 +54,21 @@ RUN apk add --no-cache \ netcat-openbsd \ # lottieconverter zlib libpng \ - && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ - olm-dev + # olm + olm-dev@edge_community \ + # matrix-nio? + py3-future \ + py3-atomicwrites \ + py3-pycryptodome@edge_main \ + py3-peewee@edge_community \ + py3-pyrsistent@edge_community \ + py3-jsonschema \ + py3-aiofiles \ + py3-cachetools@edge_community \ + py3-prometheus-client@edge_community \ + py3-unpaddedbase64 \ + py3-pyaes@edge_testing \ + py3-logbook@edge_testing COPY requirements.txt /opt/mautrix-telegram/requirements.txt COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt From 56d21bdf5953d4c737eb12b1ecb0d3f17024f8e4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Mar 2020 01:37:00 +0200 Subject: [PATCH 04/14] Add support for enabling encryption by default --- mautrix_telegram/e2ee.py | 112 ---------------------------- mautrix_telegram/matrix.py | 56 ++++++++++++-- mautrix_telegram/portal/metadata.py | 18 +++++ requirements.txt | 2 +- 4 files changed, 68 insertions(+), 120 deletions(-) delete mode 100644 mautrix_telegram/e2ee.py diff --git a/mautrix_telegram/e2ee.py b/mautrix_telegram/e2ee.py deleted file mode 100644 index cb9d6895..00000000 --- a/mautrix_telegram/e2ee.py +++ /dev/null @@ -1,112 +0,0 @@ -# mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2019 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 . -from typing import Tuple, Union -import logging -import asyncio -import hashlib -import hmac - -from nio import AsyncClient, Event as NioEvent, GroupEncryptionError, LoginError - -from mautrix.appservice import AppService -from mautrix.types import (Filter, RoomFilter, EventFilter, RoomEventFilter, StateFilter, - EventType, RoomID, Serializable, JSON, MessageEvent, Event) - -from .context import Context - - -class EncryptionManager: - loop: asyncio.AbstractEventLoop - log: logging.Logger = logging.getLogger("mau.e2ee") - client: AsyncClient - az: AppService - - login_shared_secret: bytes - - sync_task: asyncio.Task - - def __init__(self, context: 'Context') -> None: - self.loop = context.loop - self.az = context.az - self.config = context.config - lss: str = self.config["bridge.login_shared_secret"] - if not lss: - raise ValueError("login_shared_secret must be set to enable encryption") - self.login_shared_secret = lss.encode("utf-8") - self.client = AsyncClient(homeserver=self.config["homeserver.address"], - user=self.az.bot_mxid, device_id="Telegram bridge", - store_path="nio_store") - - async def encrypt(self, room_id: RoomID, event_type: EventType, - content: Union[Serializable, JSON]) -> Tuple[EventType, JSON]: - serialized = content.serialize() if isinstance(content, Serializable) else content - type_str = str(event_type) - retries = 0 - while True: - try: - type_str, encrypted = self.client.encrypt(room_id, type_str, serialized) - break - except GroupEncryptionError: - if retries > 3: - self.log.error("Got GroupEncryptionError again, giving up") - raise - retries += 1 - self.log.debug("Got GroupEncryptionError, sharing group session and trying again") - await self.client.share_group_session(room_id, ignore_unverified_devices=True) - event_type = EventType.find(type_str) - try: - encrypted["m.relates_to"] = serialized["m.relates_to"] - except KeyError: - pass - return event_type, encrypted - - def decrypt(self, event: MessageEvent) -> MessageEvent: - serialized = event.serialize() - event = self.client.decrypt_event(NioEvent.parse_encrypted_event(serialized)) - try: - event.source["content"]["m.relates_to"] = serialized["content"]["m.relates_to"] - except KeyError: - pass - return Event.deserialize(event.source) - - async def start(self) -> None: - self.log.debug("Logging in with bridge bot user") - password = hmac.new(self.login_shared_secret, self.az.bot_mxid.encode("utf-8"), - hashlib.sha512).hexdigest() - resp = await self.client.login(password, device_name="Telegram bridge") - if isinstance(resp, LoginError): - raise resp - self.sync_task = self.loop.create_task(self.client.sync_forever( - timeout=30000, sync_filter=self._filter.serialize())) - self.log.info("End-to-bridge encryption support is enabled") - - def stop(self) -> None: - self.sync_task.cancel() - - @property - def _filter(self) -> Filter: - all_events = EventType.find("*") - return Filter( - account_data=EventFilter(types=[all_events]), - presence=EventFilter(not_types=[all_events]), - room=RoomFilter( - include_leave=False, - state=StateFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), - timeline=RoomEventFilter(types=[EventType.ROOM_MEMBER, EventType.ROOM_ENCRYPTION]), - account_data=RoomEventFilter(not_types=[all_events]), - ephemeral=RoomEventFilter(not_types=[all_events]), - ), - ) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 3163e679..8e1b0038 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -20,7 +20,8 @@ from mautrix.types import (Event, EventType, RoomID, UserID, EventID, ReceiptEve ReceiptEventContent, PresenceEvent, PresenceState, TypingEvent, MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent, RoomAvatarStateEventContent, RoomTopicStateEventContent, - MemberStateEventContent, EncryptedEvent) + MemberStateEventContent, EncryptedEvent, TextMessageEventContent, + MessageType) from mautrix.errors import MatrixError from . import user as u, portal as po, puppet as pu, commands as com @@ -38,7 +39,7 @@ except ImportError: EVENT_TIME = None try: - from .e2ee import EncryptionManager + from mautrix.bridge.e2ee import EncryptionManager except ImportError: EncryptionManager = None @@ -57,10 +58,15 @@ class MatrixHandler(BaseMatrixHandler): command_processor=com.CommandProcessor(context)) self.e2ee = None if self.config["bridge.encryption.allow"]: - if EncryptionManager: - self.e2ee = EncryptionManager(context) + if not EncryptionManager: + self.log.error("Encryption enabled in config, but dependencies not installed.") + elif not self.config["bridge.login_shared_secret"]: + self.log.warning("Encryption enabled in config, but login_shared_secret not set.") else: - self.log.warning("Encryption enabled in config, but dependencies not installed.") + self.e2ee = EncryptionManager( + bot_mxid=self.az.bot_mxid, + login_shared_secret=self.config["bridge.login_shared_secret"], + homeserver_address=self.config["homeserver.address"], loop=context.loop) self.bot = context.bot self.previously_typing = {} @@ -121,14 +127,50 @@ class MatrixHandler(BaseMatrixHandler): except MatrixError: pass portal.mxid = room_id + e2be_ok = None + if self.config["bridge.encryption.default"] and self.e2ee: + e2be_ok = await self._enable_dm_encryption(portal) portal.save() inviter.register_portal(portal) - await intent.send_notice(room_id, "Portal to private chat created.") + if e2be_ok is True: + evt_type, content = await self.e2ee.encrypt( + room_id, EventType.ROOM_MESSAGE, + TextMessageEventContent(msgtype=MessageType.NOTICE, + body="Portal to private chat created and end-to-bridge" + " encryption enabled.")) + await intent.send_message_event(room_id, evt_type, content) + else: + message = "Portal to private chat created." + if e2be_ok is False: + message += "\n\nWarning: Failed to enable end-to-bridge encryption" + await intent.send_notice(room_id, message) else: await intent.join_room(room_id) await intent.send_notice(room_id, "This puppet will remain inactive until a " "Telegram chat is created for this room.") + async def _enable_dm_encryption(self, portal: po.Portal) -> bool: + try: + await portal.main_intent.invite_user(portal.mxid, self.az.bot_mxid) + await self.az.intent.join_room_by_id(portal.mxid) + await portal.main_intent.send_state_event(portal.mxid, EventType.ROOM_ENCRYPTION, { + "algorithm": "m.megolm.v1.aes-sha2" + }) + # TODO feed info about room to matrix-nio + except Exception: + self.log.warning(f"Failed to enable end-to-bridge encryption in {portal.mxid}", + exc_info=True) + return False + + try: + puppet = pu.Puppet.get(portal.tgid) + await portal.main_intent.set_room_name(portal.mxid, puppet.displayname) + except Exception: + self.log.warning(f"Failed to set room name for {portal.mxid}", exc_info=True) + + portal.encrypted = True + return True + async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None: try: is_management = len(await self.az.intent.get_room_members(room_id)) == 2 @@ -173,7 +215,7 @@ class MatrixHandler(BaseMatrixHandler): "messages for unauthenticated users.") return - self.log.debug(f"{user} joined {room_id}") + self.log.debug(f"{user.mxid} joined {room_id}") if await user.is_logged_in() or portal.has_bot: await portal.join_matrix(user, event_id) diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 4d4d491f..3c1a00d3 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -308,6 +308,17 @@ class PortalMetadata(BasePortal, ABC): "type": EventType.ROOM_POWER_LEVELS.serialize(), "content": power_levels.serialize(), }] + if config["bridge.encryption.default"] and self.matrix.e2ee: + self.encrypted = True + initial_state.append({ + "type": "m.room.encryption", + "content": {"algorithm": "m.megolm.v1.aes-sha2"}, + }) + if direct: + invites.append(self.az.bot_mxid) + # The bridge bot needs to join for e2ee, but that messes up the default name generation + # If/when canonical DMs happen, this might not be necessary anymore. + self.title = puppet.displayname if config["appservice.community_id"]: initial_state.append({ "type": "m.room.related_groups", @@ -325,6 +336,13 @@ class PortalMetadata(BasePortal, ABC): if not room_id: raise Exception(f"Failed to create room") + if self.encrypted and direct: + try: + await self.az.intent.join_room_by_id(room_id) + # TODO feed info about room to matrix-nio + except Exception: + self.log.warning(f"Failed to add bridge bot to new private chat portal {room_id}") + self.mxid = RoomID(room_id) self.by_mxid[self.mxid] = self self.save() diff --git a/requirements.txt b/requirements.txt index e841e270..7e3a246e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta5 +mautrix==0.5.0.beta6 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From d84724b8b0d4f9d9a308cc6318da0f2454840aa2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Mar 2020 01:58:38 +0200 Subject: [PATCH 05/14] Fix copying example config in docker --- docker-run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-run.sh b/docker-run.sh index 228e9f2f..bf904d45 100755 --- a/docker-run.sh +++ b/docker-run.sh @@ -17,7 +17,7 @@ fi alembic -x config=/data/config.yaml upgrade head if [ ! -f /data/config.yaml ]; then - cp example-config.yaml /data/config.yaml + cp mautrix_telegram/example-config.yaml /data/config.yaml echo "Didn't find a config file." echo "Copied default config file to /data/config.yaml" echo "Modify that config file to your liking." From 4519c882308fb0d3894552b82f28c2da6387bd1b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Mar 2020 02:12:40 +0200 Subject: [PATCH 06/14] Bump mautrix-python version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7e3a246e..60d6e0d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta6 +mautrix==0.5.0.beta7 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From 50ec2551f8c318abc7e62fcab11823d77754acf5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Mar 2020 14:28:15 +0300 Subject: [PATCH 07/14] Remove all automatic matrix-nio state receiving All state is now fed to nio from the appservice state event stream instead of /sync. This should remove all race conditions of trying to encrypt messages before nio is synced. --- mautrix_telegram/matrix.py | 58 ++++++----------------------- mautrix_telegram/portal/metadata.py | 23 +++++++----- requirements.txt | 2 +- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 8e1b0038..70ab4a33 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Set, Tuple, Union, Iterable, Optional, TYPE_CHECKING +from typing import Dict, Set, Tuple, Union, Iterable, List, TYPE_CHECKING from mautrix.bridge import BaseMatrixHandler from mautrix.types import (Event, EventType, RoomID, UserID, EventID, ReceiptEvent, ReceiptType, @@ -38,11 +38,6 @@ except ImportError: Histogram = None EVENT_TIME = None -try: - from mautrix.bridge.e2ee import EncryptionManager -except ImportError: - EncryptionManager = None - RoomMetaStateEventContent = Union[RoomNameStateEventContent, RoomAvatarStateEventContent, RoomTopicStateEventContent] @@ -50,31 +45,14 @@ RoomMetaStateEventContent = Union[RoomNameStateEventContent, RoomAvatarStateEven class MatrixHandler(BaseMatrixHandler): bot: 'Bot' commands: 'com.CommandProcessor' - e2ee: Optional[EncryptionManager] previously_typing: Dict[RoomID, Set[UserID]] def __init__(self, context: 'Context') -> None: super(MatrixHandler, self).__init__(context.az, context.config, loop=context.loop, command_processor=com.CommandProcessor(context)) - self.e2ee = None - if self.config["bridge.encryption.allow"]: - if not EncryptionManager: - self.log.error("Encryption enabled in config, but dependencies not installed.") - elif not self.config["bridge.login_shared_secret"]: - self.log.warning("Encryption enabled in config, but login_shared_secret not set.") - else: - self.e2ee = EncryptionManager( - bot_mxid=self.az.bot_mxid, - login_shared_secret=self.config["bridge.login_shared_secret"], - homeserver_address=self.config["homeserver.address"], loop=context.loop) self.bot = context.bot self.previously_typing = {} - async def init_as_bot(self) -> None: - await super().init_as_bot() - if self.e2ee: - await self.e2ee.start() - async def get_user(self, user_id: UserID) -> 'u.User': return await u.User.get_by_mxid(user_id).ensure_started() @@ -129,7 +107,7 @@ class MatrixHandler(BaseMatrixHandler): portal.mxid = room_id e2be_ok = None if self.config["bridge.encryption.default"] and self.e2ee: - e2be_ok = await self._enable_dm_encryption(portal) + e2be_ok = await self.enable_dm_encryption(portal, members=members) portal.save() inviter.register_portal(portal) if e2be_ok is True: @@ -149,27 +127,15 @@ class MatrixHandler(BaseMatrixHandler): await intent.send_notice(room_id, "This puppet will remain inactive until a " "Telegram chat is created for this room.") - async def _enable_dm_encryption(self, portal: po.Portal) -> bool: - try: - await portal.main_intent.invite_user(portal.mxid, self.az.bot_mxid) - await self.az.intent.join_room_by_id(portal.mxid) - await portal.main_intent.send_state_event(portal.mxid, EventType.ROOM_ENCRYPTION, { - "algorithm": "m.megolm.v1.aes-sha2" - }) - # TODO feed info about room to matrix-nio - except Exception: - self.log.warning(f"Failed to enable end-to-bridge encryption in {portal.mxid}", - exc_info=True) - return False - - try: - puppet = pu.Puppet.get(portal.tgid) - await portal.main_intent.set_room_name(portal.mxid, puppet.displayname) - except Exception: - self.log.warning(f"Failed to set room name for {portal.mxid}", exc_info=True) - - portal.encrypted = True - return True + async def enable_dm_encryption(self, portal: po.Portal, members: List[UserID]) -> bool: + ok = await super().enable_dm_encryption(portal, members) + if ok: + try: + puppet = pu.Puppet.get(portal.tgid) + await portal.main_intent.set_room_name(portal.mxid, puppet.displayname) + except Exception: + self.log.warning(f"Failed to set room name for {portal.mxid}", exc_info=True) + return ok async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None: try: @@ -431,8 +397,6 @@ class MatrixHandler(BaseMatrixHandler): async def handle_event(self, evt: Event) -> None: if evt.type == EventType.ROOM_REDACTION: await self.handle_redaction(evt) - elif evt.type == EventType.ROOM_ENCRYPTED and self.e2ee: - await self.int_handle_event(self.e2ee.decrypt(evt)) async def handle_state_event(self, evt: StateEvent) -> None: if evt.type == EventType.ROOM_POWER_LEVELS: diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 3c1a00d3..4b20f69e 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -30,7 +30,7 @@ from telethon.tl.types import ( from mautrix.errors import MForbidden from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership, Member, - PowerLevelStateEventContent, RoomAlias) + PowerLevelStateEventContent) from mautrix.appservice import IntentAPI from ..types import TelegramID @@ -316,9 +316,9 @@ class PortalMetadata(BasePortal, ABC): }) if direct: invites.append(self.az.bot_mxid) - # The bridge bot needs to join for e2ee, but that messes up the default name generation - # If/when canonical DMs happen, this might not be necessary anymore. - self.title = puppet.displayname + # The bridge bot needs to join for e2ee, but that messes up the default name + # generation. If/when canonical DMs happen, this might not be necessary anymore. + self.title = puppet.displayname if config["appservice.community_id"]: initial_state.append({ "type": "m.room.related_groups", @@ -336,12 +336,15 @@ class PortalMetadata(BasePortal, ABC): if not room_id: raise Exception(f"Failed to create room") - if self.encrypted and direct: - try: - await self.az.intent.join_room_by_id(room_id) - # TODO feed info about room to matrix-nio - except Exception: - self.log.warning(f"Failed to add bridge bot to new private chat portal {room_id}") + if self.encrypted: + members = [self.main_intent.mxid] + if direct: + try: + await self.az.intent.join_room_by_id(room_id) + members += [self.az.intent.mxid] + except Exception: + self.log.warning(f"Failed to add bridge bot to new private chat {room_id}") + await self.matrix.e2ee.add_room(room_id, members=members, encrypted=True) self.mxid = RoomID(room_id) self.by_mxid[self.mxid] = self diff --git a/requirements.txt b/requirements.txt index 60d6e0d0..89119906 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta7 +mautrix==0.5.0.beta8 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From 37917c497e12021fb5e8f13507263d2954ab13fb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Mar 2020 01:04:12 +0300 Subject: [PATCH 08/14] Fix encrypting outgoing Matrix events after restart --- mautrix_telegram/matrix.py | 6 ++++++ requirements.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 70ab4a33..9f168e7c 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -48,8 +48,14 @@ class MatrixHandler(BaseMatrixHandler): previously_typing: Dict[RoomID, Set[UserID]] def __init__(self, context: 'Context') -> None: + prefix, suffix = context.config["bridge.username_template"].format(userid=":").split(":") + homeserver = context.config["homeserver.domain"] + self.user_id_prefix = f"@{prefix}" + self.user_id_suffix = f"{suffix}:{homeserver}" + super(MatrixHandler, self).__init__(context.az, context.config, loop=context.loop, command_processor=com.CommandProcessor(context)) + self.bot = context.bot self.previously_typing = {} diff --git a/requirements.txt b/requirements.txt index 89119906..a64521f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta8 +mautrix==0.5.0.beta9 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From af285c5ffeaef2dfbd1e927a6a396bbb21fe33c7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Mar 2020 01:10:13 +0300 Subject: [PATCH 09/14] Allow matrix-nio 0.10 --- optional-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-requirements.txt b/optional-requirements.txt index 61f51981..c30daef8 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -20,4 +20,4 @@ prometheus_client>=0.6,<0.8 psycopg2-binary>=2,<3 #/e2be -matrix-nio[e2e]>=0.9,<0.10 +matrix-nio[e2e]>=0.9,<0.11 From 698b56afcf2b7413d763b8191048329dad69072b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Mar 2020 21:29:52 +0300 Subject: [PATCH 10/14] Encrypt media being sent to Matrix in encrypted rooms --- ...22a6acd2_add_decryption_info_field_for_.py | 26 ++++++++++ mautrix_telegram/db/telegram_file.py | 33 +++++++++++-- mautrix_telegram/matrix.py | 3 +- mautrix_telegram/portal/telegram.py | 29 ++++++++--- mautrix_telegram/util/file_transfer.py | 49 +++++++++++++------ .../util/parallel_file_transfer.py | 31 ++++++++++-- 6 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 alembic/versions/d3c922a6acd2_add_decryption_info_field_for_.py diff --git a/alembic/versions/d3c922a6acd2_add_decryption_info_field_for_.py b/alembic/versions/d3c922a6acd2_add_decryption_info_field_for_.py new file mode 100644 index 00000000..1469f2a8 --- /dev/null +++ b/alembic/versions/d3c922a6acd2_add_decryption_info_field_for_.py @@ -0,0 +1,26 @@ +"""Add decryption info field for reuploaded telegram files + +Revision ID: d3c922a6acd2 +Revises: 24f31fc8a72b +Create Date: 2020-03-30 20:07:17.340346 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd3c922a6acd2' +down_revision = '24f31fc8a72b' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("telegram_file") as batch_op: + batch_op.add_column(sa.Column("decryption_info", sa.Text(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("telegram_file") as batch_op: + batch_op.drop_column("decryption_info") diff --git a/mautrix_telegram/db/telegram_file.py b/mautrix_telegram/db/telegram_file.py index 8cc0c9c0..82f1e17c 100644 --- a/mautrix_telegram/db/telegram_file.py +++ b/mautrix_telegram/db/telegram_file.py @@ -13,15 +13,37 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional +from typing import Optional, cast, Dict, Any -from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean +from sqlalchemy import (Column, ForeignKey, Integer, BigInteger, String, Boolean, Text, + TypeDecorator) from sqlalchemy.engine.result import RowProxy -from mautrix.types import ContentURI +from mautrix.types import ContentURI, EncryptedFile from mautrix.util.db import Base +class DBEncryptedFile(TypeDecorator): + impl = Text + + @property + def python_type(self): + return EncryptedFile + + def process_bind_param(self, value: EncryptedFile, dialect) -> Optional[str]: + if value is not None: + return value.json() + return None + + def process_result_value(self, value: str, dialect) -> Optional[EncryptedFile]: + if value is not None: + return EncryptedFile.parse_json(value) + return None + + def process_literal_param(self, value, dialect): + return value + + class TelegramFile(Base): __tablename__ = "telegram_file" @@ -33,12 +55,13 @@ class TelegramFile(Base): size: Optional[int] = Column(Integer, nullable=True) width: Optional[int] = Column(Integer, nullable=True) height: Optional[int] = Column(Integer, nullable=True) + decryption_info: Optional[Dict[str, Any]] = Column(DBEncryptedFile, nullable=True) thumbnail_id: str = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True) thumbnail: Optional['TelegramFile'] = None @classmethod def scan(cls, row: RowProxy) -> 'TelegramFile': - telegram_file: TelegramFile = super().scan(row) + telegram_file = cast(TelegramFile, super().scan(row)) if isinstance(telegram_file.thumbnail, str): telegram_file.thumbnail = cls.get(telegram_file.thumbnail) return telegram_file @@ -52,5 +75,5 @@ class TelegramFile(Base): conn.execute(self.t.insert().values( id=self.id, mxc=self.mxc, mime_type=self.mime_type, was_converted=self.was_converted, timestamp=self.timestamp, size=self.size, - width=self.width, height=self.height, + width=self.width, height=self.height, decryption_info=self.decryption_info, thumbnail=self.thumbnail.id if self.thumbnail else self.thumbnail_id)) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 9f168e7c..6d600669 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -54,7 +54,8 @@ class MatrixHandler(BaseMatrixHandler): self.user_id_suffix = f"{suffix}:{homeserver}" super(MatrixHandler, self).__init__(context.az, context.config, loop=context.loop, - command_processor=com.CommandProcessor(context)) + command_processor=com.CommandProcessor(context), + bridge=context.bridge) self.bot = context.bot self.previously_typing = {} diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index 61f655fd..bd782797 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -80,7 +80,8 @@ class PortalTelegram(BasePortal, ABC): async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: Dict = None) -> Optional[EventID]: loc, largest_size = self._get_largest_photo_size(evt.media.photo) - file = await util.transfer_file_to_matrix(source.client, intent, loc) + file = await util.transfer_file_to_matrix(source.client, intent, loc, + encrypt=self.encrypted) if not file: return None if self.get_config("inline_images") and (evt.message @@ -98,9 +99,13 @@ class PortalTelegram(BasePortal, ABC): else largest_size.size)) name = f"image{sane_mimetypes.guess_extension(file.mime_type)}" await intent.set_typing(self.mxid, is_typing=False) - content = MediaMessageEventContent(url=file.mxc, msgtype=MessageType.IMAGE, info=info, + content = MediaMessageEventContent(msgtype=MessageType.IMAGE, info=info, body=name, relates_to=relates_to, external_url=self._get_external_url(evt)) + if file.decryption_info: + content.file = file.decryption_info + else: + content.url = file.mxc result = await self._send_message(intent, content, timestamp=evt.date) if evt.message: caption_content = await formatter.telegram_to_matrix(evt, source, self.main_intent, @@ -153,13 +158,20 @@ class PortalTelegram(BasePortal, ABC): info.width, info.height = attrs.width, attrs.height if file.thumbnail: - info.thumbnail_url = file.thumbnail.mxc + if file.thumbnail.decryption_info: + info.thumbnail_file = file.thumbnail.decryption_info + else: + info.thumbnail_url = file.thumbnail.mxc info.thumbnail_info = ThumbnailInfo(mimetype=file.thumbnail.mime_type, height=file.thumbnail.height or thumb_size.h, width=file.thumbnail.width or thumb_size.w, size=file.thumbnail.size) else: - info.thumbnail_url = file.mxc + # This is a hack for bad clients like Riot iOS that require a thumbnail + if file.decryption_info: + info.thumbnail_file = file.decryption_info + else: + info.thumbnail_url = file.mxc info.thumbnail_info = ImageInfo.deserialize(info.serialize()) return info, name @@ -186,7 +198,8 @@ class PortalTelegram(BasePortal, ABC): file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc, is_sticker=attrs.is_sticker, tgs_convert=config["bridge.animated_sticker"], - filename=attrs.name, parallel_id=parallel_id) + filename=attrs.name, parallel_id=parallel_id, + encrypt=self.encrypted) if not file: return None @@ -199,13 +212,17 @@ class PortalTelegram(BasePortal, ABC): if attrs.is_sticker and file.mime_type.startswith("image/"): event_type = EventType.STICKER content = MediaMessageEventContent( - body=name or "unnamed file", info=info, url=file.mxc, relates_to=relates_to, + body=name or "unnamed file", info=info, relates_to=relates_to, external_url=self._get_external_url(evt), msgtype={ "video/": MessageType.VIDEO, "audio/": MessageType.AUDIO, "image/": MessageType.IMAGE, }.get(info.mimetype[:6], MessageType.FILE)) + if file.decryption_info: + content.file = file.decryption_info + else: + content.url = file.mxc return await self._send_message(intent, content, event_type=event_type, timestamp=evt.date) def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message, diff --git a/mautrix_telegram/util/file_transfer.py b/mautrix_telegram/util/file_transfer.py index f9df31e5..36ae6a0c 100644 --- a/mautrix_telegram/util/file_transfer.py +++ b/mautrix_telegram/util/file_transfer.py @@ -29,11 +29,13 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio SecurityError, FileIdInvalidError) from mautrix.appservice import IntentAPI +from mautrix.types import EncryptedFile from ..tgclient import MautrixTelegramClient from ..db import TelegramFile as DBTelegramFile from ..util import sane_mimetypes from .parallel_file_transfer import parallel_transfer_to_matrix +from .tgs_converter import convert_tgs_to try: from PIL import Image @@ -49,7 +51,10 @@ try: except ImportError: VideoFileClip = random = string = os = mimetypes = None -from .tgs_converter import convert_tgs_to +try: + from nio.crypto import encrypt_attachment +except ImportError: + encrypt_attachment = None log: logging.Logger = logging.getLogger("mau.util") @@ -115,8 +120,8 @@ def _location_to_id(location: TypeLocation) -> str: async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, - thumbnail_loc: TypeLocation, video: bytes, - mime: str) -> Optional[DBTelegramFile]: + thumbnail_loc: TypeLocation, video: bytes, mime: str, + encrypt: bool) -> Optional[DBTelegramFile]: if not Image or not VideoFileClip: return None @@ -140,11 +145,19 @@ async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: In width, height = None, None mime_type = magic.from_buffer(file, mime=True) - content_uri = await intent.upload_media(file, mime_type) + decryption_info = None + upload_mime_type = mime_type + if encrypt: + file, decryption_info_dict = encrypt_attachment(file) + decryption_info = EncryptedFile.deserialize(decryption_info_dict) + upload_mime_type = "application/octet-stream" + content_uri = await intent.upload_media(file, upload_mime_type) + if decryption_info: + decryption_info.url = content_uri db_file = DBTelegramFile(id=loc_id, mxc=content_uri, mime_type=mime_type, was_converted=False, timestamp=int(time.time()), size=len(file), - width=width, height=height) + width=width, height=height, decryption_info=decryption_info) try: db_file.insert() except (IntegrityError, InvalidRequestError) as e: @@ -160,10 +173,10 @@ TypeThumbnail = Optional[Union[TypeLocation, TypePhotoSize]] async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, - location: TypeLocation, thumbnail: TypeThumbnail = None, + location: TypeLocation, thumbnail: TypeThumbnail = None, *, is_sticker: bool = False, tgs_convert: Optional[dict] = None, - filename: Optional[str] = None, parallel_id: Optional[int] = None - ) -> Optional[DBTelegramFile]: + filename: Optional[str] = None, encrypt: bool = False, + parallel_id: Optional[int] = None) -> Optional[DBTelegramFile]: location_id = _location_to_id(location) if not location_id: return None @@ -180,14 +193,14 @@ async def transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentA async with lock: return await _unlocked_transfer_file_to_matrix(client, intent, location_id, location, thumbnail, is_sticker, tgs_convert, - filename, parallel_id) + filename, encrypt, parallel_id) async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, loc_id: str, location: TypeLocation, thumbnail: TypeThumbnail, is_sticker: bool, tgs_convert: Optional[dict], filename: Optional[str], - parallel_id: Optional[int] + encrypt: bool, parallel_id: Optional[int] ) -> Optional[DBTelegramFile]: db_file = DBTelegramFile.get(loc_id) if db_file: @@ -195,7 +208,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten if parallel_id and isinstance(location, Document) and (not is_sticker or not tgs_convert): db_file = await parallel_transfer_to_matrix(client, intent, loc_id, location, filename, - parallel_id) + encrypt, parallel_id) mime_type = location.mime_type file = None else: @@ -228,9 +241,17 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten mime_type = new_mime_type thumbnail = None - content_uri = await intent.upload_media(file, mime_type) + decryption_info = None + upload_mime_type = mime_type + if encrypt and encrypt_attachment: + file, decryption_info_dict = encrypt_attachment(file) + decryption_info = EncryptedFile.deserialize(decryption_info_dict) + upload_mime_type = "application/octet-stream" + content_uri = await intent.upload_media(file, upload_mime_type) + if decryption_info: + decryption_info.url = content_uri - db_file = DBTelegramFile(id=loc_id, mxc=content_uri, + db_file = DBTelegramFile(id=loc_id, mxc=content_uri, decryption_info=decryption_info, mime_type=mime_type, was_converted=image_converted, timestamp=int(time.time()), size=len(file), width=width, height=height) @@ -239,7 +260,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten thumbnail = thumbnail.location try: db_file.thumbnail = await transfer_thumbnail_to_matrix(client, intent, thumbnail, file, - mime_type) + mime_type, encrypt) except FileIdInvalidError: log.warning(f"Failed to transfer thumbnail for {thumbnail!s}", exc_info=True) diff --git a/mautrix_telegram/util/parallel_file_transfer.py b/mautrix_telegram/util/parallel_file_transfer.py index 507646aa..5d316e48 100644 --- a/mautrix_telegram/util/parallel_file_transfer.py +++ b/mautrix_telegram/util/parallel_file_transfer.py @@ -34,11 +34,16 @@ from telethon.crypto import AuthKey from telethon import utils, helpers from mautrix.appservice import IntentAPI -from mautrix.types import ContentURI +from mautrix.types import ContentURI, EncryptedFile from ..tgclient import MautrixTelegramClient from ..db import TelegramFile as DBTelegramFile +try: + from nio.crypto import async_encrypt_attachment +except ImportError: + async_encrypt_attachment = None + log: logging.Logger = logging.getLogger("mau.util") TypeLocation = Union[Document, InputDocumentFileLocation, InputPeerPhotoFileLocation, @@ -242,18 +247,34 @@ parallel_transfer_locks: DefaultDict[int, asyncio.Lock] = defaultdict(lambda: as async def parallel_transfer_to_matrix(client: MautrixTelegramClient, intent: IntentAPI, loc_id: str, location: TypeLocation, filename: str, - parallel_id: int) -> DBTelegramFile: + encrypt: bool, parallel_id: int) -> DBTelegramFile: size = location.size mime_type = location.mime_type dc_id, location = utils.get_input_location(location) # We lock the transfers because telegram has connection count limits async with parallel_transfer_locks[parallel_id]: downloader = ParallelTransferrer(client, dc_id) - content_uri = await intent.upload_media(downloader.download(location, size), - mime_type=mime_type, filename=filename, size=size) + data = downloader.download(location, size) + decryption_info = None + up_mime_type = mime_type + if encrypt and async_encrypt_attachment: + async def encrypted(stream): + nonlocal decryption_info + async for chunk in async_encrypt_attachment(stream): + if isinstance(chunk, dict): + decryption_info = EncryptedFile.deserialize(chunk) + else: + yield chunk + + data = encrypted(data) + up_mime_type = "application/octet-stream" + content_uri = await intent.upload_media(data, mime_type=up_mime_type, filename=filename, + size=size if not encrypt else None) + if decryption_info: + decryption_info.url = content_uri return DBTelegramFile(id=loc_id, mxc=content_uri, mime_type=mime_type, was_converted=False, timestamp=int(time.time()), size=size, - width=None, height=None) + width=None, height=None, decryption_info=decryption_info) async def _internal_transfer_to_telegram(client: MautrixTelegramClient, response: ClientResponse From a00c58e5210861a75a4506c7cc1978eeb7a18ac3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Mar 2020 21:47:41 +0300 Subject: [PATCH 11/14] Decrypt encrypted media from Matrix --- mautrix_telegram/portal/matrix.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mautrix_telegram/portal/matrix.py b/mautrix_telegram/portal/matrix.py index e0ffb58e..afd25b6c 100644 --- a/mautrix_telegram/portal/matrix.py +++ b/mautrix_telegram/portal/matrix.py @@ -50,6 +50,11 @@ if TYPE_CHECKING: from ..tgclient import MautrixTelegramClient from ..config import Config +try: + from nio.crypto import decrypt_attachment +except ImportError: + decrypt_attachment = None + TypeMessage = Union[Message, MessageService] config: Optional['Config'] = None @@ -250,11 +255,20 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): file_name = content["net.maunium.telegram.internal.filename"] max_image_size = config["bridge.image_as_file_size"] * 1000 ** 2 - if config["bridge.parallel_file_transfer"]: + if config["bridge.parallel_file_transfer"] and content.url: file_handle, file_size = await parallel_transfer_to_telegram(client, self.main_intent, content.url, sender_id) else: - file = await self.main_intent.download_media(content.url) + if content.file: + if not decrypt_attachment: + self.log.warning(f"Can't bridge encrypted media event {event_id}:" + " matrix-nio not installed") + return + file = await self.main_intent.download_media(content.file.url) + file = decrypt_attachment(file, content.file.key.key, + content.file.hashes.get("sha256"), content.file.iv) + else: + file = await self.main_intent.download_media(content.url) if content.msgtype == MessageType.STICKER: if mime != "image/gif": From c1d4e8e48289bcd20f3cc12cabe22bc3b30d442f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 Mar 2020 22:19:43 +0300 Subject: [PATCH 12/14] Update mautrix-python to use SQLAlchemy for matrix-nio state storage --- ...d_add_matrix_nio_state_store_to_main_db.py | 71 +++++++++++++++++++ mautrix_telegram/db/__init__.py | 7 ++ requirements.txt | 2 +- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/dff56c93da8d_add_matrix_nio_state_store_to_main_db.py diff --git a/alembic/versions/dff56c93da8d_add_matrix_nio_state_store_to_main_db.py b/alembic/versions/dff56c93da8d_add_matrix_nio_state_store_to_main_db.py new file mode 100644 index 00000000..6a16f2dc --- /dev/null +++ b/alembic/versions/dff56c93da8d_add_matrix_nio_state_store_to_main_db.py @@ -0,0 +1,71 @@ +"""Add matrix-nio state store to main db + +Revision ID: dff56c93da8d +Revises: d3c922a6acd2 +Create Date: 2020-03-31 22:04:04.014048 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dff56c93da8d' +down_revision = 'd3c922a6acd2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('nio_account', + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.Column('device_id', sa.String(length=255), nullable=False), + sa.Column('shared', sa.Boolean(), nullable=False), + sa.Column('sync_token', sa.Text(), nullable=False), + sa.Column('account', sa.LargeBinary(), nullable=False), + sa.PrimaryKeyConstraint('user_id', 'device_id') + ) + op.create_table('nio_device_key', + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.Column('device_id', sa.String(length=255), nullable=False), + sa.Column('display_name', sa.String(length=255), nullable=False), + sa.Column('deleted', sa.Boolean(), nullable=False), + sa.Column('keys', sa.PickleType(), nullable=False), + sa.PrimaryKeyConstraint('user_id', 'device_id') + ) + op.create_table('nio_megolm_inbound_session', + sa.Column('session_id', sa.String(length=255), nullable=False), + sa.Column('sender_key', sa.String(length=255), nullable=False), + sa.Column('fp_key', sa.String(length=255), nullable=False), + sa.Column('room_id', sa.String(length=255), nullable=False), + sa.Column('session', sa.LargeBinary(), nullable=False), + sa.Column('forwarded_chains', sa.PickleType(), nullable=False), + sa.PrimaryKeyConstraint('session_id') + ) + op.create_table('nio_olm_session', + sa.Column('session_id', sa.String(length=255), nullable=False), + sa.Column('sender_key', sa.String(length=255), nullable=False), + sa.Column('session', sa.LargeBinary(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('last_used', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('session_id') + ) + op.create_table('nio_outgoing_key_request', + sa.Column('request_id', sa.String(length=255), nullable=False), + sa.Column('session_id', sa.String(length=255), nullable=False), + sa.Column('room_id', sa.String(length=255), nullable=False), + sa.Column('algorithm', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('request_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('nio_outgoing_key_request') + op.drop_table('nio_olm_session') + op.drop_table('nio_megolm_inbound_session') + op.drop_table('nio_device_key') + op.drop_table('nio_account') + # ### end Alembic commands ### diff --git a/mautrix_telegram/db/__init__.py b/mautrix_telegram/db/__init__.py index 92a824f0..0725d6ae 100644 --- a/mautrix_telegram/db/__init__.py +++ b/mautrix_telegram/db/__init__.py @@ -24,6 +24,11 @@ from .puppet import Puppet from .telegram_file import TelegramFile from .user import User, UserPortal, Contact +try: + from mautrix.bridge.db.nio_state_store import init as init_nio_db +except ImportError: + init_nio_db = None + def init(db_engine: Engine) -> None: for table in (Portal, Message, User, Contact, UserPortal, Puppet, TelegramFile, UserProfile, @@ -32,3 +37,5 @@ def init(db_engine: Engine) -> None: table.t = table.__table__ table.c = table.t.c table.column_names = table.c.keys() + if init_nio_db: + init_nio_db(db_engine) diff --git a/requirements.txt b/requirements.txt index a64521f8..feaf4bb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17 python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 -mautrix==0.5.0.beta9 +mautrix==0.5.0.beta10 telethon>=1.10,<1.12 telethon-session-sqlalchemy>=0.2.14,<0.3 From 32db2355a2c1b75c9525fd25c96b8db78cee5e33 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Apr 2020 22:13:02 +0300 Subject: [PATCH 13/14] Add pysocks to dockerfile Closes #445 --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index f171b2cb..7ac870c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,8 @@ RUN apk add --no-cache \ py3-numpy \ #telethon py3-rsa \ + # Optional for socks proxies + py3-pysocks \ # cryptg py3-cffi \ py3-brotli \ From 708fec6886b577ff53bc906d3f351cc6287ac5aa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Apr 2020 22:18:07 +0300 Subject: [PATCH 14/14] Add missing check --- mautrix_telegram/portal/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index 4b20f69e..c0cec1be 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -336,7 +336,7 @@ class PortalMetadata(BasePortal, ABC): if not room_id: raise Exception(f"Failed to create room") - if self.encrypted: + if self.encrypted and self.matrix.e2ee: members = [self.main_intent.mxid] if direct: try: