Pull apart the main file
This commit is contained in:
parent
fee845ef9c
commit
1fb4157794
62
xmpp_discord_bridge/avatar.py
Normal file
62
xmpp_discord_bridge/avatar.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from slixmpp.exceptions import IqError
|
||||||
|
|
||||||
|
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)
|
53
xmpp_discord_bridge/discord.py
Normal file
53
xmpp_discord_bridge/discord.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
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)
|
9
xmpp_discord_bridge/helpers.py
Normal file
9
xmpp_discord_bridge/helpers.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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")
|
@ -14,137 +14,15 @@ import slixmpp
|
|||||||
from slixmpp import Message
|
from slixmpp import Message
|
||||||
from slixmpp.componentxmpp import ComponentXMPP
|
from slixmpp.componentxmpp import ComponentXMPP
|
||||||
from slixmpp.exceptions import XMPPError, IqError
|
from slixmpp.exceptions import XMPPError, IqError
|
||||||
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
|
from slixmpp.xmlstream import register_stanza_plugin
|
||||||
from slixmpp.jid import JID
|
from slixmpp.jid import JID
|
||||||
import discord
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class OOBData(ElementBase):
|
from xmpp_discord_bridge.slixmpp.oob import OOBData
|
||||||
name = "x"
|
from xmpp_discord_bridge.avatar import AvatarManager
|
||||||
namespace = "jabber:x:oob"
|
from xmpp_discord_bridge.discord import DiscordClient
|
||||||
plugin_attrib = "oob"
|
from xmpp_discord_bridge.helpers import discord_status_to_xmpp_show
|
||||||
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):
|
class BridgeComponent(ComponentXMPP):
|
||||||
def __init__(self, jid, secret, server, port, token, config):
|
def __init__(self, jid, secret, server, port, token, config):
|
||||||
ComponentXMPP.__init__(self, jid, secret, server, port)
|
ComponentXMPP.__init__(self, jid, secret, server, port)
|
||||||
@ -170,6 +48,14 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
self._mucs = [] # List of known MUCs
|
self._mucs = [] # List of known MUCs
|
||||||
self._webhooks = {} # MUC -> Webhook URL
|
self._webhooks = {} # MUC -> Webhook URL
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
self._proxy_url_template = self._config["general"].get("proxy_discord_urls_to", "")
|
||||||
|
self._proxy_hmac_secret = self._config["general"].get("hmac_secret", "")
|
||||||
|
self._relay_xmpp_avatars = self._config["general"].get("relay_xmpp_avatars", False)
|
||||||
|
self._dont_ignore_offline = self._config["general"].get("dont_ignore_offline", True)
|
||||||
|
self._reactions_compat = self._config["general"].get("reactions_compat", True)
|
||||||
|
self._muc_mention_compat = self._config["general"].get("muc_mention_compat", True)
|
||||||
|
|
||||||
register_stanza_plugin(Message, OOBData)
|
register_stanza_plugin(Message, OOBData)
|
||||||
|
|
||||||
self.add_event_handler("session_start", self.on_session_start)
|
self.add_event_handler("session_start", self.on_session_start)
|
||||||
@ -180,12 +66,11 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
"""
|
"""
|
||||||
Send a message using XEP-0066 OOB data
|
Send a message using XEP-0066 OOB data
|
||||||
"""
|
"""
|
||||||
proxy = self._config["general"].get("proxy_discord_urls_to", None)
|
if self._proxy_url_template:
|
||||||
if proxy:
|
hmac_str = urllib.parse.quote(base64.b64encode(hmac.digest(self._proxy_hmac_secret.encode(),
|
||||||
hmac_str = urllib.parse.quote(base64.b64encode(hmac.digest(self._config["general"]["hmac_secret"].encode(),
|
|
||||||
url.encode(),
|
url.encode(),
|
||||||
"sha256")), safe="")
|
"sha256")), safe="")
|
||||||
proxy = proxy.replace("<hmac>", hmac_str).replace("<url>", urllib.parse.quote(url, safe=""))
|
proxy = self._proxy_url_template.replace("<hmac>", hmac_str).replace("<url>", urllib.parse.quote(url, safe=""))
|
||||||
url = proxy
|
url = proxy
|
||||||
|
|
||||||
self._logger.debug("OOB URL: %s", url)
|
self._logger.debug("OOB URL: %s", url)
|
||||||
@ -323,7 +208,7 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
"username": message["from"].resource
|
"username": message["from"].resource
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._config["general"]["relay_xmpp_avatars"] and self._avatars.get_avatar(message["from"]):
|
if self._relay_xmpp_avatars and self._avatars.get_avatar(message["from"]):
|
||||||
webhook["avatar_url"] = self._avatar.get_avatar(message["from"])
|
webhook["avatar_url"] = self._avatar.get_avatar(message["from"])
|
||||||
|
|
||||||
# Look for mentions and replace them
|
# Look for mentions and replace them
|
||||||
@ -361,7 +246,7 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
|
|
||||||
If update_state_tracking is True, then _virtual_muc_... gets updates.
|
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"]:
|
if member.status == discord.Status.offline and not self._dont_ignore_offline:
|
||||||
return
|
return
|
||||||
|
|
||||||
if update_state_tracking:
|
if update_state_tracking:
|
||||||
@ -416,7 +301,7 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
for channel in self._guild_map[guild]:
|
for channel in self._guild_map[guild]:
|
||||||
muc = self._guild_map[guild][channel]
|
muc = self._guild_map[guild][channel]
|
||||||
if after.status == discord.Status.offline:
|
if after.status == discord.Status.offline:
|
||||||
if self._config["general"]["dont_ignore_offline"]:
|
if self._dont_ignore_offline:
|
||||||
self.virtual_user_update_presence(muc,
|
self.virtual_user_update_presence(muc,
|
||||||
after.id,
|
after.id,
|
||||||
"xa")
|
"xa")
|
||||||
@ -466,7 +351,7 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
user: discord.Member
|
user: discord.Member
|
||||||
kind: Either "add" or "remove"
|
kind: Either "add" or "remove"
|
||||||
"""
|
"""
|
||||||
if not self._config["general"]["reactions_compat"]:
|
if not self._reactions_compat:
|
||||||
self._logger.debug("Got a reaction but reactions_compat is turned off. Ignoring.")
|
self._logger.debug("Got a reaction but reactions_compat is turned off. Ignoring.")
|
||||||
return
|
return
|
||||||
if not guild in self._guild_map:
|
if not guild in self._guild_map:
|
||||||
@ -503,7 +388,7 @@ class BridgeComponent(ComponentXMPP):
|
|||||||
self._logger.debug("Message empty. Not relaying.")
|
self._logger.debug("Message empty. Not relaying.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._config["general"]["muc_mention_compat"]:
|
if self._muc_mention_compat:
|
||||||
mentions = [mention.display_name for mention in msg.mentions]
|
mentions = [mention.display_name for mention in msg.mentions]
|
||||||
content = ", ".join(mentions) + ": " + msg.clean_content
|
content = ", ".join(mentions) + ": " + msg.clean_content
|
||||||
else:
|
else:
|
||||||
|
0
xmpp_discord_bridge/slixmpp/__init__.py
Normal file
0
xmpp_discord_bridge/slixmpp/__init__.py
Normal file
11
xmpp_discord_bridge/slixmpp/oob.py
Normal file
11
xmpp_discord_bridge/slixmpp/oob.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from slixmpp.xmlstream import ElementBase
|
||||||
|
|
||||||
|
class OOBData(ElementBase):
|
||||||
|
"""
|
||||||
|
XEP-0066 OOB data element for messages
|
||||||
|
"""
|
||||||
|
name = "x"
|
||||||
|
namespace = "jabber:x:oob"
|
||||||
|
plugin_attrib = "oob"
|
||||||
|
interfaces = {"url"}
|
||||||
|
sub_interfaces = interfaces
|
Loading…
Reference in New Issue
Block a user