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

Allow reading secrets from files

+76 -45
+12 -7
docs/example.toml
··· 1 1 [general] 2 2 jid = "discord.example.com" # JID of the server component 3 - jabber_nick = "Bot" # Nick of the bridge bot in the MUC 4 - secret = "" # Component secret 5 - server = "example.com" # Address of the actual XMPP server 6 - port = "5347" # Component port 7 - discord_token = "" # Token of your Discord bot 3 + jabber_nick = "Bot" # Nick of the bridge bot in the MUC 4 + secret = "" # Component secret 5 + secret_file = "" # Optional: Read the secret from a file 6 + server = "example.com" # Address of the actual XMPP server 7 + port = "5347" # Component port 8 + discord_token = "" # Token of your Discord bot 9 + discord_token_file = "" # Optional: Read the token from a file 8 10 9 11 # If a message is reacted to, send the following to the MUC: 10 12 # > [Message content] ··· 36 38 # Secret for the proxy. Only used when the proxy is used. 37 39 hmac_secret = "" 38 40 41 + # Optional: Read the hmac_secret from a file 42 + hmac_secret_file = "" 43 + 39 44 # Set to false to prevent the Discord channel description from being mirrored to the MUC ubject. 40 45 mirror_subject = true 41 46 ··· 53 58 [discord] 54 59 # There can be multiple [[discord.channels]] sections to bridge multiple MUCs and channels together. 55 60 [[discord.channels]] 56 - # The ID of the guild 57 61 guild = 000000000 58 62 59 - # The ID of the channel to mirror 60 63 channel = 000000000 61 64 65 + # The ID of the guild 66 + # The ID of the channel to mirror 62 67 # The MUC to mirror into 63 68 muc = "channel@muc.example.com"
+17
xmpp_discord_bridge/helpers.py
··· 1 + from pathlib import Path 2 + from typing import Dict, Optional 3 + 1 4 from nextcord import Status 2 5 3 6 def discord_status_to_xmpp_show(status): ··· 9 12 Status.invisible: "xa", # TODO: Kinda 10 13 Status.offline: "unavailable" 11 14 }.get(status, "available") 15 + 16 + def read_secret_file(path: Path) -> str: 17 + with path.open("r") as f: 18 + return f.read().strip("\n\t ") 19 + 20 + def get_secret_or_read_file(key_base: str, config: Dict[str, str]) -> Optional[str]: 21 + if key_base in config: 22 + return config[key_base] 23 + 24 + file_key = key_base + "_file" 25 + if file_key in config: 26 + return read_secret_file(Path(config[file_key])) 27 + 28 + return None
+47 -38
xmpp_discord_bridge/main.py
··· 10 10 from typing import Dict 11 11 import urllib.parse 12 12 from optparse import OptionParser 13 + from pathlib import Path 14 + from typing import Optional 13 15 14 16 import toml 15 17 import slixmpp ··· 27 29 from xmpp_discord_bridge.slixmpp_ext.oob import OOBData 28 30 from xmpp_discord_bridge.avatar import AvatarManager 29 31 from xmpp_discord_bridge.discord import DiscordClient 30 - from xmpp_discord_bridge.helpers import discord_status_to_xmpp_show 32 + from xmpp_discord_bridge.helpers import discord_status_to_xmpp_show, get_secret_or_read_file 31 33 32 34 # TODO: Move into separate file 33 35 # TODO: Add an option that allows a real user to join the MUC to replace a virtual one 34 36 class BridgeComponent(ComponentXMPP): 35 - def __init__(self, jid, secret, server, port, token, config): 37 + _proxy_hmac_secret: Optional[str] 38 + 39 + def __init__(self, jid, secret, server, port, token, hmac_token: Optional[str], config): 36 40 ComponentXMPP.__init__(self, jid, secret, server, port) 37 - 41 + 38 42 self._config = config 39 43 self._logger = logging.getLogger("xmpp.bridge") 40 44 self._domain = jid 41 45 self._bot_jid_bare = JID("bot@" + jid) 42 - 46 + 43 47 self._token = token 44 48 self._discord = None 45 - 49 + 46 50 self._avatars = AvatarManager(self, config) 47 - 51 + 48 52 # State tracking 49 53 self._virtual_muc_users = {} # MUC -> [Resources] 50 54 self._virtual_muc_nicks = {} # MUC -> User ID -> Nick ··· 56 60 self._discord_msg_cache = {} # Discord Message ID -> (Content, Author Mention) 57 61 self._mucs = [] # List of known MUCs 58 62 self._webhooks = {} # MUC -> Webhook URL 59 - 63 + 60 64 # Settings 61 65 self._bot_nick = self._config["general"].get("jabber_nick", "Bot") 62 66 self._bot_jid_full = JID("bot@" + jid + "/" + self._bot_nick) 63 67 self._proxy_url_template = self._config["general"].get("proxy_discord_urls_to", "") 64 - self._proxy_hmac_secret = self._config["general"].get("hmac_secret", "") 68 + self._proxy_hmac_secret = hmac_token 65 69 self._relay_xmpp_avatars = self._config["general"].get("relay_xmpp_avatars", False) 66 70 self._dont_ignore_offline = self._config["general"].get("dont_ignore_offline", True) 67 71 self._reactions_compat = self._config["general"].get("reactions_compat", True) 68 72 self._muc_mention_compat = self._config["general"].get("muc_mention_compat", True) 69 73 self._remove_url_on_embed = self._config["general"].get("remove_url_on_embed", False) 70 - 74 + 71 75 self._mirror_subject = self._config["general"].get("mirror_subject", True) 72 76 self._mirror_icon = self._config["general"].get("mirror_icon", True) 73 - 77 + 74 78 register_stanza_plugin(XMPPMessage, OOBData) 75 79 register_stanza_plugin(XMPPMessage, StanzaID) 76 80 register_stanza_plugin(XMPPMessage, Reply) 77 - 81 + 78 82 self.add_event_handler("session_start", self.on_session_start) 79 83 self.add_event_handler("groupchat_message", self.on_groupchat_message) 80 84 self.add_event_handler("groupchat_presence", self.on_groupchat_presence) ··· 97 101 mfrom=self.spoof_member_jid(member.id)) 98 102 msg["oob"]["url"] = url 99 103 msg.send() 100 - 104 + 101 105 def spoof_member_jid(self, id_): 102 106 """ 103 107 Return a full JID that we use for the puppets ··· 130 134 # Disconnect and close 131 135 await self.disconnect() 132 136 sys.exit() 133 - 137 + 134 138 async def on_discord_ready(self): 135 139 asyncio.get_event_loop().add_signal_handler(signal.SIGINT, 136 140 lambda: asyncio.create_task(self.on_shutdown_signal())) ··· 142 146 channel = ch["channel"] 143 147 guild = ch["guild"] 144 148 dchannel = self._discord.get_channel(channel) 145 - 149 + 146 150 # Initialise state tracking 147 151 self._muc_map[muc] = (guild, channel) 148 152 self._msg_map[muc] = bidict() ··· 151 155 for member in dchannel.members: 152 156 if member.status == Status.offline and not self._dont_ignore_offline: 153 157 continue 154 - 158 + 155 159 self._virtual_muc_users[muc].append(member.display_name) 156 160 self._virtual_muc_nicks[muc][member.id] = member.display_name 157 161 self._real_muc_users[muc] = [] ··· 215 219 vcard["PHOTO"]["TYPE"] = "image/png" 216 220 vcard["PHOTO"]["BINVAL"] = base64.b64encode(req.content) 217 221 await self.plugin["xep_0054"].publish_vcard(vcard) 218 - 222 + 219 223 # Acquire a webhook 220 224 wh = None 221 225 for webhook in await dchannel.webhooks(): ··· 242 246 "member", 243 247 jid=bare_member_jid, 244 248 ifrom=self._bot_jid_full) 245 - 249 + 246 250 await self.virtual_user_join_muc(muc, member, update_state_tracking=False) 247 251 248 252 self._logger.info("%s is ready", muc) ··· 285 289 member.display_name) 286 290 content = content.replace("@" + member.display_name, 287 291 member.mention) 288 - 292 + 289 293 if message["oob"]["url"] and message["body"] == message["oob"]["url"]: 290 294 # TODO: Quick and dirty hack. Discord requires a webhook embed to have 291 295 # a description ··· 334 338 pstatus=pstatus, 335 339 pto="%s/%s" % (muc, self._virtual_muc_nicks[muc][uid]), 336 340 pfrom=self.spoof_member_jid(uid)) 337 - 341 + 338 342 async def virtual_user_join_muc(self, muc, member, update_state_tracking=False): 339 343 """ 340 344 Makes the a puppet of member (@discord.Member) join the ··· 367 371 self.plugin["xep_0045"].make_join_stanza(muc, 368 372 nick=member.display_name, 369 373 presence_options=presence).send() 370 - 374 + 371 375 async def on_discord_member_join(self, member): 372 376 guild = member.guild.id 373 377 if not guild in self._guild_map: ··· 393 397 394 398 self.virtual_user_update_presence(muc, member.id, "unavailable") 395 399 396 - 400 + 397 401 async def on_discord_member_update(self, before, after): 398 402 guild = after.guild.id 399 403 if not guild in self._guild_map: ··· 495 499 496 500 for sticker in msg.stickers: 497 501 await self.send_oob_data(sticker.url, muc, msg.author) 498 - 502 + 499 503 if not msg.clean_content: 500 504 self._logger.debug("Message empty. Not relaying.") 501 505 return 502 506 503 507 mentions = [mention.display_name for mention in msg.mentions] 504 - if self._muc_mention_compat and mentions: 508 + if self._muc_mention_compat and mentions: 505 509 content = ", ".join(mentions) + ": " + msg.clean_content 506 510 else: 507 511 content = msg.clean_content ··· 544 548 return 545 549 if resource == self._bot_nick: 546 550 return 547 - 551 + 548 552 if presence["type"] == "unavailable": 549 553 try: 550 554 self._real_muc_users.remove(resource) ··· 556 560 if self._relay_xmpp_avatars: 557 561 await self._avatars.acquire_avatar(presence["from"]) 558 562 self._real_muc_users[muc].append(resource) 559 - 563 + 560 564 async def on_session_start(self, event): 561 565 self._discord = DiscordClient(self, self._config) 562 566 asyncio.ensure_future(self._discord.start(self._token)) ··· 572 576 573 577 (options, args) = parser.parse_args() 574 578 verbosity = logging.DEBUG if options.debug else logging.INFO 575 - 579 + 576 580 if options.config: 577 581 config = toml.load(options.config) 578 582 elif os.path.exists("./config.toml"): ··· 583 587 raise Exception("config.toml not found") 584 588 585 589 general = config["general"] 586 - secret = "" 587 - if not "secret" in general: 588 - if "secret_file" in general: 589 - with open(general["secret_file"], "r") as f: 590 - secret = f.read().replace("\n", "") 591 - else: 592 - raise Exception("No component secret specified") 593 - else: 594 - secret = general["secret"] 590 + 591 + # Get the component secret 592 + secret: Optional[str] = get_secret_or_read_file("secret", general) 593 + if secret is None: 594 + raise Exception("No component secret specified") 595 + 596 + # Get the Discord token 597 + discord_token: Optional[str] = get_secret_or_read_file("discord_token", general) 598 + if discord_token is None: 599 + raise Exception("No Discord token specified") 600 + 601 + # Get the HMAC token 602 + hmac_secret = get_secret_or_read_file("hmac_secret", general) 595 603 596 604 xmpp = BridgeComponent(general["jid"], 597 605 secret, 598 606 general["server"], 599 607 general["port"], 600 - general["discord_token"], 608 + discord_token, 609 + hmac_secret, 601 610 config) 602 611 for xep in [ 603 612 "0030", ··· 613 622 xmpp.register_plugin(f"xep_{xep}") 614 623 615 624 logging.basicConfig(stream=sys.stdout, level=verbosity) 616 - 625 + 617 626 xmpp.connect() 618 627 xmpp.loop.run_forever() 619 - 628 + 620 629 if __name__ == "__main__": 621 630 main()