diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index c1de7c6e..9fb9b795 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -672,7 +672,7 @@ class AbstractUser(ABC): if isinstance(update, MessageService): if isinstance(update.action, MessageActionChannelMigrateFrom): - self.log.trace( + self.log.debug( "Received %s in %s by %d, unregistering portal...", update.action, portal.tgid_log, diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index a44de293..16527d73 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -2580,7 +2580,7 @@ class Portal(DBPortal, BasePortal): self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message ) -> None: if not self.mxid: - self.log.trace("Ignoring edit to %d as chat has no Matrix room", evt.id) + self.log.debug("Ignoring edit to %d as chat has no Matrix room", evt.id) return elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame): self.log.debug("Ignoring game message edit event") @@ -2892,7 +2892,14 @@ class Portal(DBPortal, BasePortal): if sender: await add_member(intent, sender.displayname, sender.avatar_url) is_bot = sender.is_bot if sender else False - converted = await self._msg_conv.convert(source, intent, is_bot, msg, client=client) + converted = await self._msg_conv.convert( + source, + intent, + is_bot, + msg, + client=client, + deterministic_reply_id=self.bridge.homeserver_software.is_hungry, + ) return converted, intent async def _wrap_batch_msg( @@ -2901,6 +2908,7 @@ class Portal(DBPortal, BasePortal): msg: Message, converted: putil.ConvertedMessage, caption: bool = False, + event_id: EventID | None = None, ) -> BatchSendEvent: if caption: content = converted.caption @@ -2917,6 +2925,7 @@ class Portal(DBPortal, BasePortal): timestamp=int(msg.date.timestamp() * 1000), content=content, type=event_type, + event_id=event_id, ) async def _backfill_messages( @@ -2940,6 +2949,7 @@ class Portal(DBPortal, BasePortal): else set() ) before_first_msg_timestamp = 0 + tg_space = self.tgid if self.peer_type == "channel" else source.tgid async def add_member(intent: IntentAPI, displayname: str, avatar_url: ContentURI) -> None: if self.bridge.homeserver_software.is_hungry or intent.mxid in added_members: @@ -2993,7 +3003,10 @@ class Portal(DBPortal, BasePortal): converted, intent = await self._convert_batch_msg(source, client, msg, add_member) if converted is None: continue - events.append(await self._wrap_batch_msg(intent, msg, converted)) + d_event_id = None + if self.bridge.homeserver_software.is_hungry: + d_event_id = self._msg_conv.deterministic_event_id(tg_space, msg.id) + events.append(await self._wrap_batch_msg(intent, msg, converted, event_id=d_event_id)) intents.append(intent) metas.append(msg) if converted.caption: diff --git a/mautrix_telegram/portal_util/message_convert.py b/mautrix_telegram/portal_util/message_convert.py index c59e649a..fd657eee 100644 --- a/mautrix_telegram/portal_util/message_convert.py +++ b/mautrix_telegram/portal_util/message_convert.py @@ -18,6 +18,7 @@ from __future__ import annotations from typing import Any, NamedTuple import base64 import codecs +import hashlib import html import mimetypes import unicodedata @@ -63,6 +64,7 @@ from telethon.utils import decode_waveform from mautrix.appservice import IntentAPI from mautrix.types import ( + EventID, EventType, Format, ImageInfo, @@ -150,6 +152,7 @@ class TelegramMessageConverter: is_bot: bool, evt: Message, no_reply_fallback: bool = False, + deterministic_reply_id: bool = False, client: MautrixTelegramClient | None = None, ) -> ConvertedMessage | None: if not client: @@ -180,7 +183,13 @@ class TelegramMessageConverter: converted.caption.external_url = converted.content.external_url if self.portal.get_config("caption_in_message"): self._caption_to_message(converted) - await self._set_reply(source, evt, converted.content, no_fallback=no_reply_fallback) + await self._set_reply( + source, + evt, + converted.content, + no_fallback=no_reply_fallback, + deterministic_id=deterministic_reply_id, + ) return converted @staticmethod @@ -227,12 +236,19 @@ class TelegramMessageConverter: raise ValueError("Portal has invalid peer type") return base64.b64encode(play_id).decode("utf-8").rstrip("=") + def deterministic_event_id(self, space: TelegramID, msg_id: TelegramID) -> EventID: + hash_content = f"{self.portal.mxid}/telegram/{space}/{msg_id}" + hashed = hashlib.sha256(hash_content.encode("utf-8")).digest() + b64hash = base64.urlsafe_b64encode(hashed).decode("utf-8").rstrip("=") + return EventID(f"${b64hash}:telegram.org") + async def _set_reply( self, source: au.AbstractUser, evt: Message, content: MessageEventContent, no_fallback: bool = False, + deterministic_id: bool = False, ) -> None: if not evt.reply_to: return @@ -241,8 +257,11 @@ class TelegramMessageConverter: if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel) else source.tgid ) - msg = await DBMessage.get_one_by_tgid(TelegramID(evt.reply_to.reply_to_msg_id), space) + reply_to_id = TelegramID(evt.reply_to.reply_to_msg_id) + msg = await DBMessage.get_one_by_tgid(reply_to_id, space) if not msg or msg.mx_room != self.portal.mxid: + if deterministic_id: + content.set_reply(self.deterministic_event_id(space, reply_to_id)) return elif not isinstance(content, TextMessageEventContent) or no_fallback: # Not a text message, just set the reply metadata and return diff --git a/requirements.txt b/requirements.txt index 36efcec8..f3188c4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ python-magic>=0.4,<0.5 commonmark>=0.8,<0.10 aiohttp>=3,<4 yarl>=1,<2 -mautrix>=0.18.6,<0.19 +mautrix>=0.18.8,<0.19 #telethon>=1.25.4,<1.26 tulir-telethon==1.26.0a10 asyncpg>=0.20,<0.27