diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index dbe71487..772ceb46 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -1,5 +1,5 @@ # mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2020 Tulir Asokan +# Copyright (C) 2021 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 @@ -15,9 +15,9 @@ # along with this program. If not, see . from typing import Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING from abc import ABC, abstractmethod +import platform import asyncio import logging -import platform import time from telethon.sessions import Session @@ -31,7 +31,8 @@ from telethon.tl.types import ( UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox, UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus, UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox, - UpdateReadChannelInbox, MessageEmpty) + UpdateReadChannelInbox, MessageEmpty, UpdateFolderPeers, UpdatePinnedDialogs, + UpdateNotifySettings) from mautrix.types import UserID, PresenceState from mautrix.errors import MatrixError @@ -235,8 +236,7 @@ class AbstractUser(ABC): # region Telegram update handling async def _update(self, update: TypeUpdate) -> None: - asyncio.ensure_future(self._handle_entity_updates(getattr(update, "_entities", {})), - loop=self.loop) + asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {}))) if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage, UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)): await self.update_message(update) @@ -260,9 +260,24 @@ class AbstractUser(ABC): await self.update_read_receipt(update) elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)): await self.update_own_read_receipt(update) + elif isinstance(update, UpdateFolderPeers): + await self.update_folder_peers(update) + elif isinstance(update, UpdatePinnedDialogs): + await self.update_pinned_dialogs(update) + elif isinstance(update, UpdateNotifySettings): + await self.update_notify_settings(update) else: self.log.trace("Unhandled update: %s", update) + async def update_folder_peers(self, update: UpdateFolderPeers) -> None: + pass + + async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None: + pass + + async def update_notify_settings(self, update: UpdateNotifySettings) -> None: + pass + async def update_pinned_messages(self, update: Union[UpdatePinnedMessages, UpdatePinnedChannelMessages]) -> None: if isinstance(update, UpdatePinnedMessages): diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index f2a5f97c..f43b0885 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -132,6 +132,9 @@ class Config(BaseBridgeConfig): copy("bridge.delivery_receipts") copy("bridge.delivery_error_reports") copy("bridge.resend_bridge_info") + copy("bridge.mute_bridging") + copy("bridge.pinned_tag") + copy("bridge.archive_tag") copy("bridge.backfill.invite_own_puppet") copy("bridge.backfill.takeout_limit") copy("bridge.backfill.initial_limit") diff --git a/mautrix_telegram/example-config.yaml b/mautrix_telegram/example-config.yaml index 5fb1540f..c074751f 100644 --- a/mautrix_telegram/example-config.yaml +++ b/mautrix_telegram/example-config.yaml @@ -273,6 +273,13 @@ bridge: # This field will automatically be changed back to false after it, # except if the config file is not writable. resend_bridge_info: false + # When using double puppeting, should muted chats be muted in Matrix? + mute_bridging: false + # When using double puppeting, should pinned chats be moved to a specific tag in Matrix? + # The favorites tag is `m.favourite`. + pinned_tag: null + # Same as above for archived chats, the low priority tag is `m.lowpriority`. + archive_tag: null # Settings for backfilling messages from Telegram. backfill: # Whether or not the Telegram ghosts of logged in Matrix users should be diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 2216d817..83c17753 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -1,5 +1,5 @@ # mautrix-telegram - A Matrix-Telegram puppeting bridge -# Copyright (C) 2020 Tulir Asokan +# Copyright (C) 2021 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 @@ -16,20 +16,22 @@ from typing import (Awaitable, Dict, List, Iterable, NamedTuple, Optional, Tuple, Any, cast, TYPE_CHECKING) from collections import defaultdict +from datetime import datetime, timezone import logging import asyncio from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage, UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat, - ChatForbidden) + ChatForbidden, UpdateFolderPeers, UpdatePinnedDialogs, + UpdateNotifySettings, NotifyPeer) from telethon.tl.custom import Dialog from telethon.tl.types.contacts import ContactsNotModified from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest from telethon.tl.functions.account import UpdateStatusRequest from mautrix.client import Client -from mautrix.errors import MatrixRequestError -from mautrix.types import UserID, RoomID +from mautrix.errors import MatrixRequestError, MNotFound +from mautrix.types import UserID, RoomID, PushRuleScope, PushRuleKind, PushActionType, RoomTagInfo from mautrix.bridge import BaseUser from mautrix.util.logging import TraceLogger from mautrix.util.opt_prometheus import Gauge @@ -376,6 +378,61 @@ class User(AbstractUser, BaseUser): if portal.mxid } + async def _tag_room(self, puppet: pu.Puppet, portal: po.Portal, tag: str, active: bool + ) -> None: + if not tag or not portal or not portal.mxid: + return + tag_info = await puppet.intent.get_room_tag(portal.mxid, tag) + if active and tag_info is None: + tag_info = RoomTagInfo(order=0.5) + tag_info[self.bridge.real_user_content_key] = True + await puppet.intent.set_room_tag(portal.mxid, tag, tag_info) + elif not active and tag_info and tag_info.get(self.bridge.real_user_content_key, False): + await puppet.intent.remove_room_tag(portal.mxid, tag) + + @staticmethod + async def _mute_room(puppet: pu.Puppet, portal: po.Portal, mute_until: datetime) -> None: + if not config["bridge.mute_bridging"] or not portal or not portal.mxid: + return + now = datetime.utcnow().replace(tzinfo=timezone.utc) + if mute_until is not None and mute_until > now: + await puppet.intent.set_push_rule(PushRuleScope.GLOBAL, PushRuleKind.ROOM, portal.mxid, + actions=[PushActionType.DONT_NOTIFY]) + else: + try: + await puppet.intent.remove_push_rule(PushRuleScope.GLOBAL, PushRuleKind.ROOM, + portal.mxid) + except MNotFound: + pass + + async def update_folder_peers(self, update: UpdateFolderPeers) -> None: + puppet = await pu.Puppet.get_by_custom_mxid(self.mxid) + if not puppet or not puppet.is_real_user: + return + for peer in update.folder_peers: + portal = po.Portal.get_by_entity(peer.peer, receiver_id=self.tgid, create=False) + await self._tag_room(puppet, portal, config["bridge.archive_tag"], + peer.folder_id == 1) + + async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None: + puppet = await pu.Puppet.get_by_custom_mxid(self.mxid) + if not puppet or not puppet.is_real_user: + return + # TODO bridge unpinning properly + for pinned in update.order: + portal = po.Portal.get_by_entity(pinned.peer, receiver_id=self.tgid, create=False) + await self._tag_room(puppet, portal, config["bridge.pinned_tag"], True) + + async def update_notify_settings(self, update: UpdateNotifySettings) -> None: + if not isinstance(update.peer, NotifyPeer): + # TODO handle global notification setting changes? + return + puppet = await pu.Puppet.get_by_custom_mxid(self.mxid) + if not puppet or not puppet.is_real_user: + return + portal = po.Portal.get_by_entity(update.peer.peer, receiver_id=self.tgid, create=False) + await self._mute_room(puppet, portal, update.notify_settings.mute_until) + async def _sync_dialog(self, portal: po.Portal, dialog: Dialog, should_create: bool, puppet: Optional[pu.Puppet]) -> None: if portal.mxid: @@ -403,6 +460,9 @@ class User(AbstractUser, BaseUser): dialog.dialog.read_inbox_max_id) if last_read: await puppet.intent.mark_read(last_read.mx_room, last_read.mxid) + await self._mute_room(puppet, portal, dialog.dialog.notify_settings.mute_until) + await self._tag_room(puppet, portal, config["bridge.pinned_tag"], dialog.pinned) + await self._tag_room(puppet, portal, config["bridge.archive_tag"], dialog.archived) async def sync_dialogs(self) -> None: if self.is_bot: diff --git a/requirements.txt b/requirements.txt index b75e2231..7b3ba0eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 yarl>=1,<2 -mautrix>=0.9,<0.10 +mautrix>=0.9.1,<0.10 telethon>=1.20,<1.22 telethon-session-sqlalchemy>=0.2.14,<0.3