Skip to content

Add Rocket.Chat integration #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions zulip/integrations/rocket.chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Zulip <--> Rocket.Chat mirror

## Usage

0. `pip install zulip rocketchat_API`

### 1. Zulip endpoint
1. Create a generic Zulip bot
2. (don't forget this step!) Make sure the bot is subscribed to the relevant stream
3. Enter the bot's email and api_key into rocket_mirror_config.py
4. Enter the destination subject and realm into the config file

### 2. Rocket.Chat endpoint
1. Create a user
2. Enter the user's username and password into rocket_mirror_config.py
3. Enter the Rocket.Chat server url into the config file
4. Enter the channel id and channel name to be mirrored into the config file

After the steps above have been completed, run `./rocket.chat-mirror` to start the mirroring.
note: Run the script relative to its directory !
124 changes: 124 additions & 0 deletions zulip/integrations/rocket.chat/rocket.chat-mirror
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3

import os
import logging
import time
import signal
import traceback
import multiprocessing as mp
import datetime

from types import FrameType
from typing import Any, Callable, Dict

from rocketchat_API.rocketchat import RocketChat
from rocket_mirror_config import config
import zulip
from pprint import pprint

Cfg = Dict[str, Any]

def die(signal: int, frame: FrameType) -> None:

# We actually want to exit, so run os._exit (so as not to be caught and restarted)
os._exit(1)

def zulip_to_rocket_username(full_name: str, site: str) -> str:
return "@**{0}**:{1}".format(full_name, site)

def rocket_to_zulip(zulip_client: zulip.Client, cfg: Cfg, res: Dict[str, Any]) -> None:
zulip_cfg = cfg["zulip"]
rocket_cfg = cfg["rocket"]

if res['success']:
for msg in res['messages']:
user = msg['u']
content = "**{0}**: {1}".format(user['name'], msg['msg'])
pprint(msg['u']['username'])

is_not_from_bot = user['username'] != rocket_cfg['username']
if is_not_from_bot:
msg_data = dict(
sender=zulip_client.email,
type="stream",
to=zulip_cfg["stream"],
subject=zulip_cfg["subject"],
content=content)
print(msg_data)
zulip_client.send_message(msg_data)

def zulip_to_rocket(rocket_client: RocketChat,
cfg: Cfg) -> Callable[[Dict[str, Any]], None]:
zulip_cfg = cfg["zulip"]
rocket_cfg = cfg["rocket"]
site_without_http = zulip_cfg["site"].replace("https://", "").replace("http://", "")

def _zulip_to_rocket(msg: Dict[str, Any]) -> None:
"""Zulip -> Matrix
"""
isa_stream = msg["type"] == "stream"
not_from_bot = msg["sender_email"] != zulip_cfg["email"]
in_the_specified_stream = msg["display_recipient"] == zulip_cfg["stream"]
at_the_specified_subject = msg["subject"] == zulip_cfg["subject"]
if isa_stream and not_from_bot and in_the_specified_stream and at_the_specified_subject:
rocket_username = zulip_to_rocket_username(msg["sender_full_name"], site_without_http)
rocket_text = "{0}: {1}".format(rocket_username,
msg["content"])
pprint(rocket_client.chat_post_message(rocket_text, channel=rocket_cfg["channel_id"]).json())
return _zulip_to_rocket

def rocket_listener(rocket_client: RocketChat, zulip_client: zulip.Client, cfg: Cfg):
interval = 2
rocket_cfg = cfg["rocket"]
while True:
now = datetime.datetime.utcnow()
oldest = now - datetime.timedelta(seconds=interval)
oldest_str = str(oldest)[:-3] + 'Z'
res = rocket_client.channels_history(rocket_cfg['channel_id'],
oldest=oldest_str).json()
rocket_to_zulip(zulip_client, cfg, res)
time.sleep(interval)

if __name__ == '__main__':
signal.signal(signal.SIGINT, die)
logging.basicConfig(level=logging.WARNING)

# Get config for each clients
zulip_config = config["zulip"]
rocket_config = config["rocket"]

# Initiate clients
print("Starting rocketchat mirroring bot")

backoff = zulip.RandomExponentialBackoff(timeout_success_equivalent=300)
while backoff.keep_going():
try:
zulip_client = zulip.Client(email=zulip_config["email"],
api_key=zulip_config["api_key"],
site=zulip_config["site"])

rocket_client = RocketChat(rocket_config['username'],
rocket_config['password'],
server_url=rocket_config['server_url'])

# A bidirectional mirror

# returns configured function handler
zulip_message_handler = zulip_to_rocket(rocket_client, config)
p1 = mp.Process(target=zulip_client.call_on_each_message,
args=(zulip_message_handler,))

p2 = mp.Process(target=rocket_listener,
args=(rocket_client, zulip_client, config))

print("Starting message handler on Zulip client")
p1.start()
print("Starting message handler on Rocket.Chat client")
p2.start()

p1.join()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a more idiomatic way to handle two forever-running subprocesses?

Copy link
Contributor Author

@rht rht Nov 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is only if one of the client has the feature to be non-blocking. So far, only Matrix does: https://github.com/rht/python-zulip-api/blob/b94bf2a31325243f0c4e5402087f17fca77da79e/zulip/integrations/matrix/matrix_bridge.py#L94.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If zulip_client.call_on_each_message can be made non-blocking, then no multiprocessing is necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually have no problem with these being multiprocess, since the two processes are essentially independent and don't require any coordination. I'm more concerned about tearing them down, so it might make sense to use something like supervisord.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm concerned with the zombie processes even with p.join(). To remain lightweight, what about using multithreading like the Matrix client does[1]?

[1] https://github.com/matrix-org/matrix-python-sdk/blob/cb815ad96c60573f189ead02037f4d1207554717/matrix_client/client.py#L409-L414

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually prefer multiple processes here.

p2.join()

except Exception:
traceback.print_exc()
backoff.fail()
16 changes: 16 additions & 0 deletions zulip/integrations/rocket.chat/rocket_mirror_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
config = {
"rocket": {
"server_url": "https://chat.serverurl.org",
"username": "botusername",
"password": "botpassword",
"channel_id": "idofthechannel",
"channel_name": "nameofthechannel"
},
"zulip": {
"email": "[email protected]",
"api_key": "someapikey",
"site": "https://chat.someserver.org",
"stream": "somestream",
"subject": "somesubject"
}
}