Compare commits

..

10 Commits

Author SHA1 Message Date
Tulir Asokan 570372fa83 Bump version to 0.10.0 2021-06-14 19:45:00 +03:00
Tulir Asokan 5ed09ad783 Fix Telegram->Matrix typing notifications 2021-06-10 15:44:12 +03:00
Tulir Asokan c385aa0b8d Add real-time bridge status push option 2021-06-09 20:04:17 +03:00
Tulir Asokan ec152cbd9d Pass through Telegram gif meta as custom fields 2021-05-24 16:04:53 +03:00
Tulir Asokan b36fc35e04 Don't remove zero-width joiners from middle of displaynames 2021-05-23 16:28:26 +03:00
Tulir Asokan 198e77cae9 Remove commented edge things from dockerfile 2021-05-13 14:56:09 +03:00
Tulir Asokan 9c4beb29a5 Send m.bridge data when bridging existing room to Telegram 2021-05-12 19:21:37 +03:00
Tulir Asokan 6accb530c6 Add option to only bridge mute status and tags when creating portal 2021-04-29 12:09:54 +03:00
Tulir Asokan 1a77ba5fcd Add option to bridge archive, pin and mute status from Telegram 2021-04-20 14:52:19 +03:00
Tulir Asokan 7e9dd8b895 Update mautrix-python 2021-04-16 15:27:56 +03:00
14 changed files with 185 additions and 46 deletions
+1 -6
View File
@@ -2,11 +2,6 @@ FROM dock.mau.dev/tulir/lottieconverter:alpine-3.13
ARG TARGETARCH=amd64 ARG TARGETARCH=amd64
#RUN echo $'\
#@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
#@edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
#@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \ python3 py3-pip py3-setuptools py3-wheel \
py3-virtualenv \ py3-virtualenv \
@@ -27,7 +22,7 @@ RUN apk add --no-cache \
py3-requests \ py3-requests \
#imageio #imageio
py3-numpy \ py3-numpy \
#py3-telethon@edge \ (outdated) #py3-telethon \ (outdated)
# Optional for socks proxies # Optional for socks proxies
py3-pysocks \ py3-pysocks \
# cryptg # cryptg
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.10.0rc1" __version__ = "0.10.0"
__author__ = "Tulir Asokan <tulir@maunium.net>" __author__ = "Tulir Asokan <tulir@maunium.net>"
+38 -11
View File
@@ -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, UpdateChannelUserTyping)
from mautrix.types import UserID, PresenceState from mautrix.types import UserID, PresenceState
from mautrix.errors import MatrixError from mautrix.errors import MatrixError
@@ -57,6 +58,7 @@ MAX_DELETIONS: int = 10
UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage, UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage] UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService] UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService]
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates", UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates",
("update_type",)) ("update_type",))
@@ -235,8 +237,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)
@@ -244,7 +245,7 @@ class AbstractUser(ABC):
await self.delete_message(update) await self.delete_message(update)
elif isinstance(update, UpdateDeleteChannelMessages): elif isinstance(update, UpdateDeleteChannelMessages):
await self.delete_channel_message(update) await self.delete_channel_message(update)
elif isinstance(update, (UpdateChatUserTyping, UpdateUserTyping)): elif isinstance(update, (UpdateChatUserTyping, UpdateChannelUserTyping, UpdateUserTyping)):
await self.update_typing(update) await self.update_typing(update)
elif isinstance(update, UpdateUserStatus): elif isinstance(update, UpdateUserStatus):
await self.update_status(update) await self.update_status(update)
@@ -260,9 +261,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):
@@ -330,16 +346,27 @@ class AbstractUser(ABC):
await portal.set_telegram_admin(TelegramID(update.user_id)) await portal.set_telegram_admin(TelegramID(update.user_id))
async def update_typing(self, update: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None: async def update_typing(self, update: UpdateTyping) -> None:
sender = None
if isinstance(update, UpdateUserTyping): if isinstance(update, UpdateUserTyping):
portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user") portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user")
else: sender = pu.Puppet.get(TelegramID(update.user_id))
elif isinstance(update, UpdateChannelUserTyping):
portal = po.Portal.get_by_tgid(TelegramID(update.channel_id))
elif isinstance(update, UpdateChatUserTyping):
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id)) portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
else:
if not portal or not portal.mxid: return
if isinstance(update, (UpdateChannelUserTyping, UpdateChatUserTyping)):
# Can typing notifications come from non-user peers?
if not update.from_id.user_id:
return
sender = pu.Puppet.get(TelegramID(update.from_id.user_id))
if not sender or not portal or not portal.mxid:
return return
sender = pu.Puppet.get(TelegramID(update.user_id))
await portal.handle_telegram_typing(sender, update) await portal.handle_telegram_typing(sender, update)
async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]] async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]]
@@ -186,6 +186,7 @@ async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id
portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id) portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
portal.photo_id = "" portal.photo_id = ""
await portal.save() await portal.save()
await portal.update_bridge_info()
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels), asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
loop=evt.loop) loop=evt.loop)
+4
View File
@@ -132,6 +132,10 @@ 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.tag_only_on_create")
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")
+15
View File
@@ -8,6 +8,12 @@ homeserver:
# Only applies if address starts with https:// # Only applies if address starts with https://
verify_ssl: true verify_ssl: true
asmux: false asmux: false
# Number of retries for all HTTP requests if the homeserver isn't reachable.
http_retry_count: 4
# The URL to push real-time bridge status to.
# If set, the bridge will make POST requests to this URL whenever a user's Telegram connection state changes.
# The bridge will use the appservice as_token to authorize requests.
status_endpoint: null
# Application service host/registration related details # Application service host/registration related details
# Changing these values requires regeneration of the registration. # Changing these values requires regeneration of the registration.
@@ -271,6 +277,15 @@ 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
# Whether or not mute status and tags should only be bridged when the portal room is created.
tag_only_on_create: true
# 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
View File
@@ -109,6 +109,7 @@ class MatrixHandler(BaseMatrixHandler):
if e2be_ok is False: if e2be_ok is False:
message += "\n\nWarning: Failed to enable end-to-bridge encryption" message += "\n\nWarning: Failed to enable end-to-bridge encryption"
await intent.send_notice(room_id, message) await intent.send_notice(room_id, message)
await portal.update_bridge_info()
else: else:
await intent.join_room(room_id) await intent.join_room(room_id)
await intent.send_notice(room_id, "This puppet will remain inactive until a " await intent.send_notice(room_id, "This puppet will remain inactive until a "
+1
View File
@@ -170,6 +170,7 @@ class PortalMetadata(BasePortal, ABC):
levels = self._get_base_power_levels(levels, entity) levels = self._get_base_power_levels(levels, entity)
await self.main_intent.set_power_levels(self.mxid, levels) await self.main_intent.set_power_levels(self.mxid, levels)
await self.handle_matrix_power_levels(source, levels.users, {}, None) await self.handle_matrix_power_levels(source, levels.users, {}, None)
await self.update_bridge_info()
async def invite_telegram(self, source: 'u.User', async def invite_telegram(self, source: 'u.User',
puppet: Union[p.Puppet, 'AbstractUser']) -> None: puppet: Union[p.Puppet, 'AbstractUser']) -> None:
+17 -6
View File
@@ -34,7 +34,8 @@ from telethon.tl.types import (
MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser, MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser,
PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute, PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute,
TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping, TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping,
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize) MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize, DocumentAttributeAnimated,
UpdateChannelUserTyping, SendMessageTypingAction)
from mautrix.appservice import IntentAPI from mautrix.appservice import IntentAPI
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType, from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
@@ -56,16 +57,18 @@ if TYPE_CHECKING:
InviteList = Union[UserID, List[UserID]] InviteList = Union[UserID, List[UserID]]
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant] TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
DocAttrs = NamedTuple("DocAttrs", name=Optional[str], mime_type=Optional[str], is_sticker=bool, DocAttrs = NamedTuple("DocAttrs", name=Optional[str], mime_type=Optional[str], is_sticker=bool,
sticker_alt=Optional[str], width=int, height=int) sticker_alt=Optional[str], width=int, height=int, is_gif=bool)
config: Optional['Config'] = None config: Optional['Config'] = None
class PortalTelegram(BasePortal, ABC): class PortalTelegram(BasePortal, ABC):
async def handle_telegram_typing(self, user: p.Puppet, async def handle_telegram_typing(self, user: p.Puppet, update: UpdateTyping) -> None:
_: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None: is_typing = isinstance(update.action, SendMessageTypingAction)
await user.intent_for(self).set_typing(self.mxid, is_typing=True) # Always use the default puppet here to avoid any problems with echoing
await user.default_mxid_intent.set_typing(self.mxid, is_typing=is_typing)
def _get_external_url(self, evt: Message) -> Optional[str]: def _get_external_url(self, evt: Message) -> Optional[str]:
if self.peer_type == "channel" and self.username is not None: if self.peer_type == "channel" and self.username is not None:
@@ -134,6 +137,7 @@ class PortalTelegram(BasePortal, ABC):
@staticmethod @staticmethod
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs: def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs:
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0 name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
is_gif = False
for attr in attributes: for attr in attributes:
if isinstance(attr, DocumentAttributeFilename): if isinstance(attr, DocumentAttributeFilename):
name = name or attr.file_name name = name or attr.file_name
@@ -141,11 +145,13 @@ class PortalTelegram(BasePortal, ABC):
elif isinstance(attr, DocumentAttributeSticker): elif isinstance(attr, DocumentAttributeSticker):
is_sticker = True is_sticker = True
sticker_alt = attr.alt sticker_alt = attr.alt
elif isinstance(attr, DocumentAttributeAnimated):
is_gif = True
elif isinstance(attr, DocumentAttributeVideo): elif isinstance(attr, DocumentAttributeVideo):
width, height = attr.w, attr.h width, height = attr.w, attr.h
elif isinstance(attr, DocumentAttributeImageSize): elif isinstance(attr, DocumentAttributeImageSize):
width, height = attr.w, attr.h width, height = attr.w, attr.h
return DocAttrs(name, mime_type, is_sticker, sticker_alt, width, height) return DocAttrs(name, mime_type, is_sticker, sticker_alt, width, height, is_gif)
@staticmethod @staticmethod
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs, def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs,
@@ -241,6 +247,11 @@ class PortalTelegram(BasePortal, ABC):
if info.thumbnail_info: if info.thumbnail_info:
info.thumbnail_info.width = info.width info.thumbnail_info.width = info.width
info.thumbnail_info.height = info.height info.thumbnail_info.height = info.height
if attrs.is_gif:
info["fi.mau.telegram.gif"] = True
info["fi.mau.loop"] = True
info["fi.mau.autoplay"] = True
info["fi.mau.no_audio"] = True
content = MediaMessageEventContent( content = MediaMessageEventContent(
body=name or "unnamed file", info=info, relates_to=relates_to, body=name or "unnamed file", info=info, relates_to=relates_to,
+3 -1
View File
@@ -209,7 +209,9 @@ class Puppet(BasePuppet):
whitespace = ("\t\n\r\v\f \u00a0\u034f\u180e\u2063\u202f\u205f\u2800\u3000\u3164\ufeff" whitespace = ("\t\n\r\v\f \u00a0\u034f\u180e\u2063\u202f\u205f\u2800\u3000\u3164\ufeff"
"\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b" "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b"
"\u200c\u200d\u200e\u200f\ufe0f") "\u200c\u200d\u200e\u200f\ufe0f")
name = "".join(c for c in name.strip(whitespace) if unicodedata.category(c) != 'Cf') allowed_other_format = ("\u200d", "\u200c")
name = "".join(c for c in name.strip(whitespace) if unicodedata.category(c) != 'Cf'
or c in allowed_other_format)
return name return name
@classmethod @classmethod
+98 -13
View File
@@ -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,22 +15,23 @@
# 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 (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 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, BridgeState
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
@@ -50,6 +51,11 @@ SearchResult = NamedTuple('SearchResult', puppet='pu.Puppet', similarity=int)
METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge') METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge')
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram') METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
BridgeState.human_readable_errors.update({
"tg-not-connected": "Your Telegram connection failed",
"logged-out": "You're not logged into Telegram",
})
class User(AbstractUser, BaseUser): class User(AbstractUser, BaseUser):
log: TraceLogger = logging.getLogger("mau.user") log: TraceLogger = logging.getLogger("mau.user")
@@ -72,8 +78,9 @@ class User(AbstractUser, BaseUser):
saved_contacts: int = 0, is_bot: bool = False, saved_contacts: int = 0, is_bot: bool = False,
db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None, db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None,
db_instance: Optional[DBUser] = None) -> None: db_instance: Optional[DBUser] = None) -> None:
super().__init__() AbstractUser.__init__(self)
self.mxid = mxid self.mxid = mxid
BaseUser.__init__(self)
self.tgid = tgid self.tgid = tgid
self.is_bot = is_bot self.is_bot = is_bot
self.username = username self.username = username
@@ -85,12 +92,8 @@ class User(AbstractUser, BaseUser):
self.db_portals = db_portals or [] self.db_portals = db_portals or []
self._db_instance = db_instance self._db_instance = db_instance
self._ensure_started_lock = asyncio.Lock() self._ensure_started_lock = asyncio.Lock()
self.dm_update_lock = asyncio.Lock()
self._metric_value = defaultdict(lambda: False)
self._track_connection_task = None self._track_connection_task = None
self.command_status = None
(self.relaybot_whitelisted, (self.relaybot_whitelisted,
self.whitelisted, self.whitelisted,
self.puppet_whitelisted, self.puppet_whitelisted,
@@ -102,8 +105,6 @@ class User(AbstractUser, BaseUser):
if tgid: if tgid:
self.by_tgid[tgid] = self self.by_tgid[tgid] = self
self.log = self.log.getChild(self.mxid)
@property @property
def name(self) -> str: def name(self) -> str:
return self.mxid return self.mxid
@@ -217,6 +218,21 @@ class User(AbstractUser, BaseUser):
connected = bool(self.client._sender._transport_connected connected = bool(self.client._sender._transport_connected
if self.client and self.client._sender else False) if self.client and self.client._sender else False)
self._track_metric(METRIC_CONNECTED, connected) self._track_metric(METRIC_CONNECTED, connected)
await self.push_bridge_state(ok=connected, ttl=3600 if connected else 240,
error="tg-not-connected" if not connected else None)
async def fill_bridge_state(self, state: BridgeState) -> None:
await super().fill_bridge_state(state)
state.remote_id = str(self.tgid)
state.remote_name = self.human_tg_id
async def get_bridge_state(self) -> BridgeState:
if not self.client:
return BridgeState(ok=False, error="logged-out")
elif not self.client._sender or not self.client._sender._transport_connected:
return BridgeState(ok=False, error="tg-not-connected")
else:
return BridgeState(ok=True)
async def stop(self) -> None: async def stop(self) -> None:
await super().stop() await super().stop()
@@ -224,6 +240,7 @@ class User(AbstractUser, BaseUser):
self._track_connection_task.cancel() self._track_connection_task.cancel()
self._track_connection_task = None self._track_connection_task = None
self._track_metric(METRIC_CONNECTED, False) self._track_metric(METRIC_CONNECTED, False)
await self.push_bridge_state(ok=False, error="tg-not-connected")
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None: async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
if config["metrics.enabled"] and not self._track_connection_task: if config["metrics.enabled"] and not self._track_connection_task:
@@ -328,6 +345,7 @@ class User(AbstractUser, BaseUser):
self.delete() self.delete()
await self.stop() await self.stop()
self._track_metric(METRIC_LOGGED_IN, False) self._track_metric(METRIC_LOGGED_IN, False)
await self.push_bridge_state(ok=False, error="logged-out")
return True return True
def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45 def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45
@@ -376,8 +394,70 @@ 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:
if config["bridge.tag_only_on_create"]:
return
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:
if config["bridge.tag_only_on_create"]:
return
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 config["bridge.tag_only_on_create"]:
return
elif 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:
was_created = False
if portal.mxid: if portal.mxid:
try: try:
await portal.backfill(self, last_id=dialog.message.id) await portal.backfill(self, last_id=dialog.message.id)
@@ -390,6 +470,7 @@ class User(AbstractUser, BaseUser):
elif should_create: elif should_create:
try: try:
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid]) await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
was_created = True
except Exception: except Exception:
self.log.exception(f"Error while creating {portal.tgid_log}") self.log.exception(f"Error while creating {portal.tgid_log}")
if portal.mxid and puppet and puppet.is_real_user: if portal.mxid and puppet and puppet.is_real_user:
@@ -403,6 +484,10 @@ 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)
if was_created or not config["bridge.tag_only_on_create"]:
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:
+2 -5
View File
@@ -30,7 +30,6 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio
SecurityError, FileIdInvalidError) SecurityError, FileIdInvalidError)
from mautrix.appservice import IntentAPI from mautrix.appservice import IntentAPI
from mautrix.util.network_retry import call_with_net_retry
from ..tgclient import MautrixTelegramClient from ..tgclient import MautrixTelegramClient
from ..db import TelegramFile as DBTelegramFile from ..db import TelegramFile as DBTelegramFile
@@ -145,8 +144,7 @@ async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: In
if encrypt: if encrypt:
file, decryption_info = encrypt_attachment(file) file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream" upload_mime_type = "application/octet-stream"
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type, content_uri = await intent.upload_media(file, upload_mime_type)
_action="upload media")
if decryption_info: if decryption_info:
decryption_info.url = content_uri decryption_info.url = content_uri
@@ -239,8 +237,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
if encrypt and encrypt_attachment: if encrypt and encrypt_attachment:
file, decryption_info = encrypt_attachment(file) file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream" upload_mime_type = "application/octet-stream"
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type, content_uri = await intent.upload_media(file, upload_mime_type)
_action="upload media")
if decryption_info: if decryption_info:
decryption_info.url = content_uri decryption_info.url = content_uri
+2 -2
View File
@@ -15,13 +15,13 @@ qrcode>=6,<7
moviepy>=1,<2 moviepy>=1,<2
#/metrics #/metrics
prometheus_client>=0.6,<0.11 prometheus_client>=0.6,<0.12
#/postgres #/postgres
psycopg2-binary>=2,<3 psycopg2-binary>=2,<3
#/e2be #/e2be
asyncpg>=0.20,<0.23 asyncpg>=0.20,<0.24
python-olm>=3,<4 python-olm>=3,<4
pycryptodome>=3,<4 pycryptodome>=3,<4
unpaddedbase64>=1,<2 unpaddedbase64>=1,<2
+1 -1
View File
@@ -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.8.11,<0.9 mautrix>=0.9.3,<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