Add existing code
This commit is contained in:
		
							parent
							
								
									3152aa732c
								
							
						
					
					
						commit
						fee845ef9c
					
				
							
								
								
									
										28
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | from setuptools import setup, find_packages | ||||||
|  | 
 | ||||||
|  | setup( | ||||||
|  |     name = "xmpp_discord_bridge", | ||||||
|  |     version = "0.1.0", | ||||||
|  |     url = "https://git.polynom.me/PapaTutuWawa/xmpp-discord-bridge", | ||||||
|  |     author = "Alexander \"PapaTutuWawa\"", | ||||||
|  |     author_email = "papatutuwawa <at> polynom.me", | ||||||
|  |     license = "GPLc3", | ||||||
|  |     packages = find_packages(), | ||||||
|  |     install_requires = [ | ||||||
|  |         "requests>=2.26.0", | ||||||
|  |         "slixmpp>=1.7.1", | ||||||
|  |         "discord.py>=1.7.3", | ||||||
|  |         "toml>=0.10.2" | ||||||
|  |     ], | ||||||
|  |     extra_require = { | ||||||
|  |         "dev": [ | ||||||
|  |             "black" | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
|  |     zip_safe = True, | ||||||
|  |     entry_points = { | ||||||
|  |         "console_scripts": [ | ||||||
|  |             "discord-xmpp-bridge = discord_xmpp_bridge.main:main" | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | ) | ||||||
							
								
								
									
										0
									
								
								xmpp_discord_bridge/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								xmpp_discord_bridge/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										578
									
								
								xmpp_discord_bridge/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										578
									
								
								xmpp_discord_bridge/main.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,578 @@ | |||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import asyncio | ||||||
|  | import signal | ||||||
|  | import base64 | ||||||
|  | import hashlib | ||||||
|  | import hmac | ||||||
|  | import urllib.parse | ||||||
|  | from optparse import OptionParser | ||||||
|  | 
 | ||||||
|  | import toml | ||||||
|  | import slixmpp | ||||||
|  | from slixmpp import Message | ||||||
|  | from slixmpp.componentxmpp import ComponentXMPP | ||||||
|  | from slixmpp.exceptions import XMPPError, IqError | ||||||
|  | from slixmpp.xmlstream import ElementBase, register_stanza_plugin | ||||||
|  | from slixmpp.jid import JID | ||||||
|  | import discord | ||||||
|  | import requests | ||||||
|  | 
 | ||||||
|  | class OOBData(ElementBase): | ||||||
|  |     name = "x" | ||||||
|  |     namespace = "jabber:x:oob" | ||||||
|  |     plugin_attrib = "oob" | ||||||
|  |     interfaces = {"url"} | ||||||
|  |     sub_interfaces = interfaces | ||||||
|  | 
 | ||||||
|  | class AvatarManager: | ||||||
|  |     def __init__(self, xmpp, config): | ||||||
|  |         self._xmpp = xmpp | ||||||
|  |         self._path = config["avatars"]["path"] | ||||||
|  |         self._public = config["avatars"]["url"] | ||||||
|  | 
 | ||||||
|  |         self._avatars = {} | ||||||
|  | 
 | ||||||
|  |         self._logger = logging.getLogger("xmpp.avatar") | ||||||
|  | 
 | ||||||
|  |     def save_avatar(self, jid, data, type_): | ||||||
|  |         filename = hashlib.sha1(data).hexdigest() + "." + type_.split("/")[1] | ||||||
|  |         path = os.path.join(self._path, filename) | ||||||
|  | 
 | ||||||
|  |         if os.path.exists(path): | ||||||
|  |             self._logger.debug("Avatar for %s already exists, not saving it again", | ||||||
|  |                                jid) | ||||||
|  |         else: | ||||||
|  |             with open(path, "wb") as f: | ||||||
|  |                 f.write(data) | ||||||
|  | 
 | ||||||
|  |         self._avatars[jid] = filename | ||||||
|  |          | ||||||
|  |     async def try_xep_0153(self, jid): | ||||||
|  |          try: | ||||||
|  |             iq = await self._xmpp.plugin["xep_0054"].get_vcard(jid=jid, | ||||||
|  |                                                                ifrom=self._xmpp._bot_jid_full) | ||||||
|  |             type_ = iq["vcard_temp"]["PHOTO"]["TYPE"] | ||||||
|  |             data = iq["vcard_temp"]["PHOTO"]["BINVAL"] | ||||||
|  |             self.save_avatar(jid, data, type_) | ||||||
|  |             return True | ||||||
|  |          except IqError: | ||||||
|  |             self._logger.debug("Avatar retrieval via XEP-0054/XEP-0153 failed. Probably no vCard for XEP-0054 published") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     async def try_xep_0084(self, jid): | ||||||
|  |         try: | ||||||
|  |             iq = await self._xmpp.plugin["xep_0060"].get_items(jid=jid, | ||||||
|  |                                                                node="urn:xmpp:avatar:data", | ||||||
|  |                                                                max_items=1, | ||||||
|  |                                                                ifrom=self._xmpp._bot_jid_full) | ||||||
|  |         except IqError: | ||||||
|  |             self._logger.debug("Avatar retrieval via XEP-0084 failed. Probably no avatar published or subscription model not fulfilled.") | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |     async def aquire_avatar(self, jid): | ||||||
|  |         # First try vCard via 0054/0153 | ||||||
|  |         for f in [self.try_xep_0153, self.try_xep_0084]: | ||||||
|  |             if await f(jid): | ||||||
|  |                 self._logger.debug("Avatar retrieval successful for %s", | ||||||
|  |                                    jid) | ||||||
|  |                 return | ||||||
|  |          | ||||||
|  |         self._logger.debug("Avatar retrieval failed for %s. Giving up.", | ||||||
|  |                            jid) | ||||||
|  | 
 | ||||||
|  |     def get_avatar(self, jid): | ||||||
|  |         return self._avatars.get(jid, None) | ||||||
|  | 
 | ||||||
|  | class DiscordClient(discord.Client): | ||||||
|  |     def __init__(self, xmpp, config): | ||||||
|  |         intents = discord.Intents.default() | ||||||
|  |         intents.members = True | ||||||
|  |         intents.presences = True | ||||||
|  |         intents.messages = True | ||||||
|  |         intents.reactions = True | ||||||
|  | 
 | ||||||
|  |         discord.Client.__init__(self, intents=intents) | ||||||
|  | 
 | ||||||
|  |         self._xmpp = xmpp | ||||||
|  |         self._config = config | ||||||
|  |         self._logger = logging.getLogger("discord.client") | ||||||
|  | 
 | ||||||
|  |     async def on_ready(self): | ||||||
|  |         await self._xmpp.on_discord_ready() | ||||||
|  | 
 | ||||||
|  |     async def on_message(self, message): | ||||||
|  |         await self._xmpp.on_discord_message(message) | ||||||
|  | 
 | ||||||
|  |     async def on_member_update(self, before, after): | ||||||
|  |         await self._xmpp.on_discord_member_update(before, after) | ||||||
|  | 
 | ||||||
|  |     async def on_member_join(self, member): | ||||||
|  |         await self._xmpp.on_discord_member_join(member) | ||||||
|  | 
 | ||||||
|  |     async def on_member_leave(self, member): | ||||||
|  |         await self._xmpp.on_discord_member_leave(member) | ||||||
|  | 
 | ||||||
|  |     async def on_guild_channel_update(self, before, after): | ||||||
|  |         await self._xmpp.on_discord_channel_update(before, after) | ||||||
|  | 
 | ||||||
|  |     async def on_reaction(self, payload): | ||||||
|  |         message = await (self.get_guild(payload.guild_id) | ||||||
|  |                          .get_channel(payload.channel_id) | ||||||
|  |                          .fetch_message(payload.message_id)) | ||||||
|  |          | ||||||
|  |         await self._xmpp.on_discord_reaction(payload.guild_id, | ||||||
|  |                                              payload.channel_id, | ||||||
|  |                                              payload.emoji.name, | ||||||
|  |                                              message, | ||||||
|  |                                              payload.user_id, | ||||||
|  |                                              payload.event_type) | ||||||
|  |         | ||||||
|  |     async def on_raw_reaction_add(self, payload): | ||||||
|  |         await self.on_reaction(payload) | ||||||
|  | 
 | ||||||
|  |     async def on_raw_reaction_remove(self, payload): | ||||||
|  |         await self.on_reaction(payload) | ||||||
|  | 
 | ||||||
|  | def discord_status_to_xmpp_show(status): | ||||||
|  |     return { | ||||||
|  |         discord.Status.online: "available", | ||||||
|  |         discord.Status.idle: "away", | ||||||
|  |         discord.Status.dnd: "dnd", | ||||||
|  |         discord.Status.do_not_disturb: "dnd", | ||||||
|  |         discord.Status.invisible: "xa", # TODO: Kinda | ||||||
|  |         discord.Status.offline: "unavailable" | ||||||
|  |     }.get(status, "available") | ||||||
|  |          | ||||||
|  | class BridgeComponent(ComponentXMPP): | ||||||
|  |     def __init__(self, jid, secret, server, port, token, config): | ||||||
|  |         ComponentXMPP.__init__(self, jid, secret, server, port) | ||||||
|  | 
 | ||||||
|  |         self._config = config | ||||||
|  |         self._logger = logging.getLogger("xmpp.bridge") | ||||||
|  |         self._domain = jid | ||||||
|  |         self._bot_nick = "Bot" | ||||||
|  |         self._bot_jid_bare = JID("bot@" + jid) | ||||||
|  |         self._bot_jid_full = JID("bot@" + jid + "/" + self._bot_nick) | ||||||
|  | 
 | ||||||
|  |         self._token = token | ||||||
|  |         self._discord = None | ||||||
|  | 
 | ||||||
|  |         self._avatars = AvatarManager(self, config) | ||||||
|  |          | ||||||
|  |         # State tracking | ||||||
|  |         self._virtual_muc_users = {} # MUC -> [Resources] | ||||||
|  |         self._virtual_muc_nicks = {} # MUC -> User ID -> Nick | ||||||
|  |         self._real_muc_users = {} # MUC -> [Resources] | ||||||
|  |         self._guild_map = {} # Guild ID -> Channel ID -> MUC | ||||||
|  |         self._muc_map = {} # MUC -> (Guild ID, Channel ID) | ||||||
|  |         self._mucs = [] # List of known MUCs | ||||||
|  |         self._webhooks = {} # MUC -> Webhook URL | ||||||
|  | 
 | ||||||
|  |         register_stanza_plugin(Message, OOBData) | ||||||
|  |          | ||||||
|  |         self.add_event_handler("session_start", self.on_session_start) | ||||||
|  |         self.add_event_handler("groupchat_message", self.on_groupchat_message) | ||||||
|  |         self.add_event_handler("groupchat_presence", self.on_groupchat_presence) | ||||||
|  | 
 | ||||||
|  |     async def send_oob_data(self, url, muc, member): | ||||||
|  |         """ | ||||||
|  |         Send a message using XEP-0066 OOB data | ||||||
|  |         """ | ||||||
|  |         proxy = self._config["general"].get("proxy_discord_urls_to", None) | ||||||
|  |         if proxy: | ||||||
|  |             hmac_str = urllib.parse.quote(base64.b64encode(hmac.digest(self._config["general"]["hmac_secret"].encode(), | ||||||
|  |                                                                    url.encode(), | ||||||
|  |                                                                    "sha256")), safe="") | ||||||
|  |             proxy = proxy.replace("<hmac>", hmac_str).replace("<url>", urllib.parse.quote(url, safe="")) | ||||||
|  |             url = proxy | ||||||
|  | 
 | ||||||
|  |         self._logger.debug("OOB URL: %s", url) | ||||||
|  |         msg = self.make_message(muc, | ||||||
|  |                                 mbody=url, | ||||||
|  |                                 mtype="groupchat", | ||||||
|  |                                 mfrom=self.spoof_member_jid(member.id)) | ||||||
|  |         msg["oob"]["url"] = url | ||||||
|  |         msg.send() | ||||||
|  |          | ||||||
|  |     def spoof_member_jid(self, id_): | ||||||
|  |         """ | ||||||
|  |         Return a full JID that we use for the puppets | ||||||
|  |         """ | ||||||
|  |         return JID(str(id_) + "@" + self._domain + "/discord") | ||||||
|  | 
 | ||||||
|  |     async def on_sigint(self): | ||||||
|  |         await self._discord.close() | ||||||
|  | 
 | ||||||
|  |         # Remove all virtual users | ||||||
|  |         # NOTE: We cannot use leave_muc as this would also remove the MUC | ||||||
|  |         #       from slixmpp's internal tracking, which would probably break | ||||||
|  |         #       later leaves | ||||||
|  |         for muc in self._mucs: | ||||||
|  |             for uid in self._virtual_muc_nicks[muc]: | ||||||
|  |                 nick = self._virtual_muc_nicks[muc][uid] | ||||||
|  |                 self.send_presence(pshow='unavailable', | ||||||
|  |                                    pto="%s/%s" % (muc, nick), | ||||||
|  |                                    pfrom=self.spoof_member_jid(uid)) | ||||||
|  | 
 | ||||||
|  |             # Remove the Bot user | ||||||
|  |             self.send_presence(pshow='unavailable', | ||||||
|  |                                pto="%s/%s" % (muc, "Bot"), | ||||||
|  |                                pfrom=self._bot_jid_full) | ||||||
|  | 
 | ||||||
|  |         # Disconnect and close | ||||||
|  |         await self.disconnect() | ||||||
|  |      | ||||||
|  |     async def on_discord_ready(self): | ||||||
|  |         asyncio.get_event_loop().add_signal_handler(signal.SIGINT, | ||||||
|  |                                                       lambda: asyncio.create_task(self.on_sigint())) | ||||||
|  | 
 | ||||||
|  |         for ch in self._config["discord"]["channels"]: | ||||||
|  |             muc = ch["muc"] | ||||||
|  |             channel = ch["channel"] | ||||||
|  |             guild = ch["guild"] | ||||||
|  |             dchannel = self._discord.get_channel(channel) | ||||||
|  | 
 | ||||||
|  |             # Initialise state tracking | ||||||
|  |             self._muc_map[muc] = (guild, channel) | ||||||
|  |             self._virtual_muc_users[muc] = [] | ||||||
|  |             self._virtual_muc_nicks[muc] = {} | ||||||
|  |             for member in dchannel.members: | ||||||
|  |                 if member.status == discord.Status.offline: | ||||||
|  |                     continue | ||||||
|  |                  | ||||||
|  |                 self._virtual_muc_users[muc].append(member.display_name) | ||||||
|  |                 self._virtual_muc_nicks[muc][member.id] = member.display_name | ||||||
|  |             self._real_muc_users[muc] = [] | ||||||
|  |             self._mucs.append(muc) | ||||||
|  |             if not guild in self._guild_map: | ||||||
|  |                 self._guild_map[guild] = { | ||||||
|  |                     channel: muc | ||||||
|  |                 } | ||||||
|  |             else: | ||||||
|  |                 self._guild_map[guild][channel] = muc | ||||||
|  | 
 | ||||||
|  |             self._logger.debug("Joining %s", muc) | ||||||
|  |             self.plugin["xep_0045"].join_muc(muc, | ||||||
|  |                                              nick=self._bot_nick, | ||||||
|  |                                              pfrom=self._bot_jid_full) | ||||||
|  | 
 | ||||||
|  |             # Set the subject | ||||||
|  |             subject = dchannel.topic or "" | ||||||
|  |             self.plugin["xep_0045"].set_subject(muc, | ||||||
|  |                                                 subject, | ||||||
|  |                                                 mfrom=self._bot_jid_full) | ||||||
|  | 
 | ||||||
|  |             # TODO: Is this working? | ||||||
|  |             # Mirror the guild's icon | ||||||
|  |             icon = await dchannel.guild.icon_url_as(static_format="png", | ||||||
|  |                                                     format="png", | ||||||
|  |                                                     size=128).read() | ||||||
|  |             vcard = self.plugin["xep_0054"].make_vcard() | ||||||
|  |             vcard["PHOTO"]["TYPE"] = "image/png" | ||||||
|  |             vcard["PHOTO"]["BINVAL"] = base64.b64encode(icon) | ||||||
|  |             self.send_raw(""" | ||||||
|  | <iq type="set" from="{}" to="{}"> | ||||||
|  |     <vCard xmlns="vcard-temp"> | ||||||
|  | {} | ||||||
|  |     </vCard> | ||||||
|  | </iq> | ||||||
|  |             """.format(self._bot_jid_full, | ||||||
|  |                        muc, | ||||||
|  |                        str(vcard))) | ||||||
|  |              | ||||||
|  |             # Aquire a webhook | ||||||
|  |             webhook_url = "" | ||||||
|  |             for webhook in await dchannel.webhooks(): | ||||||
|  |                 if webhook.name == "discord-xmpp-bridge": | ||||||
|  |                     webhook_url = webhook.url | ||||||
|  |                     break | ||||||
|  |             if not webhook_url: | ||||||
|  |                 webhook = dchannel.create_webhook(name="discord-xmpp-bridge", | ||||||
|  |                                                   reason="Bridging Discord and XMPP") | ||||||
|  |                 webhook_url = webhook.url | ||||||
|  |             self._webhooks[muc] = webhook_url | ||||||
|  | 
 | ||||||
|  |             # Make sure our virtual users can join | ||||||
|  |             affiliation_delta = [ | ||||||
|  |                 (self.spoof_member_jid(member.id).bare, "member") for member in dchannel.members | ||||||
|  |             ] | ||||||
|  |             await self.plugin["xep_0045"].send_affiliation_list(muc, | ||||||
|  |                                                                 affiliation_delta, | ||||||
|  |                                                                 ifrom=self._bot_jid_full) | ||||||
|  |              | ||||||
|  |             for member in dchannel.members: | ||||||
|  |                 self.virtual_user_join_muc(muc, member, update_state_tracking=False) | ||||||
|  | 
 | ||||||
|  |             self._logger.info("%s is ready", muc) | ||||||
|  |         self._logger.info("Bridge is ready") | ||||||
|  | 
 | ||||||
|  |     async def on_groupchat_message(self, message): | ||||||
|  |         muc = message["from"].bare | ||||||
|  |         if not message["body"]: | ||||||
|  |             return | ||||||
|  |         if not message["from"].resource in self._real_muc_users[muc]: | ||||||
|  |             return | ||||||
|  |         # Prevent the message being reflected back into Discord | ||||||
|  |         if not message["to"] == self._bot_jid_full: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         webhook = { | ||||||
|  |             "content": message["body"], | ||||||
|  |             "username": message["from"].resource | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if self._config["general"]["relay_xmpp_avatars"] and self._avatars.get_avatar(message["from"]): | ||||||
|  |             webhook["avatar_url"] = self._avatar.get_avatar(message["from"]) | ||||||
|  | 
 | ||||||
|  |         # Look for mentions and replace them | ||||||
|  |         guild, channel = self._muc_map[muc] | ||||||
|  |         for member in self._discord.get_guild(guild).get_channel(channel).members: | ||||||
|  |             self._logger.debug("Checking %s", member.display_name) | ||||||
|  |             if "@" + member.display_name in webhook["content"]: | ||||||
|  |                 self._logger.debug("Found mention for %s. Replaceing.", | ||||||
|  |                                    member.display_name) | ||||||
|  |                 webhook["content"] = webhook["content"].replace("@" + member.display_name, | ||||||
|  |                                                                 member.mention) | ||||||
|  |              | ||||||
|  |         if message["oob"]["url"] and message["body"] == message["oob"]["url"]: | ||||||
|  |             webhook["embed"] = [{ | ||||||
|  |                 "type": "rich", | ||||||
|  |                 "url": message["oob"]["url"] | ||||||
|  |             }] | ||||||
|  |              | ||||||
|  |         requests.post(self._webhooks[muc], | ||||||
|  |                       data=webhook) | ||||||
|  | 
 | ||||||
|  |     def virtual_user_update_presence(self, muc, uid, pshow): | ||||||
|  |         """ | ||||||
|  |         Change the status of a virtual MUC member. | ||||||
|  |         NOTE: This assumes that the user is in the MUC | ||||||
|  |         """ | ||||||
|  |         self.send_presence(pshow=pshow, | ||||||
|  |                            pto="%s/%s" % (muc, self._virtual_muc_nicks[muc][uid]), | ||||||
|  |                            pfrom=self.spoof_member_jid(uid)) | ||||||
|  |          | ||||||
|  |     def virtual_user_join_muc(self, muc, member, update_state_tracking=False): | ||||||
|  |         """ | ||||||
|  |         Makes the a puppet of member (@discord.Member) join the | ||||||
|  |         MUC. Does nothing if the member is offline. | ||||||
|  | 
 | ||||||
|  |         If update_state_tracking is True, then _virtual_muc_... gets updates. | ||||||
|  |         """ | ||||||
|  |         if member.status == discord.Status.offline and not self._config["general"]["dont_ignore_offline"]: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         if update_state_tracking: | ||||||
|  |             self._virtual_muc_users[muc].append(member.display_name) | ||||||
|  |             self._virtual_muc_nicks[muc][member.id] = member.display_name | ||||||
|  | 
 | ||||||
|  |         # Prevent offline users from getting an unavailable presence by | ||||||
|  |         # accident | ||||||
|  |         pshow = discord_status_to_xmpp_show(member.status) | ||||||
|  |         if member.status == discord.Status.offline: | ||||||
|  |             pshow = "xa" | ||||||
|  | 
 | ||||||
|  |         self.plugin["xep_0045"].join_muc(muc, | ||||||
|  |                                          nick=member.display_name, | ||||||
|  |                                          pfrom=self.spoof_member_jid(member.id), | ||||||
|  |                                          pshow=pshow) | ||||||
|  |          | ||||||
|  |     async def on_discord_member_join(self, member): | ||||||
|  |         guild = member.guild.id | ||||||
|  |         if not guild in self._guild_map: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         self._logger.debug("%s joined a known guild. Updating channels.", | ||||||
|  |                            member.display_name) | ||||||
|  |         for channel in self._guild_map[guild]: | ||||||
|  |             muc = self._guild_map[guild][channel] | ||||||
|  |             self.virtual_user_join_muc(muc, member, update_state_tracking=True) | ||||||
|  | 
 | ||||||
|  |     async def on_discord_member_leave(self, member): | ||||||
|  |         guild = member.guild.id | ||||||
|  |         if not guild in self._guild_map: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         self._logger.debug("%s left a known guild. Updating channels.", | ||||||
|  |                            member.display_name) | ||||||
|  |         for channel in self._guild_map[guild]: | ||||||
|  |             muc = self._guild_map[member.guild.id][channel] | ||||||
|  |             self._virtual_muc_users[muc].remove(member.display_name) | ||||||
|  |             del self._virtual_muc_nicks[muc][member.id] | ||||||
|  | 
 | ||||||
|  |             self.virtual_user_update_presence(muc, member.id, "unavailable") | ||||||
|  | 
 | ||||||
|  |              | ||||||
|  |     async def on_discord_member_update(self, before, after): | ||||||
|  |         guild = after.guild.id | ||||||
|  |         if not guild in self._guild_map: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # TODO: Handle nick changes | ||||||
|  |         if before.status != after.status: | ||||||
|  |             # Handle a status change | ||||||
|  |             for channel in self._guild_map[guild]: | ||||||
|  |                 muc = self._guild_map[guild][channel] | ||||||
|  |                 if after.status == discord.Status.offline: | ||||||
|  |                     if self._config["general"]["dont_ignore_offline"]: | ||||||
|  |                         self.virtual_user_update_presence(muc, | ||||||
|  |                                                           after.id, | ||||||
|  |                                                           "xa") | ||||||
|  |                     else: | ||||||
|  |                         self._logger.debug("%s went offline. Removing from state tracking.", | ||||||
|  |                                            after.display_name) | ||||||
|  |                         self.virtual_user_update_presence(muc, | ||||||
|  |                                                           after.id, | ||||||
|  |                                                           discord_status_to_xmpp_show(after.status)) | ||||||
|  | 
 | ||||||
|  |                         # Remove from all state tracking | ||||||
|  |                         self._virtual_muc_users[muc].remove(after.display_name) | ||||||
|  |                         del self._virtual_muc_nicks[muc][after.id] | ||||||
|  |                 elif before.status == discord.Status.offline and after.status != discord.Status.offline: | ||||||
|  |                     self.virtual_user_join_muc(muc, after, update_state_tracking=True) | ||||||
|  |                 else: | ||||||
|  |                     self.virtual_user_update_presence(muc, | ||||||
|  |                                                       after.id, | ||||||
|  |                                                       discord_status_to_xmpp_show(after.status)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     async def on_discord_channel_update(self, before, after): | ||||||
|  |         if after.type != discord.ChannelType.text: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         guild = after.guild.id | ||||||
|  |         channel = after.id | ||||||
|  |         if not guild in self._guild_map: | ||||||
|  |             return | ||||||
|  |         if not channel in self._guild_map[guild]: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # NOTE: We can only really handle description changes | ||||||
|  |         muc = self._guild_map[guild][channel] | ||||||
|  |         if before.topic != after.topic: | ||||||
|  |             self._logger.debug("Channel %s changed the topic. Relaying.", | ||||||
|  |                                after.name) | ||||||
|  |             self.plugin["xep_0045"].set_subject(muc, | ||||||
|  |                                                 after.topic or "", | ||||||
|  |                                                 mfrom=self._bot_jid_full) | ||||||
|  | 
 | ||||||
|  |     async def on_discord_reaction(self, guild, channel, emoji_str, msg, uid, kind): | ||||||
|  |         """ | ||||||
|  |         Handle a Discord reaction. | ||||||
|  | 
 | ||||||
|  |         reaction: discord.Reaction | ||||||
|  |         user: discord.Member | ||||||
|  |         kind: Either "add" or "remove" | ||||||
|  |         """ | ||||||
|  |         if not self._config["general"]["reactions_compat"]: | ||||||
|  |             self._logger.debug("Got a reaction but reactions_compat is turned off. Ignoring.") | ||||||
|  |             return | ||||||
|  |         if not guild in self._guild_map: | ||||||
|  |             return | ||||||
|  |         if not channel in self._guild_map[guild]: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         muc = self._guild_map[guild][channel] | ||||||
|  | 
 | ||||||
|  |         # TODO: Handle attachments | ||||||
|  |         content = "> " + msg.clean_content.replace("\n", "\n> ") + "\n" | ||||||
|  |         content += "+" if kind == "REACTION_ADD" else "-" | ||||||
|  |         content += " " + emoji_str | ||||||
|  | 
 | ||||||
|  |         self.send_message(mto=muc, | ||||||
|  |                           mbody=content, | ||||||
|  |                           mtype="groupchat", | ||||||
|  |                           mfrom=self.spoof_member_jid(uid)) | ||||||
|  | 
 | ||||||
|  |     async def on_discord_message(self, msg): | ||||||
|  |         guild, channel = msg.guild.id, msg.channel.id | ||||||
|  |         if not (guild in self._guild_map and channel in self._guild_map[guild]): | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         muc = self._guild_map[guild][channel] | ||||||
|  |         if msg.author.bot and msg.author.display_name in self._real_muc_users[muc]: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # TODO: Handle embeds | ||||||
|  |         for attachment in msg.attachments: | ||||||
|  |             await self.send_oob_data(attachment.url, muc, msg.author) | ||||||
|  | 
 | ||||||
|  |         if not msg.clean_content: | ||||||
|  |             self._logger.debug("Message empty. Not relaying.") | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         if self._config["general"]["muc_mention_compat"]: | ||||||
|  |             mentions = [mention.display_name for mention in msg.mentions] | ||||||
|  |             content = ", ".join(mentions) + ": " + msg.clean_content | ||||||
|  |         else: | ||||||
|  |             content = msg.clean_content | ||||||
|  |          | ||||||
|  |         self.send_message(mto=muc, | ||||||
|  |                           mbody=content, | ||||||
|  |                           mtype="groupchat", | ||||||
|  |                           mfrom=self.spoof_member_jid(msg.author.id)) | ||||||
|  | 
 | ||||||
|  |     async def on_groupchat_presence(self, presence): | ||||||
|  |         muc = presence["from"].bare | ||||||
|  |         resource = presence["from"].resource | ||||||
|  | 
 | ||||||
|  |         if not muc in self._mucs: | ||||||
|  |             self._logger.warn("Received presence in unknown MUC %s", muc) | ||||||
|  |             return | ||||||
|  |         if resource in self._virtual_muc_users[muc]: | ||||||
|  |             return | ||||||
|  |         if not presence["to"] == self._bot_jid_full: | ||||||
|  |             return | ||||||
|  |         if resource == self._bot_nick: | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         if presence["type"] == "unavailable": | ||||||
|  |             try: | ||||||
|  |                 self._real_muc_users.remove(resource) | ||||||
|  |             except: | ||||||
|  |                 self._logger.debug("Trying to remove %s from %s, but user is not in list. Skipping...", | ||||||
|  |                                    muc, | ||||||
|  |                                    resource) | ||||||
|  |         else: | ||||||
|  |             await self._avatars.aquire_avatar(presence["from"]) | ||||||
|  |             self._real_muc_users[muc].append(resource) | ||||||
|  |          | ||||||
|  |     async def on_session_start(self, event): | ||||||
|  |         self._discord = DiscordClient(self, config) | ||||||
|  |         asyncio.ensure_future(self._discord.start(self._token)) | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     if os.path.exists("./config.toml"): | ||||||
|  |         config = toml.load("./config.toml") | ||||||
|  |     elif os.path.exists("/etc/discord-xmpp-bridge/config.toml"): | ||||||
|  |         config = toml.load("/etc/discord-xmpp-bridge/config.toml") | ||||||
|  |     else: | ||||||
|  |         raise Exception("config.toml not found") | ||||||
|  | 
 | ||||||
|  |     parser = OptionParser() | ||||||
|  |     parser.add_option( | ||||||
|  |         "-d", "--debug", dest="debug", help="Enable debug logging", action="store_true" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     (option, args) = parser.parse_args() | ||||||
|  |     verbosity = logging.DEBUG if options.debug else logging.INFO | ||||||
|  |      | ||||||
|  |     general = config["general"] | ||||||
|  |     xmpp = BridgeComponent(general["jid"], | ||||||
|  |                            general["secret"], | ||||||
|  |                            general["server"], | ||||||
|  |                            general["port"], | ||||||
|  |                            general["discord_token"], | ||||||
|  |                            config) | ||||||
|  |     for xep in [ "0030", "0199", "0045", "0084", "0153", "0054", "0060" ]: | ||||||
|  |         xmpp.register_plugin(f"xep_{xep}") | ||||||
|  | 
 | ||||||
|  |     logging.basicConfig(stream=sys.stdout, level=verbosity) | ||||||
|  |      | ||||||
|  |     xmpp.connect() | ||||||
|  |     xmpp.process(forever=False) | ||||||
|  |          | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user