fork of https://f-hub.org/XMPP/xmpp-discord-bridge

Keep track of discord/xmpp msg counterparts for reply context

+47 -11
+2 -1
setup.py
··· 12 "requests==2.27.1", 13 "slixmpp @ git+https://codeberg.org/poezio/slixmpp.git@fix-muc-join", 14 "nextcord", 15 - "toml==0.10.2" 16 ], 17 extra_require = { 18 "dev": [
··· 12 "requests==2.27.1", 13 "slixmpp @ git+https://codeberg.org/poezio/slixmpp.git@fix-muc-join", 14 "nextcord", 15 + "toml==0.10.2", 16 + "bidict" 17 ], 18 extra_require = { 19 "dev": [
+45 -10
xmpp_discord_bridge/main.py
··· 4 import asyncio 5 import signal 6 import base64 7 import hashlib 8 import hmac 9 import urllib.parse 10 from optparse import OptionParser 11 12 import toml 13 import slixmpp 14 from slixmpp.plugins.xep_0359 import StanzaID 15 from slixmpp.stanza import Message as XMPPMessage 16 from slixmpp.componentxmpp import ComponentXMPP 17 from slixmpp.exceptions import XMPPError, IqError ··· 50 self._real_muc_users = {} # MUC -> [Resources] 51 self._guild_map = {} # Guild ID -> Channel ID -> MUC 52 self._muc_map = {} # MUC -> (Guild ID, Channel ID) 53 - self._msg_map = {} # MUC -> Discord Message ID -> (XMPP Message ID, JID, MUC Nick, Body) 54 self._mucs = [] # List of known MUCs 55 self._webhooks = {} # MUC -> Webhook URL 56 ··· 65 66 register_stanza_plugin(XMPPMessage, OOBData) 67 register_stanza_plugin(XMPPMessage, StanzaID) 68 69 self.add_event_handler("session_start", self.on_session_start) 70 self.add_event_handler("groupchat_message", self.on_groupchat_message) ··· 135 136 # Initialise state tracking 137 self._muc_map[muc] = (guild, channel) 138 - self._msg_map[muc] = {} 139 self._virtual_muc_users[muc] = [] 140 self._virtual_muc_nicks[muc] = {} 141 for member in dchannel.members: ··· 241 if not message["body"]: 242 return 243 if not message["from"].resource in self._real_muc_users[muc]: 244 return 245 # Prevent the message being reflected back into Discord 246 if not message["to"] == self._bot_jid_full: ··· 254 for member in self._discord.get_guild(guild).get_channel(channel).members: 255 self._logger.debug("Checking %s", member.display_name) 256 if "@" + member.display_name in content: 257 - self._logger.debug("Found mention for %s. Replaceing.", 258 member.display_name) 259 content = content.replace("@" + member.display_name, 260 member.mention) ··· 272 # Makes it feel more native. 273 content = None 274 275 try: 276 webhook_msg: WebhookMessage = await self._webhooks[muc].send(content=content, 277 username=message["from"].resource, ··· 280 wait=True 281 ) 282 stanza_id = message["stanza_id"]["id"] 283 - self._msg_map[muc][webhook_msg.id] = (stanza_id, message["from"], message.get_mucnick(), content) 284 except Exception as err: 285 self._logger.error("Webhook execution failed", exc_info=err) 286 ··· 324 pstatus=pstatus 325 ) 326 self.plugin["xep_0045"].make_join_stanza(muc, 327 - nick=self._config["general"]["discord2xmpp_name_fmt"].format( 328 - display_name=member.display_name 329 - ), 330 presence_options=presence).send() 331 332 async def on_discord_member_join(self, member): ··· 467 else: 468 content = msg.clean_content 469 470 if msg.type == MessageType.reply and msg.reference: 471 # get xmpp message corresponding to replied-to discord message 472 discord_msg_id = msg.reference.message_id 473 - xmpp_msg_data = self._msg_map[muc].get(discord_msg_id) 474 - if xmpp_msg_data: 475 - xmpp_msg_id, xmpp_msg_from, xmpp_msg_from_nick, xmpp_msg_content = xmpp_msg_data 476 self.plugin["xep_0461"].send_reply( 477 xmpp_msg_from, 478 xmpp_msg_id,
··· 4 import asyncio 5 import signal 6 import base64 7 + from bidict import bidict 8 import hashlib 9 import hmac 10 + from typing import Dict 11 import urllib.parse 12 from optparse import OptionParser 13 14 import toml 15 import slixmpp 16 from slixmpp.plugins.xep_0359 import StanzaID 17 + from slixmpp.plugins.xep_0461.stanza import Reply 18 from slixmpp.stanza import Message as XMPPMessage 19 from slixmpp.componentxmpp import ComponentXMPP 20 from slixmpp.exceptions import XMPPError, IqError ··· 53 self._real_muc_users = {} # MUC -> [Resources] 54 self._guild_map = {} # Guild ID -> Channel ID -> MUC 55 self._muc_map = {} # MUC -> (Guild ID, Channel ID) 56 + self._msg_map: Dict[str, bidict] = {} # MUC -> Discord Message ID <-> XMPP Message ID 57 + self._xmpp_msg_cache = {} # XMPP Message ID -> (JID, MUC Nick, Body) 58 + self._discord_msg_cache = {} # Discord Message ID -> (Content, Author Mention) 59 self._mucs = [] # List of known MUCs 60 self._webhooks = {} # MUC -> Webhook URL 61 ··· 70 71 register_stanza_plugin(XMPPMessage, OOBData) 72 register_stanza_plugin(XMPPMessage, StanzaID) 73 + register_stanza_plugin(XMPPMessage, Reply) 74 75 self.add_event_handler("session_start", self.on_session_start) 76 self.add_event_handler("groupchat_message", self.on_groupchat_message) ··· 141 142 # Initialise state tracking 143 self._muc_map[muc] = (guild, channel) 144 + self._msg_map[muc] = bidict() 145 self._virtual_muc_users[muc] = [] 146 self._virtual_muc_nicks[muc] = {} 147 for member in dchannel.members: ··· 247 if not message["body"]: 248 return 249 if not message["from"].resource in self._real_muc_users[muc]: 250 + # We should cache our mirrored messages for future reference; stanza_id not known at time of sending 251 + stanza_id = message["stanza_id"]["id"] 252 + for discord_msg_id in self._discord_msg_cache.keys(): 253 + discord_content, discord_mention = self._discord_msg_cache[discord_msg_id] 254 + xmpp_body = message["body"] 255 + # remove reply fallback text for edge case in message content comparison 256 + if message["reply"]: 257 + xmpp_reply: Reply = message["reply"] 258 + xmpp_body = xmpp_reply.strip_fallback_content() 259 + # naive string comparison between our just-sent and newly-received xmpp message 260 + if discord_content == xmpp_body: 261 + self._msg_map[muc][discord_msg_id] = stanza_id 262 + self._logger.debug("Matched discord message %s with xmpp stanza_id %s", discord_msg_id, stanza_id) 263 + # content no longer needed, prevents duplicate matches 264 + self._discord_msg_cache[discord_msg_id] = (None, discord_mention) 265 return 266 # Prevent the message being reflected back into Discord 267 if not message["to"] == self._bot_jid_full: ··· 275 for member in self._discord.get_guild(guild).get_channel(channel).members: 276 self._logger.debug("Checking %s", member.display_name) 277 if "@" + member.display_name in content: 278 + self._logger.debug("Found mention for %s. Replacing.", 279 member.display_name) 280 content = content.replace("@" + member.display_name, 281 member.mention) ··· 293 # Makes it feel more native. 294 content = None 295 296 + if message["reply"]: 297 + xmpp_reply: Reply = message["reply"] 298 + discord_replied_id = self._msg_map[muc].inverse.get(xmpp_reply["id"]) 299 + if discord_replied_id and discord_replied_id in self._discord_msg_cache: 300 + _, mention = self._discord_msg_cache[discord_replied_id] 301 + # rewrite the fallback text to use a Discord-compatible ping 302 + fallback = xmpp_reply.get_fallback_body() 303 + if mention: 304 + fallback += mention + "\n" 305 + content = fallback + "\n" + xmpp_reply.strip_fallback_content() 306 + else: 307 + self._logger.warning("Cache miss for XMPP message with stanza id {}".format(xmpp_reply["id"])) 308 + 309 try: 310 webhook_msg: WebhookMessage = await self._webhooks[muc].send(content=content, 311 username=message["from"].resource, ··· 314 wait=True 315 ) 316 stanza_id = message["stanza_id"]["id"] 317 + self._msg_map[muc][webhook_msg.id] = stanza_id 318 + self._xmpp_msg_cache[stanza_id] = (message["from"], message.get_mucnick(), content) 319 except Exception as err: 320 self._logger.error("Webhook execution failed", exc_info=err) 321 ··· 359 pstatus=pstatus 360 ) 361 self.plugin["xep_0045"].make_join_stanza(muc, 362 + nick=member.display_name, 363 presence_options=presence).send() 364 365 async def on_discord_member_join(self, member): ··· 500 else: 501 content = msg.clean_content 502 503 + self._discord_msg_cache[msg.id] = (content, msg.author.mention) 504 + 505 if msg.type == MessageType.reply and msg.reference: 506 # get xmpp message corresponding to replied-to discord message 507 discord_msg_id = msg.reference.message_id 508 + xmpp_msg_id = self._msg_map[muc].get(discord_msg_id) 509 + if xmpp_msg_id: 510 + xmpp_msg_from, xmpp_msg_from_nick, xmpp_msg_content = self._xmpp_msg_cache[xmpp_msg_id] 511 self.plugin["xep_0461"].send_reply( 512 xmpp_msg_from, 513 xmpp_msg_id,