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

Pull apart the main file

+156 -136
+62
xmpp_discord_bridge/avatar.py
··· 1 + import logging 2 + 3 + from slixmpp.exceptions import IqError 4 + 5 + class AvatarManager: 6 + def __init__(self, xmpp, config): 7 + self._xmpp = xmpp 8 + self._path = config["avatars"]["path"] 9 + self._public = config["avatars"]["url"] 10 + 11 + self._avatars = {} 12 + 13 + self._logger = logging.getLogger("xmpp.avatar") 14 + 15 + def save_avatar(self, jid, data, type_): 16 + filename = hashlib.sha1(data).hexdigest() + "." + type_.split("/")[1] 17 + path = os.path.join(self._path, filename) 18 + 19 + if os.path.exists(path): 20 + self._logger.debug("Avatar for %s already exists, not saving it again", 21 + jid) 22 + else: 23 + with open(path, "wb") as f: 24 + f.write(data) 25 + 26 + self._avatars[jid] = filename 27 + 28 + async def try_xep_0153(self, jid): 29 + try: 30 + iq = await self._xmpp.plugin["xep_0054"].get_vcard(jid=jid, 31 + ifrom=self._xmpp._bot_jid_full) 32 + type_ = iq["vcard_temp"]["PHOTO"]["TYPE"] 33 + data = iq["vcard_temp"]["PHOTO"]["BINVAL"] 34 + self.save_avatar(jid, data, type_) 35 + return True 36 + except IqError: 37 + self._logger.debug("Avatar retrieval via XEP-0054/XEP-0153 failed. Probably no vCard for XEP-0054 published") 38 + return False 39 + 40 + async def try_xep_0084(self, jid): 41 + try: 42 + iq = await self._xmpp.plugin["xep_0060"].get_items(jid=jid, 43 + node="urn:xmpp:avatar:data", 44 + max_items=1, 45 + ifrom=self._xmpp._bot_jid_full) 46 + except IqError: 47 + self._logger.debug("Avatar retrieval via XEP-0084 failed. Probably no avatar published or subscription model not fulfilled.") 48 + return False 49 + 50 + async def aquire_avatar(self, jid): 51 + # First try vCard via 0054/0153 52 + for f in [self.try_xep_0153, self.try_xep_0084]: 53 + if await f(jid): 54 + self._logger.debug("Avatar retrieval successful for %s", 55 + jid) 56 + return 57 + 58 + self._logger.debug("Avatar retrieval failed for %s. Giving up.", 59 + jid) 60 + 61 + def get_avatar(self, jid): 62 + return self._avatars.get(jid, None)
+53
xmpp_discord_bridge/discord.py
··· 1 + import logging 2 + 3 + import discord 4 + 5 + class DiscordClient(discord.Client): 6 + def __init__(self, xmpp, config): 7 + intents = discord.Intents.default() 8 + intents.members = True 9 + intents.presences = True 10 + intents.messages = True 11 + intents.reactions = True 12 + 13 + discord.Client.__init__(self, intents=intents) 14 + 15 + self._xmpp = xmpp 16 + self._config = config 17 + self._logger = logging.getLogger("discord.client") 18 + 19 + async def on_ready(self): 20 + await self._xmpp.on_discord_ready() 21 + 22 + async def on_message(self, message): 23 + await self._xmpp.on_discord_message(message) 24 + 25 + async def on_member_update(self, before, after): 26 + await self._xmpp.on_discord_member_update(before, after) 27 + 28 + async def on_member_join(self, member): 29 + await self._xmpp.on_discord_member_join(member) 30 + 31 + async def on_member_leave(self, member): 32 + await self._xmpp.on_discord_member_leave(member) 33 + 34 + async def on_guild_channel_update(self, before, after): 35 + await self._xmpp.on_discord_channel_update(before, after) 36 + 37 + async def on_reaction(self, payload): 38 + message = await (self.get_guild(payload.guild_id) 39 + .get_channel(payload.channel_id) 40 + .fetch_message(payload.message_id)) 41 + 42 + await self._xmpp.on_discord_reaction(payload.guild_id, 43 + payload.channel_id, 44 + payload.emoji.name, 45 + message, 46 + payload.user_id, 47 + payload.event_type) 48 + 49 + async def on_raw_reaction_add(self, payload): 50 + await self.on_reaction(payload) 51 + 52 + async def on_raw_reaction_remove(self, payload): 53 + await self.on_reaction(payload)
+9
xmpp_discord_bridge/helpers.py
··· 1 + def discord_status_to_xmpp_show(status): 2 + return { 3 + discord.Status.online: "available", 4 + discord.Status.idle: "away", 5 + discord.Status.dnd: "dnd", 6 + discord.Status.do_not_disturb: "dnd", 7 + discord.Status.invisible: "xa", # TODO: Kinda 8 + discord.Status.offline: "unavailable" 9 + }.get(status, "available")
+21 -136
xmpp_discord_bridge/main.py
··· 14 14 from slixmpp import Message 15 15 from slixmpp.componentxmpp import ComponentXMPP 16 16 from slixmpp.exceptions import XMPPError, IqError 17 - from slixmpp.xmlstream import ElementBase, register_stanza_plugin 17 + from slixmpp.xmlstream import register_stanza_plugin 18 18 from slixmpp.jid import JID 19 - import discord 20 19 import requests 21 20 22 - class OOBData(ElementBase): 23 - name = "x" 24 - namespace = "jabber:x:oob" 25 - plugin_attrib = "oob" 26 - interfaces = {"url"} 27 - sub_interfaces = interfaces 28 - 29 - class AvatarManager: 30 - def __init__(self, xmpp, config): 31 - self._xmpp = xmpp 32 - self._path = config["avatars"]["path"] 33 - self._public = config["avatars"]["url"] 34 - 35 - self._avatars = {} 36 - 37 - self._logger = logging.getLogger("xmpp.avatar") 38 - 39 - def save_avatar(self, jid, data, type_): 40 - filename = hashlib.sha1(data).hexdigest() + "." + type_.split("/")[1] 41 - path = os.path.join(self._path, filename) 42 - 43 - if os.path.exists(path): 44 - self._logger.debug("Avatar for %s already exists, not saving it again", 45 - jid) 46 - else: 47 - with open(path, "wb") as f: 48 - f.write(data) 21 + from xmpp_discord_bridge.slixmpp.oob import OOBData 22 + from xmpp_discord_bridge.avatar import AvatarManager 23 + from xmpp_discord_bridge.discord import DiscordClient 24 + from xmpp_discord_bridge.helpers import discord_status_to_xmpp_show 49 25 50 - self._avatars[jid] = filename 51 - 52 - async def try_xep_0153(self, jid): 53 - try: 54 - iq = await self._xmpp.plugin["xep_0054"].get_vcard(jid=jid, 55 - ifrom=self._xmpp._bot_jid_full) 56 - type_ = iq["vcard_temp"]["PHOTO"]["TYPE"] 57 - data = iq["vcard_temp"]["PHOTO"]["BINVAL"] 58 - self.save_avatar(jid, data, type_) 59 - return True 60 - except IqError: 61 - self._logger.debug("Avatar retrieval via XEP-0054/XEP-0153 failed. Probably no vCard for XEP-0054 published") 62 - return False 63 - 64 - async def try_xep_0084(self, jid): 65 - try: 66 - iq = await self._xmpp.plugin["xep_0060"].get_items(jid=jid, 67 - node="urn:xmpp:avatar:data", 68 - max_items=1, 69 - ifrom=self._xmpp._bot_jid_full) 70 - except IqError: 71 - self._logger.debug("Avatar retrieval via XEP-0084 failed. Probably no avatar published or subscription model not fulfilled.") 72 - return False 73 - 74 - async def aquire_avatar(self, jid): 75 - # First try vCard via 0054/0153 76 - for f in [self.try_xep_0153, self.try_xep_0084]: 77 - if await f(jid): 78 - self._logger.debug("Avatar retrieval successful for %s", 79 - jid) 80 - return 81 - 82 - self._logger.debug("Avatar retrieval failed for %s. Giving up.", 83 - jid) 84 - 85 - def get_avatar(self, jid): 86 - return self._avatars.get(jid, None) 87 - 88 - class DiscordClient(discord.Client): 89 - def __init__(self, xmpp, config): 90 - intents = discord.Intents.default() 91 - intents.members = True 92 - intents.presences = True 93 - intents.messages = True 94 - intents.reactions = True 95 - 96 - discord.Client.__init__(self, intents=intents) 97 - 98 - self._xmpp = xmpp 99 - self._config = config 100 - self._logger = logging.getLogger("discord.client") 101 - 102 - async def on_ready(self): 103 - await self._xmpp.on_discord_ready() 104 - 105 - async def on_message(self, message): 106 - await self._xmpp.on_discord_message(message) 107 - 108 - async def on_member_update(self, before, after): 109 - await self._xmpp.on_discord_member_update(before, after) 110 - 111 - async def on_member_join(self, member): 112 - await self._xmpp.on_discord_member_join(member) 113 - 114 - async def on_member_leave(self, member): 115 - await self._xmpp.on_discord_member_leave(member) 116 - 117 - async def on_guild_channel_update(self, before, after): 118 - await self._xmpp.on_discord_channel_update(before, after) 119 - 120 - async def on_reaction(self, payload): 121 - message = await (self.get_guild(payload.guild_id) 122 - .get_channel(payload.channel_id) 123 - .fetch_message(payload.message_id)) 124 - 125 - await self._xmpp.on_discord_reaction(payload.guild_id, 126 - payload.channel_id, 127 - payload.emoji.name, 128 - message, 129 - payload.user_id, 130 - payload.event_type) 131 - 132 - async def on_raw_reaction_add(self, payload): 133 - await self.on_reaction(payload) 134 - 135 - async def on_raw_reaction_remove(self, payload): 136 - await self.on_reaction(payload) 137 - 138 - def discord_status_to_xmpp_show(status): 139 - return { 140 - discord.Status.online: "available", 141 - discord.Status.idle: "away", 142 - discord.Status.dnd: "dnd", 143 - discord.Status.do_not_disturb: "dnd", 144 - discord.Status.invisible: "xa", # TODO: Kinda 145 - discord.Status.offline: "unavailable" 146 - }.get(status, "available") 147 - 148 26 class BridgeComponent(ComponentXMPP): 149 27 def __init__(self, jid, secret, server, port, token, config): 150 28 ComponentXMPP.__init__(self, jid, secret, server, port) ··· 170 48 self._mucs = [] # List of known MUCs 171 49 self._webhooks = {} # MUC -> Webhook URL 172 50 51 + # Settings 52 + self._proxy_url_template = self._config["general"].get("proxy_discord_urls_to", "") 53 + self._proxy_hmac_secret = self._config["general"].get("hmac_secret", "") 54 + self._relay_xmpp_avatars = self._config["general"].get("relay_xmpp_avatars", False) 55 + self._dont_ignore_offline = self._config["general"].get("dont_ignore_offline", True) 56 + self._reactions_compat = self._config["general"].get("reactions_compat", True) 57 + self._muc_mention_compat = self._config["general"].get("muc_mention_compat", True) 58 + 173 59 register_stanza_plugin(Message, OOBData) 174 60 175 61 self.add_event_handler("session_start", self.on_session_start) ··· 180 66 """ 181 67 Send a message using XEP-0066 OOB data 182 68 """ 183 - proxy = self._config["general"].get("proxy_discord_urls_to", None) 184 - if proxy: 185 - hmac_str = urllib.parse.quote(base64.b64encode(hmac.digest(self._config["general"]["hmac_secret"].encode(), 69 + if self._proxy_url_template: 70 + hmac_str = urllib.parse.quote(base64.b64encode(hmac.digest(self._proxy_hmac_secret.encode(), 186 71 url.encode(), 187 72 "sha256")), safe="") 188 - proxy = proxy.replace("<hmac>", hmac_str).replace("<url>", urllib.parse.quote(url, safe="")) 73 + proxy = self._proxy_url_template.replace("<hmac>", hmac_str).replace("<url>", urllib.parse.quote(url, safe="")) 189 74 url = proxy 190 75 191 76 self._logger.debug("OOB URL: %s", url) ··· 323 208 "username": message["from"].resource 324 209 } 325 210 326 - if self._config["general"]["relay_xmpp_avatars"] and self._avatars.get_avatar(message["from"]): 211 + if self._relay_xmpp_avatars and self._avatars.get_avatar(message["from"]): 327 212 webhook["avatar_url"] = self._avatar.get_avatar(message["from"]) 328 213 329 214 # Look for mentions and replace them ··· 361 246 362 247 If update_state_tracking is True, then _virtual_muc_... gets updates. 363 248 """ 364 - if member.status == discord.Status.offline and not self._config["general"]["dont_ignore_offline"]: 249 + if member.status == discord.Status.offline and not self._dont_ignore_offline: 365 250 return 366 251 367 252 if update_state_tracking: ··· 416 301 for channel in self._guild_map[guild]: 417 302 muc = self._guild_map[guild][channel] 418 303 if after.status == discord.Status.offline: 419 - if self._config["general"]["dont_ignore_offline"]: 304 + if self._dont_ignore_offline: 420 305 self.virtual_user_update_presence(muc, 421 306 after.id, 422 307 "xa") ··· 466 351 user: discord.Member 467 352 kind: Either "add" or "remove" 468 353 """ 469 - if not self._config["general"]["reactions_compat"]: 354 + if not self._reactions_compat: 470 355 self._logger.debug("Got a reaction but reactions_compat is turned off. Ignoring.") 471 356 return 472 357 if not guild in self._guild_map: ··· 503 388 self._logger.debug("Message empty. Not relaying.") 504 389 return 505 390 506 - if self._config["general"]["muc_mention_compat"]: 391 + if self._muc_mention_compat: 507 392 mentions = [mention.display_name for mention in msg.mentions] 508 393 content = ", ".join(mentions) + ": " + msg.clean_content 509 394 else:
xmpp_discord_bridge/slixmpp/__init__.py

This is a binary file and will not be displayed.

+11
xmpp_discord_bridge/slixmpp/oob.py
··· 1 + from slixmpp.xmlstream import ElementBase 2 + 3 + class OOBData(ElementBase): 4 + """ 5 + XEP-0066 OOB data element for messages 6 + """ 7 + name = "x" 8 + namespace = "jabber:x:oob" 9 + plugin_attrib = "oob" 10 + interfaces = {"url"} 11 + sub_interfaces = interfaces