diff --git a/example-config.yaml b/example-config.yaml index 2e697f97..cadd50c8 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -120,18 +120,27 @@ bridge: # Telegram doesn't have built-in emotes, so the m.emote format is also used for non-relaybot users. # # Available variables: - # $sender_display_name - The display name of the sender (e.g. Example User) + # $sender_displayname - The display name of the sender (e.g. Example User) # $sender_username - The username (Matrix ID localpart) of the sender (e.g. exampleuser) # $sender_mxid - The Matrix ID of the sender (e.g. @exampleuser:example.com) # $message - The message content as HTML message_formats: - m.text: "<$sender_display_name> $message" - m.emote: "* $sender_display_name $message" - m.file: "$sender_display_name sent a file: $message" - m.image: "$sender_display_name sent an image: $message" - m.audio: "$sender_display_name sent an audio file: $message" - m.video: "$sender_display_name sent a video: $message" - m.location: "$sender_display_name sent a location: $message" + m.text: "$sender_displayname: $message" + m.emote: "* $sender_displayname $message" + m.file: "$sender_displayname sent a file: $message" + m.image: "$sender_displayname sent an image: $message" + m.audio: "$sender_displayname sent an audio file: $message" + m.video: "$sender_displayname sent a video: $message" + m.location: "$sender_displayname sent a location: $message" + + # The format sto use when sending state events to Telegram via the relay bot. + # + # Variables from `message_formats` that have the `sender_` prefix are available without the prefix. + # In name_change events, `$prev_displayname` is the previous displayname. + state_event_formats: + join: "$displayname joined the room." + leave: "$displayname left the room." + name_change: "$prev_displayname changed their name to $displayname" filter: # Filter mode to use. Either "blacklist" or "whitelist". diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index a26dd8ef..ec922e13 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -182,6 +182,9 @@ class Config(DictWithRecursion): copy("bridge.catch_up") copy_dict("bridge.message_formats") + copy("bridge.state_event_formats.join") + copy("bridge.state_event_formats.leave") + copy("bridge.state_event_formats.name_change") copy("bridge.filter.mode") copy("bridge.filter.list") diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 0a3bd34f..662cf48a 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import logging import asyncio +import re from mautrix_appservice import MatrixRequestError, IntentError @@ -264,6 +265,15 @@ class MatrixHandler: # All pinned events removed, remove pinned event in Telegram. await portal.handle_matrix_pin(sender, None) + async def handle_name_change(self, room, user, displayname, prev_displayname, event_id): + portal = Portal.get_by_mxid(room) + if not portal or not portal.has_bot: + return + + user = await User.get_by_mxid(user).ensure_started() + if await user.needs_relaybot(portal): + await portal.name_change_matrix(user, displayname, prev_displayname, event_id) + def filter_matrix_event(self, event): return (event["sender"] == self.az.bot_mxid or Puppet.get_id_from_mxid(event["sender"]) is not None) @@ -273,36 +283,43 @@ class MatrixHandler: return self.log.debug("Received event: %s", evt) type = evt["type"] + room_id = evt["room_id"] + event_id = evt["event_id"] + sender = evt["sender"] content = evt.get("content", {}) if type == "m.room.member": + state_key = evt["state_key"] prev_content = evt.get("unsigned", {}).get("prev_content", {}) membership = content.get("membership", "") prev_membership = prev_content.get("membership", "leave") if membership == prev_membership: - # TODO handle displayname/avatar changes - pass + match = re.compile("@(.+):(.+)").match(state_key) + localpart = match.group(1) + displayname = content.get("displayname", localpart) + prev_displayname = prev_content.get("displayname", localpart) + if displayname != prev_displayname: + await self.handle_name_change(room_id, state_key, displayname, + prev_displayname, event_id) elif membership == "invite": - await self.handle_invite(evt["room_id"], evt["state_key"], evt["sender"]) + await self.handle_invite(room_id, state_key, sender) elif prev_membership == "join" and membership == "leave": - await self.handle_part(evt["room_id"], evt["state_key"], evt["sender"], - evt["event_id"]) + await self.handle_part(room_id, state_key, sender, event_id) elif membership == "join": - await self.handle_join(evt["room_id"], evt["state_key"], evt["event_id"]) + await self.handle_join(room_id, state_key, event_id) elif type in ("m.room.message", "m.sticker"): if type != "m.room.message": content["msgtype"] = type - await self.handle_message(evt["room_id"], evt["sender"], content, evt["event_id"]) + await self.handle_message(room_id, sender, content, event_id) elif type == "m.room.redaction": - await self.handle_redaction(evt["room_id"], evt["sender"], evt["redacts"]) + await self.handle_redaction(room_id, sender, evt["redacts"]) elif type == "m.room.power_levels": - await self.handle_power_levels(evt["room_id"], evt["sender"], evt["content"], - evt["prev_content"]) + await self.handle_power_levels(room_id, sender, evt["content"], evt["prev_content"]) elif type in ("m.room.name", "m.room.avatar", "m.room.topic"): - await self.handle_room_meta(type, evt["room_id"], evt["sender"], evt["content"]) + await self.handle_room_meta(type, room_id, sender, evt["content"]) elif type == "m.room.pinned_events": new_events = set(evt["content"]["pinned"]) try: old_events = set(evt["unsigned"]["prev_content"]["pinned"]) except KeyError: old_events = set() - await self.handle_room_pin(evt["room_id"], evt["sender"], new_events, old_events) + await self.handle_room_pin(room_id, sender, new_events, old_events) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index dbb2dcfc..1459b3f4 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -618,11 +618,42 @@ class Portal: else: return "" + async def _get_state_change_message(self, event, user, arguments={}): + tpl = config[f"bridge.state_event_formats.{event}"] + displayname = await self.get_displayname(user) + + tpl_args = dict(mxid=user.mxid, + username=user.mxid_localpart, + displayname=displayname) + tpl_args = {**tpl_args, **arguments} + message = Template(tpl).safe_substitute(tpl_args) + return { + "format": "org.matrix.custom.html", + "formatted_body": message, + } + + async def name_change_matrix(self, user, displayname, prev_displayname, event_id): + async with self.require_send_lock(self.bot.tgid): + message = await self._get_state_change_message( + "name_change", user, + dict(displayname=displayname, prev_displayname=prev_displayname)) + response = await self.bot.client.send_message( + self.peer, message, + parse_mode=self._matrix_event_to_entities) + space = self.tgid if self.peer_type == "channel" else self.bot.tgid + self.is_duplicate(response, (event_id, space)) + + async def get_displayname(self, user): + return (await self.main_intent.get_displayname(self.mxid, user.mxid) + or user.mxid_localpart) + async def leave_matrix(self, user, source, event_id): if await user.needs_relaybot(self): async with self.require_send_lock(self.bot.tgid): + message = await self._get_state_change_message("leave", user) response = await self.bot.client.send_message( - self.peer, f"__{user.displayname} left the room.__") + self.peer, message, + parse_mode=self._matrix_event_to_entities) space = self.tgid if self.peer_type == "channel" else self.bot.tgid self.is_duplicate(response, (event_id, space)) return @@ -653,8 +684,10 @@ class Portal: async def join_matrix(self, user, event_id): if await user.needs_relaybot(self): async with self.require_send_lock(self.bot.tgid): + message = await self._get_state_change_message("join", user) response = await self.bot.client.send_message( - self.peer, f"__{user.displayname} joined the room.__") + self.peer, message, + parse_mode=self._matrix_event_to_entities) space = self.tgid if self.peer_type == "channel" else self.bot.tgid self.is_duplicate(response, (event_id, space)) return @@ -665,29 +698,28 @@ class Portal: # We'll just assume the user is already in the chat. pass - @staticmethod - def _apply_msg_format(sender, msgtype, message): + async def _apply_msg_format(self, sender, msgtype, message): if "formatted_body" not in message: message["format"] = "org.matrix.custom.html" - message["formatted_body"] = escape_html(message["body"]) + message["formatted_body"] = escape_html(message.get("body", "")) body = message["formatted_body"] tpl = config["bridge.message_formats"].get(msgtype, "<$sender_display_name> $message") + displayname = await self.get_displayname(sender) tpl_args = dict(sender_mxid=sender.mxid, sender_username=sender.mxid_localpart, - sender_display_name=sender.displayname, + sender_displayname=displayname, message=body) message["formatted_body"] = Template(tpl).safe_substitute(tpl_args) - @classmethod - def _preprocess_matrix_message(cls, sender, use_relaybot, message): - msgtype = message["msgtype"] + async def _preprocess_matrix_message(self, sender, use_relaybot, message): + msgtype = message.get("msgtype", "m.text") if msgtype == "m.emote": - cls._apply_msg_format(sender, msgtype, message) + await self._apply_msg_format(sender, msgtype, message) message["msgtype"] = "m.text" elif use_relaybot: - cls._apply_msg_format(sender, msgtype, message) + await self._apply_msg_format(sender, msgtype, message) def _matrix_event_to_entities(self, event): try: @@ -792,7 +824,7 @@ class Portal: reply_to = formatter.matrix_reply_to_telegram(message, space, room_id=self.mxid) message["mxtg_filename"] = message["body"] - self._preprocess_matrix_message(sender, not logged_in, message) + await self._preprocess_matrix_message(sender, not logged_in, message) type = message["msgtype"] if type == "m.text" or (self.bridge_notices and type == "m.notice"): diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 8fa72233..06418203 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -68,9 +68,9 @@ class User(AbstractUser): match = re.compile("@(.+):(.+)").match(self.mxid) return match.group(1) + # TODO replace with proper displayname getting everywhere @property def displayname(self): - # TODO show better display name return self.mxid_localpart @property