From 864f2e22198ec11c4ce58c9af251a0c83a6c5a1f Mon Sep 17 00:00:00 2001 From: PieterCK Date: Fri, 7 Jun 2024 22:22:41 +0700 Subject: [PATCH 1/3] slack bridge: Remove the legacy RTM API based bridge. Slack Bridge now uses the Slack Webhook integration to get messages accross from Slack instead of the legacy RTM API based we preivouslt use. --- .../bridge_with_slack_config.py | 4 +- .../bridge_with_slack/run-slack-bridge | 75 ++++++------------- 2 files changed, 25 insertions(+), 54 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 9dd733313..f1a89d82f 100644 --- a/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py +++ b/zulip/integrations/bridge_with_slack/bridge_with_slack_config.py @@ -13,8 +13,8 @@ "channel_mapping": { # Slack channel; must be channel ID "C5Z5N7R8A": { - # Zulip stream - "stream": "test here", + # Zulip channel + "channel": "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 32c6deb1b..ff90ca807 100755 --- a/zulip/integrations/bridge_with_slack/run-slack-bridge +++ b/zulip/integrations/bridge_with_slack/run-slack-bridge @@ -8,13 +8,11 @@ import traceback from typing import Any, Callable, Dict, Optional, Tuple import bridge_with_slack_config -import slack_sdk -from slack_sdk.rtm_v2 import RTMClient +from slack_sdk.web.client import WebClient import zulip # change these templates to change the format of displayed message -ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}" SLACK_MESSAGE_TEMPLATE = "<{username}> {message}" StreamTopicT = Tuple[str, str] @@ -41,15 +39,26 @@ def get_slack_channel_for_zulip_message( return zulip_to_slack_map[stream_topic] +def check_token_access(token: str) -> None: + if token.startswith("xoxp-"): + print( + "--- Warning! ---\n" + "You entered a Slack user token, please copy the token under\n" + "'Bot User OAuth Token' which starts with 'xoxb-...'." + ) + sys.exit(1) + elif token.startswith("xoxb-"): + return + + class SlackBridge: def __init__(self, config: Dict[str, Any]) -> None: self.config = config 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() + (z["channel"], z["topic"]): s for s, z in config["channel_mapping"].items() } # zulip-specific @@ -65,11 +74,9 @@ class SlackBridge: # https://github.com/zulip/python-zulip-api/issues/761 is fixed. self.zulip_client_constructor = zulip_client_constructor - # slack-specific - self.slack_client = rtm # Spawn a non-websocket client for getting the users # list and for posting messages in Slack. - self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"]) + self.slack_webclient = WebClient(token=self.slack_config["token"]) def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None: words = zulip_msg["content"].split(" ") @@ -77,13 +84,6 @@ class SlackBridge: if w.startswith("@"): zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">") - def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None: - words = msg["text"].split(" ") - for w in words: - if w.startswith("<@") and w.endswith(">"): - _id = w[2:-1] - msg["text"] = msg["text"].replace(_id, self.slack_id_to_name[_id]) - def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]: def _zulip_to_slack(msg: Dict[str, Any]) -> None: slack_channel = get_slack_channel_for_zulip_message( @@ -101,36 +101,6 @@ class SlackBridge: return _zulip_to_slack - def run_slack_listener(self) -> None: - members = self.slack_webclient.users_list()["members"] - # See also https://api.slack.com/changelog/2017-09-the-one-about-usernames - self.slack_id_to_name: Dict[str, str] = { - u["id"]: u["profile"].get("display_name", u["profile"]["real_name"]) for u in members - } - self.slack_name_to_id = {v: k for k, v in self.slack_id_to_name.items()} - - @rtm.on("message") - def slack_to_zulip(client: RTMClient, event: Dict[str, Any]) -> None: - if event["channel"] not in self.slack_to_zulip_map: - return - user_id = event["user"] - user = self.slack_id_to_name[user_id] - from_bot = user == self.slack_config["username"] - if from_bot: - 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=zulip_endpoint["stream"], - subject=zulip_endpoint["topic"], - content=content, - ) - self.zulip_client_constructor().send_message(msg_data) - - self.slack_client.start() - if __name__ == "__main__": usage = """run-slack-bridge @@ -142,6 +112,8 @@ if __name__ == "__main__": sys.path.append(os.path.join(os.path.dirname(__file__), "..")) parser = argparse.ArgumentParser(usage=usage) + args = parser.parse_args() + config: Dict[str, Any] = bridge_with_slack_config.config if "channel_mapping" not in config: print( @@ -150,12 +122,11 @@ if __name__ == "__main__": ) sys.exit(1) + check_token_access(config["slack"]["token"]) + print("Starting slack mirroring bot") print("MAKE SURE THE BOT IS SUBSCRIBED TO THE RELEVANT ZULIP STREAM(S) & SLACK CHANNEL(S)!") - # We have to define rtm outside of SlackBridge because the rtm variable is used as a method decorator. - rtm = RTMClient(token=config["slack"]["token"]) - backoff = zulip.RandomExponentialBackoff(timeout_success_equivalent=300) while backoff.keep_going(): try: @@ -164,14 +135,14 @@ if __name__ == "__main__": zp = threading.Thread( target=sb.zulip_client.call_on_each_message, args=(sb.zulip_to_slack(),) ) - sp = threading.Thread(target=sb.run_slack_listener, args=()) print("Starting message handler on Zulip client") zp.start() - print("Starting message handler on Slack client") - sp.start() + print( + "Make sure your Slack Webhook integration is running\n" + "to receive messages from Slack." + ) zp.join() - sp.join() except Exception: traceback.print_exc() backoff.fail() From ddc1dccc10d172fbdc988d337516511ac7f9e39e Mon Sep 17 00:00:00 2001 From: PieterCK Date: Tue, 11 Jun 2024 21:39:24 +0700 Subject: [PATCH 2/3] slack bridge: Add logic to prevent looping messages. When using Slack Webhook integration to get messages from Slack to Zulip, we don't want to send back messages from the Slack integration bot. This prevents that by filtering out any messages from the Slack Webhook bots when sending messages from Zulip to Slack.. Fixes #825. --- zulip/integrations/bridge_with_slack/run-slack-bridge | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zulip/integrations/bridge_with_slack/run-slack-bridge b/zulip/integrations/bridge_with_slack/run-slack-bridge index ff90ca807..225f1af41 100755 --- a/zulip/integrations/bridge_with_slack/run-slack-bridge +++ b/zulip/integrations/bridge_with_slack/run-slack-bridge @@ -84,12 +84,18 @@ class SlackBridge: if w.startswith("@"): zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">") + def is_message_from_slack(self, msg: Dict[str, Any]) -> bool: + # Check whether or not this message is from Slack to prevent + # them from being tossed back to Zulip. + return msg["sender_email"] == self.zulip_config.get("email") + def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]: def _zulip_to_slack(msg: Dict[str, Any]) -> None: slack_channel = get_slack_channel_for_zulip_message( msg, self.zulip_to_slack_map, self.zulip_config["email"] ) - if slack_channel is not None: + + if slack_channel is not None and not self.is_message_from_slack(msg): self.wrap_slack_mention_with_bracket(msg) slack_text = SLACK_MESSAGE_TEMPLATE.format( username=msg["sender_full_name"], message=msg["content"] From 67c80343b3b5c92952bc22488a1dea0fa5a8a92a Mon Sep 17 00:00:00 2001 From: PieterCK Date: Tue, 11 Jun 2024 19:25:47 +0700 Subject: [PATCH 3/3] slack bridge: Update doc for the new webhook based Slack Bridge. This commit updates the Slack Bridge doc, primarily guiding the user to use our Slack Webhook integration. With significant rewriting by tabbott. --- .../integrations/bridge_with_slack/README.md | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/zulip/integrations/bridge_with_slack/README.md b/zulip/integrations/bridge_with_slack/README.md index 209c64446..c1698bc5b 100644 --- a/zulip/integrations/bridge_with_slack/README.md +++ b/zulip/integrations/bridge_with_slack/README.md @@ -1,33 +1,51 @@ # Slack <--> Zulip bridge -This is a bridge between Slack and Zulip. +This integration is a bridge with Slack, delivering messages from +Zulip into Slack. It is designed for bidirectional bridging, with the +[Slack integration](https://zulip.com/integrations/doc/slack) used to +deliver messages from Slack into Zulip. + +Note that using these integrations together for bidirectional bridging +requires the updated version of the Slack integration included in +Zulip 9.4+. ## Usage ### 1. Zulip endpoint -1. Create a generic Zulip bot, with a full name like `Slack Bot`. -2. (Important) Subscribe the bot user to the Zulip stream you'd like to bridge your Slack - channel into. -3. In the `zulip` section of the configuration file, enter the bot's `zuliprc` - details (`email`, `api_key`, and `site`). -4. In the same section, also enter the Zulip `stream` and `topic`. + +1. Create a generic Zulip bot, with a full name like `Slack Bridge`. + +2. [Subscribe](https://zulip.com/help/manage-user-channel-subscriptions#subscribe-a-user-to-a-channel) + the bot user to the Zulip channel(s) you'd like to bridge with + Slack. + +3. Create a [Slack webhook integration bot](https://zulip.com/integrations/doc/slack) + to get messages from Slack to Zulip. Make sure to follow the additional instruction + for setting up a Slack bridge. + +4. In the `zulip` section of the `bridge_with_slack_config.py` + configuration file, the bot's `zuliprc` details (`email`, + `api_key`, and `site`). + +5. In the `channel_mapping` section, enter the Zulip `channel` and + `topic` that you'd like to use for each Slack channel. Make sure + that they match the same `channel` and `topic` you configured in + steps 2 and 3. ### 2. Slack endpoint -1. Make sure Websocket isn't blocked in the computer where you run this bridge. - Test it at https://www.websocket.org/echo.html. -2. Go to https://api.slack.com/apps?new_classic_app=1 and create a new classic - app (note: must be a classic app). Choose a bot name that will be put into - bridge_with_slack_config.py, e.g. "zulip_mirror". In the process of doing - this, you need to add oauth token scope. Simply choose `bot`. Slack will say - that this is a legacy scope, but we still need to use it anyway. The reason - why we need the legacy scope is because otherwise the RTM API wouldn't work. - We might remove the RTM API usage in newer version of this bot. Make sure to - install the app to the workspace. When successful, you should see a token - that starts with "xoxb-...". There is also a token that starts with - "xoxp-...", we need the "xoxb-..." one. -3. Go to "App Home", click the button "Add Legacy Bot User". -4. (Important) Make sure the bot is subscribed to the channel. You can do this by typing e.g. `/invite @zulip_mirror` in the relevant channel. -5. In the `slack` section of the Zulip-Slack bridge configuration file, enter the bot name (e.g. "zulip_mirror") and token, and the channel ID (note: must be ID, not name). + +1. Go to the [Slack Apps menu](https://api.slack.com/apps) and open the same Slack app + that you used to set up the Slack Webhook integration previously. + +2. Navigate to the "OAuth & Permissions" menu and scroll down to the "Scopes" + section in the same page. Make sure "Bot Token Scopes" includes: `chat:write` + +3. Next, also in the same menu find and note down the "Bot User OAuth Token". + It starts with "xoxb-..." and not "xoxp". + +4. In the `slack` section of `bridge_with_slack_config.py`, enter the + bot name (e.g "slack_bridge"), token (e.g xoxb-...), and the + channel ID (note: must be ID, not name). ### Running the bridge