From eef02fbb76d9b270c03f8fbc3f48c6b1a9a8cbb5 Mon Sep 17 00:00:00 2001 From: rht Date: Sun, 28 Nov 2021 00:56:56 -0500 Subject: [PATCH] Slack bridge: Implement multiple channels bridges. --- .../bridge_with_slack_config.py | 14 ++++- .../bridge_with_slack/run-slack-bridge | 63 +++++++++++++------ 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py b/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py index 403e7cee..9dd73331 100644 --- a/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py +++ b/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py @@ -3,12 +3,20 @@ config = { "email": "zulip-bot@email.com", "api_key": "put api key here", "site": "https://chat.zulip.org", - "stream": "test here", - "topic": "<- slack-bridge", }, "slack": { "username": "slack_username", "token": "xoxb-your-slack-token", - "channel": "C5Z5N7R8A -- must be channel id", + }, + # Mapping between Slack channels and Zulip stream-topic's. + # You can specify multiple pairs. + "channel_mapping": { + # Slack channel; must be channel ID + "C5Z5N7R8A": { + # Zulip stream + "stream": "test here", + # Zulip topic + "topic": "<- slack-bridge", + }, }, } diff --git a/zulip/integrations/bridge_with_slack/run-slack-bridge b/zulip/integrations/bridge_with_slack/run-slack-bridge index d627cad2..b2ee92c6 100755 --- a/zulip/integrations/bridge_with_slack/run-slack-bridge +++ b/zulip/integrations/bridge_with_slack/run-slack-bridge @@ -5,7 +5,7 @@ import os import sys import threading import traceback -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional, Tuple import bridge_with_slack_config import slack_sdk @@ -17,18 +17,28 @@ import zulip ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}" SLACK_MESSAGE_TEMPLATE = "<{username}> {message}" +StreamTopicT = Tuple[str, str] -def check_zulip_message_validity(msg: Dict[str, Any], config: Dict[str, Any]) -> bool: + +def get_slack_channel_for_zulip_message( + msg: Dict[str, Any], zulip_to_slack_map: Dict[StreamTopicT, Any], bot_email: str +) -> Optional[str]: is_a_stream = msg["type"] == "stream" - in_the_specified_stream = msg["display_recipient"] == config["stream"] - at_the_specified_subject = msg["subject"] == config["topic"] + if not is_a_stream: + return None - # We do this to identify the messages generated from Matrix -> Zulip - # and we make sure we don't forward it again to the Matrix. - not_from_zulip_bot = msg["sender_email"] != config["email"] - if is_a_stream and not_from_zulip_bot and in_the_specified_stream and at_the_specified_subject: - return True - return False + stream_name = msg["display_recipient"] + topic_name = msg["subject"] + stream_topic: StreamTopicT = (stream_name, topic_name) + if stream_topic not in zulip_to_slack_map: + return None + + # We do this to identify the messages generated from Slack -> Zulip + # and we make sure we don't forward it again to the Slack. + from_zulip_bot = msg["sender_email"] == bot_email + if from_zulip_bot: + return None + return zulip_to_slack_map[stream_topic] class SlackBridge: @@ -37,14 +47,17 @@ class SlackBridge: self.zulip_config = config["zulip"] self.slack_config = config["slack"] + self.slack_to_zulip_map: Dict[str, Dict[str, str]] = config["channel_mapping"] + self.zulip_to_slack_map: Dict[StreamTopicT, str] = { + (z["stream"], z["topic"]): s for s, z in config["channel_mapping"].items() + } + # zulip-specific self.zulip_client = zulip.Client( email=self.zulip_config["email"], api_key=self.zulip_config["api_key"], site=self.zulip_config["site"], ) - self.zulip_stream = self.zulip_config["stream"] - self.zulip_subject = self.zulip_config["topic"] # slack-specific self.channel = self.slack_config["channel"] @@ -68,14 +81,16 @@ class SlackBridge: def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]: def _zulip_to_slack(msg: Dict[str, Any]) -> None: - message_valid = check_zulip_message_validity(msg, self.zulip_config) - if message_valid: + slack_channel = get_slack_channel_for_zulip_message( + msg, self.zulip_to_slack_map, self.zulip_config["email"] + ) + if slack_channel is not None: self.wrap_slack_mention_with_bracket(msg) slack_text = SLACK_MESSAGE_TEMPLATE.format( username=msg["sender_full_name"], message=msg["content"] ) self.slack_webclient.chat_postMessage( - channel=self.channel, + channel=slack_channel, text=slack_text, ) @@ -91,7 +106,7 @@ class SlackBridge: @rtm.on("message") def slack_to_zulip(client: RTMClient, event: Dict[str, Any]) -> None: - if event["channel"] != self.channel: + if event["channel"] not in self.slack_to_zulip_map: return user_id = event["user"] user = self.slack_id_to_name[user_id] @@ -100,8 +115,12 @@ class SlackBridge: return self.replace_slack_id_with_name(event) content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=event["text"]) + zulip_endpoint = self.slack_to_zulip_map[event["channel"]] msg_data = dict( - type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content + type="stream", + to=zulip_endpoint["stream"], + subject=zulip_endpoint["topic"], + content=content, ) self.zulip_client.send_message(msg_data) @@ -118,11 +137,17 @@ if __name__ == "__main__": sys.path.append(os.path.join(os.path.dirname(__file__), "..")) parser = argparse.ArgumentParser(usage=usage) + config: Dict[str, Any] = bridge_with_slack_config.config + if "channel_mapping" not in config: + print( + 'The key "channel_mapping" is not found in bridge_with_slack_config.py.\n' + "Your config file may be outdated." + ) + exit(1) + print("Starting slack mirroring bot") print("MAKE SURE THE BOT IS SUBSCRIBED TO THE RELEVANT ZULIP STREAM") - config = bridge_with_slack_config.config - # We have to define rtm outside of SlackBridge because the rtm variable is used as a method decorator. rtm = RTMClient(token=config["slack"]["token"])