Add option to bridge archive, pin and mute status from Telegram
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
# 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
|
# 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
|
# 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 <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
|
from typing import Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import platform
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import platform
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from telethon.sessions import Session
|
from telethon.sessions import Session
|
||||||
@@ -31,7 +31,8 @@ from telethon.tl.types import (
|
|||||||
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
|
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
|
||||||
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
|
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
|
||||||
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
|
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
|
||||||
UpdateReadChannelInbox, MessageEmpty)
|
UpdateReadChannelInbox, MessageEmpty, UpdateFolderPeers, UpdatePinnedDialogs,
|
||||||
|
UpdateNotifySettings)
|
||||||
|
|
||||||
from mautrix.types import UserID, PresenceState
|
from mautrix.types import UserID, PresenceState
|
||||||
from mautrix.errors import MatrixError
|
from mautrix.errors import MatrixError
|
||||||
@@ -235,8 +236,7 @@ class AbstractUser(ABC):
|
|||||||
# region Telegram update handling
|
# region Telegram update handling
|
||||||
|
|
||||||
async def _update(self, update: TypeUpdate) -> None:
|
async def _update(self, update: TypeUpdate) -> None:
|
||||||
asyncio.ensure_future(self._handle_entity_updates(getattr(update, "_entities", {})),
|
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {})))
|
||||||
loop=self.loop)
|
|
||||||
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
||||||
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
|
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
|
||||||
await self.update_message(update)
|
await self.update_message(update)
|
||||||
@@ -260,9 +260,24 @@ class AbstractUser(ABC):
|
|||||||
await self.update_read_receipt(update)
|
await self.update_read_receipt(update)
|
||||||
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
|
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
|
||||||
await self.update_own_read_receipt(update)
|
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:
|
else:
|
||||||
self.log.trace("Unhandled update: %s", update)
|
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,
|
async def update_pinned_messages(self, update: Union[UpdatePinnedMessages,
|
||||||
UpdatePinnedChannelMessages]) -> None:
|
UpdatePinnedChannelMessages]) -> None:
|
||||||
if isinstance(update, UpdatePinnedMessages):
|
if isinstance(update, UpdatePinnedMessages):
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.delivery_receipts")
|
copy("bridge.delivery_receipts")
|
||||||
copy("bridge.delivery_error_reports")
|
copy("bridge.delivery_error_reports")
|
||||||
copy("bridge.resend_bridge_info")
|
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.invite_own_puppet")
|
||||||
copy("bridge.backfill.takeout_limit")
|
copy("bridge.backfill.takeout_limit")
|
||||||
copy("bridge.backfill.initial_limit")
|
copy("bridge.backfill.initial_limit")
|
||||||
|
|||||||
@@ -273,6 +273,13 @@ bridge:
|
|||||||
# This field will automatically be changed back to false after it,
|
# This field will automatically be changed back to false after it,
|
||||||
# except if the config file is not writable.
|
# except if the config file is not writable.
|
||||||
resend_bridge_info: false
|
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.
|
# Settings for backfilling messages from Telegram.
|
||||||
backfill:
|
backfill:
|
||||||
# Whether or not the Telegram ghosts of logged in Matrix users should be
|
# Whether or not the Telegram ghosts of logged in Matrix users should be
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
# 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
|
# 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
|
# 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,
|
from typing import (Awaitable, Dict, List, Iterable, NamedTuple, Optional, Tuple, Any, cast,
|
||||||
TYPE_CHECKING)
|
TYPE_CHECKING)
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
|
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
|
||||||
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
|
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
|
||||||
ChatForbidden)
|
ChatForbidden, UpdateFolderPeers, UpdatePinnedDialogs,
|
||||||
|
UpdateNotifySettings, NotifyPeer)
|
||||||
from telethon.tl.custom import Dialog
|
from telethon.tl.custom import Dialog
|
||||||
from telethon.tl.types.contacts import ContactsNotModified
|
from telethon.tl.types.contacts import ContactsNotModified
|
||||||
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
||||||
from telethon.tl.functions.account import UpdateStatusRequest
|
from telethon.tl.functions.account import UpdateStatusRequest
|
||||||
|
|
||||||
from mautrix.client import Client
|
from mautrix.client import Client
|
||||||
from mautrix.errors import MatrixRequestError
|
from mautrix.errors import MatrixRequestError, MNotFound
|
||||||
from mautrix.types import UserID, RoomID
|
from mautrix.types import UserID, RoomID, PushRuleScope, PushRuleKind, PushActionType, RoomTagInfo
|
||||||
from mautrix.bridge import BaseUser
|
from mautrix.bridge import BaseUser
|
||||||
from mautrix.util.logging import TraceLogger
|
from mautrix.util.logging import TraceLogger
|
||||||
from mautrix.util.opt_prometheus import Gauge
|
from mautrix.util.opt_prometheus import Gauge
|
||||||
@@ -376,6 +378,61 @@ class User(AbstractUser, BaseUser):
|
|||||||
if portal.mxid
|
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,
|
async def _sync_dialog(self, portal: po.Portal, dialog: Dialog, should_create: bool,
|
||||||
puppet: Optional[pu.Puppet]) -> None:
|
puppet: Optional[pu.Puppet]) -> None:
|
||||||
if portal.mxid:
|
if portal.mxid:
|
||||||
@@ -403,6 +460,9 @@ class User(AbstractUser, BaseUser):
|
|||||||
dialog.dialog.read_inbox_max_id)
|
dialog.dialog.read_inbox_max_id)
|
||||||
if last_read:
|
if last_read:
|
||||||
await puppet.intent.mark_read(last_read.mx_room, last_read.mxid)
|
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:
|
async def sync_dialogs(self) -> None:
|
||||||
if self.is_bot:
|
if self.is_bot:
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@ python-magic>=0.4,<0.5
|
|||||||
commonmark>=0.8,<0.10
|
commonmark>=0.8,<0.10
|
||||||
aiohttp>=3,<4
|
aiohttp>=3,<4
|
||||||
yarl>=1,<2
|
yarl>=1,<2
|
||||||
mautrix>=0.9,<0.10
|
mautrix>=0.9.1,<0.10
|
||||||
telethon>=1.20,<1.22
|
telethon>=1.20,<1.22
|
||||||
telethon-session-sqlalchemy>=0.2.14,<0.3
|
telethon-session-sqlalchemy>=0.2.14,<0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user