python-zulip-api/zulip/integrations/bridge_with_slack/run-slack-bridge
rht 58e51c7ae5 Slack bridge: Bump slack-sdk to 3.11.2.
We also upgrade the RTM client API from v1 to v2. This is so that we no
longer require aiohttp. If we use v1, it would still require aiohttp.
2021-10-19 16:26:40 -07:00

148 lines
5.4 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import os
import sys
import threading
import traceback
from typing import Any, Callable, Dict
import bridge_with_slack_config
import slack_sdk
from slack_sdk.rtm_v2 import RTMClient
import zulip
# change these templates to change the format of displayed message
ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}"
SLACK_MESSAGE_TEMPLATE = "<{username}> {message}"
def check_zulip_message_validity(msg: Dict[str, Any], config: Dict[str, Any]) -> bool:
is_a_stream = msg["type"] == "stream"
in_the_specified_stream = msg["display_recipient"] == config["stream"]
at_the_specified_subject = msg["subject"] == config["topic"]
# 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
class SlackBridge:
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
self.zulip_config = config["zulip"]
self.slack_config = config["slack"]
# 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"]
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"])
def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None:
words = zulip_msg["content"].split(" ")
for w in words:
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:
message_valid = check_zulip_message_validity(msg, self.zulip_config)
if message_valid:
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,
text=slack_text,
)
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"] != self.channel:
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"])
msg_data = dict(
type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content
)
self.zulip_client.send_message(msg_data)
self.slack_client.start()
if __name__ == "__main__":
usage = """run-slack-bridge
Relay each message received at a specified subject in a specified stream from
the first realm to a channel in a Slack workspace.
"""
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
parser = argparse.ArgumentParser(usage=usage)
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"])
backoff = zulip.RandomExponentialBackoff(timeout_success_equivalent=300)
while backoff.keep_going():
try:
sb = SlackBridge(config)
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()
zp.join()
sp.join()
except Exception:
traceback.print_exc()
backoff.fail()