From 8a99e67c6d9c1d686810c3a8a50bc9b59e601872 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 15 Jun 2020 14:43:38 +0300 Subject: [PATCH] Update bridge info when portal metadata changes --- alembic/env.py | 7 ++- ...458_store_matrix_avatar_url_in_database.py | 32 ++++++++++ mautrix_telegram/db/portal.py | 5 +- mautrix_telegram/portal/base.py | 24 +++++--- mautrix_telegram/portal/matrix.py | 7 ++- mautrix_telegram/portal/metadata.py | 58 ++++++++++++++++--- mautrix_telegram/portal/telegram.py | 4 ++ requirements.txt | 2 +- 8 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 alembic/versions/3e3745baa458_store_matrix_avatar_url_in_database.py diff --git a/alembic/env.py b/alembic/env.py index 2fe2d45b..4f91e8a4 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -21,7 +21,6 @@ mxtg_config = Config(mxtg_config_path, None, None) mxtg_config.load() config.set_main_option("sqlalchemy.url", mxtg_config["appservice.database"].replace("%", "%%")) - AlchemySessionContainer.create_table_classes(None, "telethon_", Base) # Interpret the config file for Python logging. @@ -55,7 +54,8 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") context.configure( - url=url, target_metadata=target_metadata, literal_binds=True) + url=url, target_metadata=target_metadata, literal_binds=True, + render_as_batch=True) with context.begin_transaction(): context.run_migrations() @@ -76,7 +76,8 @@ def run_migrations_online(): with connectable.connect() as connection: context.configure( connection=connection, - target_metadata=target_metadata + target_metadata=target_metadata, + render_as_batch=True ) with context.begin_transaction(): diff --git a/alembic/versions/3e3745baa458_store_matrix_avatar_url_in_database.py b/alembic/versions/3e3745baa458_store_matrix_avatar_url_in_database.py new file mode 100644 index 00000000..31db36c2 --- /dev/null +++ b/alembic/versions/3e3745baa458_store_matrix_avatar_url_in_database.py @@ -0,0 +1,32 @@ +"""Store Matrix avatar URL in database + +Revision ID: 3e3745baa458 +Revises: dff56c93da8d +Create Date: 2020-06-15 14:32:10.454033 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3e3745baa458' +down_revision = 'dff56c93da8d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('portal', schema=None) as batch_op: + batch_op.add_column(sa.Column('avatar_url', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('portal', schema=None) as batch_op: + batch_op.drop_column('avatar_url') + + # ### end Alembic commands ### diff --git a/mautrix_telegram/db/portal.py b/mautrix_telegram/db/portal.py index 8112e9ad..8fbe1a5a 100644 --- a/mautrix_telegram/db/portal.py +++ b/mautrix_telegram/db/portal.py @@ -17,7 +17,7 @@ from typing import Optional from sqlalchemy import Column, Integer, String, Boolean, Text, func, sql -from mautrix.types import RoomID +from mautrix.types import RoomID, ContentURI from mautrix.util.db import Base from ..types import TelegramID @@ -33,7 +33,8 @@ class Portal(Base): megagroup: bool = Column(Boolean) # Matrix portal information - mxid: RoomID = Column(String, unique=True, nullable=True) + mxid: Optional[RoomID] = Column(String, unique=True, nullable=True) + avatar_url: Optional[ContentURI] = Column(String, nullable=True) encrypted: bool = Column(Boolean, nullable=False, server_default=sql.expression.false()) config: str = Column(Text, nullable=True) diff --git a/mautrix_telegram/portal/base.py b/mautrix_telegram/portal/base.py index 7476287b..127cf6fa 100644 --- a/mautrix_telegram/portal/base.py +++ b/mautrix_telegram/portal/base.py @@ -31,7 +31,7 @@ from telethon.tl.types import (Channel, ChannelFull, Chat, ChatFull, ChatInviteE from mautrix.errors import MatrixRequestError, IntentError from mautrix.appservice import AppService, IntentAPI from mautrix.types import (RoomID, RoomAlias, UserID, EventID, EventType, MessageEventContent, - PowerLevelStateEventContent) + PowerLevelStateEventContent, ContentURI) from mautrix.util.simple_template import SimpleTemplate from mautrix.util.logging import TraceLogger @@ -90,6 +90,7 @@ class BasePortal(ABC): about: Optional[str] photo_id: Optional[str] local_config: Dict[str, Any] + avatar_url: Optional[ContentURI] encrypted: bool deleted: bool backfilling: bool @@ -108,8 +109,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, encrypted: Optional[bool] = False, - db_instance: DBPortal = None) -> None: + local_config: Optional[str] = None, avatar_url: Optional[ContentURI] = None, + encrypted: Optional[bool] = False, db_instance: DBPortal = None) -> None: self.mxid = mxid self.tgid = tgid self.tg_receiver = tg_receiver or tgid @@ -120,6 +121,7 @@ class BasePortal(ABC): self.about = about self.photo_id = photo_id self.local_config = json.loads(local_config or "{}") + self.avatar_url = avatar_url self.encrypted = encrypted self._db_instance = db_instance self._main_intent = None @@ -335,12 +337,14 @@ 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), encrypted=self.encrypted) + config=json.dumps(self.local_config), avatar_url=self.avatar_url, + 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), encrypted=self.encrypted) + config=json.dumps(self.local_config), avatar_url=self.avatar_url, + encrypted=self.encrypted) def delete(self) -> None: try: @@ -362,7 +366,8 @@ class BasePortal(ABC): 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) + avatar_url=db_portal.avatar_url, encrypted=db_portal.encrypted, + db_instance=db_portal) # endregion # region Class instance lookup @@ -509,6 +514,10 @@ class BasePortal(ABC): def _migrate_and_save_telegram(self, new_id: TelegramID) -> None: pass + @abstractmethod + async def _update_bridge_info(self) -> None: + pass + @abstractmethod def handle_matrix_power_levels(self, sender: 'u.User', new_levels: Dict[UserID, int], old_levels: Dict[UserID, int], event_id: Optional[EventID] @@ -520,7 +529,8 @@ class BasePortal(ABC): pass @abstractmethod - async def _send_delivery_receipt(self, event_id: EventID) -> None: + async def _send_delivery_receipt(self, event_id: EventID, room_id: Optional[RoomID] = None + ) -> None: pass # endregion diff --git a/mautrix_telegram/portal/matrix.py b/mautrix_telegram/portal/matrix.py index 9ead60e7..b0c77cee 100644 --- a/mautrix_telegram/portal/matrix.py +++ b/mautrix_telegram/portal/matrix.py @@ -500,13 +500,17 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): self.title = title self.save() await self._send_delivery_receipt(event_id) + await self._update_bridge_info() async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI, event_id: EventID ) -> None: if self.peer_type not in ("chat", "channel"): # Invalid peer type return + elif self.avatar_url == url: + return + self.avatar_url = url file = await self.main_intent.download_media(url) mime = magic.from_buffer(file, mime=True) ext = sane_mimetypes.guess_extension(mime) @@ -529,6 +533,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): self.save() break await self._send_delivery_receipt(event_id) + await self._update_bridge_info() async def handle_matrix_upgrade(self, sender: UserID, new_room: RoomID, event_id: EventID ) -> None: @@ -558,7 +563,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): return await self.update_matrix_room(user, entity, direct=self.peer_type == "user") self.log.info(f"{sender} upgraded room from {old_room} to {self.mxid}") - await self._send_delivery_receipt(event_id) + await self._send_delivery_receipt(event_id, room_id=old_room) def migrate_and_save_matrix(self, new_id: RoomID) -> None: try: diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index d98891e7..d5933202 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.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 List, Optional, Tuple, Union, Callable, Awaitable, TYPE_CHECKING +from typing import List, Optional, Tuple, Union, Dict, Any, TYPE_CHECKING from abc import ABC import asyncio @@ -45,6 +45,9 @@ if TYPE_CHECKING: config: Optional['Config'] = None +StateBridge = EventType.find("m.bridge", EventType.Class.STATE) +StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE) + class PortalMetadata(BasePortal, ABC): _room_create_lock: asyncio.Lock @@ -226,6 +229,7 @@ class PortalMetadata(BasePortal, ABC): changed = await self._update_avatar(user, entity.photo) or changed if changed: self.save() + await self._update_bridge_info() if self.sync_matrix_state: await self.sync_matrix_members() @@ -253,6 +257,38 @@ class PortalMetadata(BasePortal, ABC): except Exception: self.log.exception("Fatal error creating Matrix room") + @property + def bridge_info_state_key(self) -> str: + return f"net.maunium.telegram://telegram/{self.tgid}" + + @property + def bridge_info(self) -> Dict[str, Any]: + return { + "bridgebot": self.az.bot_mxid, + "creator": self.main_intent.mxid, + "protocol": { + "id": "telegram", + "displayname": "Telegram", + "avatar_url": config["appservice.bot_avatar"], + }, + "channel": { + "id": str(self.tgid), + "displayname": self.title, + "avatar_url": self.avatar_url, + } + } + + async def _update_bridge_info(self) -> None: + try: + self.log.debug("Updating bridge info...") + await self.main_intent.send_state_event(self.mxid, StateBridge, + self.bridge_info, self.bridge_info_state_key) + # TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec + await self.main_intent.send_state_event(self.mxid, StateHalfShotBridge, + self.bridge_info, self.bridge_info_state_key) + except Exception: + self.log.warning("Failed to update bridge info", exc_info=True) + async def _create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User], invites: InviteList) -> Optional[RoomID]: direct = self.peer_type == "user" @@ -333,14 +369,14 @@ class PortalMetadata(BasePortal, ABC): "type": EventType.ROOM_POWER_LEVELS.serialize(), "content": power_levels.serialize(), }, { - "type": "m.bridge", - "state_key": f"net.maunium.telegram://telegram/{self.tgid}", - "content": bridge_info + "type": str(StateBridge), + "state_key": self.bridge_info_state_key, + "content": self.bridge_info, }, { # TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec - "type": "uk.half-shot.bridge", - "state_key": f"net.maunium.telegram://telegram/{self.tgid}", - "content": bridge_info + "type": str(StateHalfShotBridge), + "state_key": self.bridge_info_state_key, + "content": self.bridge_info, }] if config["bridge.encryption.default"] and self.matrix.e2ee: self.encrypted = True @@ -620,6 +656,7 @@ class PortalMetadata(BasePortal, ABC): if changed: self.save() + await self._update_bridge_info() async def _update_username(self, username: str, save: bool = False) -> bool: if self.username == username: @@ -702,6 +739,7 @@ class PortalMetadata(BasePortal, ABC): await self._try_set_state(sender, EventType.ROOM_AVATAR, RoomAvatarStateEventContent(url=None)) self.photo_id = "" + self.avatar_url = None if save: self.save() return True @@ -710,6 +748,7 @@ class PortalMetadata(BasePortal, ABC): await self._try_set_state(sender, EventType.ROOM_AVATAR, RoomAvatarStateEventContent(url=file.mxc)) self.photo_id = photo_id + self.avatar_url = file.mxc if save: self.save() return True @@ -762,10 +801,11 @@ class PortalMetadata(BasePortal, ABC): # endregion - async def _send_delivery_receipt(self, event_id: EventID) -> None: + async def _send_delivery_receipt(self, event_id: EventID, room_id: Optional[RoomID] = None + ) -> None: if event_id and config["bridge.delivery_receipts"]: try: - await self.az.intent.mark_read(self.mxid, event_id) + await self.az.intent.mark_read(room_id or self.mxid, event_id) except Exception: self.log.exception("Failed to send delivery receipt for %s", event_id) diff --git a/mautrix_telegram/portal/telegram.py b/mautrix_telegram/portal/telegram.py index cb26805f..4da6be75 100644 --- a/mautrix_telegram/portal/telegram.py +++ b/mautrix_telegram/portal/telegram.py @@ -555,10 +555,13 @@ class PortalTelegram(BasePortal, ABC): return if isinstance(action, MessageActionChatEditTitle): await self._update_title(action.title, sender=sender, save=True) + await self._update_bridge_info() elif isinstance(action, MessageActionChatEditPhoto): await self._update_avatar(source, action.photo, sender=sender, save=True) + await self._update_bridge_info() elif isinstance(action, MessageActionChatDeletePhoto): await self._update_avatar(source, ChatPhotoEmpty(), sender=sender, save=True) + await self._update_bridge_info() elif isinstance(action, MessageActionChatAddUser): for user_id in action.users: await self._add_telegram_user(TelegramID(user_id), source) @@ -572,6 +575,7 @@ class PortalTelegram(BasePortal, ABC): # TODO encrypt await sender.intent_for(self).send_emote(self.mxid, "upgraded this group to a supergroup.") + await self._update_bridge_info() elif isinstance(action, MessageActionGameScore): # TODO handle game score pass diff --git a/requirements.txt b/requirements.txt index 9251c7de..1e41c679 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.2,<0.6 +mautrix>=0.5.5,<0.6 telethon>=1.13,<1.15 telethon-session-sqlalchemy>=0.2.14,<0.3