#!/usr/bin/python # # Copyright (C) 2013 Permabit, Inc. # Copyright (C) 2013--2014 Zulip, Inc. # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import logging import threading import optparse from sleekxmpp import ClientXMPP from sleekxmpp.exceptions import IqError, IqTimeout import os, sys, zulip, getpass import re def room_to_stream(room): return str(room).rpartition("@")[0] def jid_to_zulip(jid): return "%s@%s" % (str(jid).rpartition("@")[0], options.zulip_domain) class JabberToZulipBot(ClientXMPP): def __init__(self, nick, password, rooms, openfire=False): self.nick = nick jid = "%s/zulip" % (nick,) ClientXMPP.__init__(self, jid, password) self.password = password self.rooms = rooms self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) self.password = password self.zulip = None self.use_ipv6 = False if options.conference_domain is not None: # Jabber chatroom support. self.register_plugin('xep_0045') if openfire: # OpenFire Jabber servers use a different SSL protocol version import ssl self.ssl_version = ssl.PROTOCOL_SSLv3 def set_zulip_client(self, client): self.zulip = client def session_start(self, event): self.get_roster() self.send_presence() if options.stream_mirror and options.conference_domain is not None: for room in self.rooms: self.plugin['xep_0045'].joinMUC(room + "@" + options.conference_domain, self.nick) def message(self, msg): try: if msg["type"] == "groupchat": return self.group(msg) elif msg["type"] == "chat": return self.private(msg) else: logging.warning("Got unexpected message type") logging.warning(msg) except Exception: logging.exception("Error forwarding Jabber => Zulip") def private(self, msg): if msg["from"] == self.jid or msg['thread'] == u'\u1B80': return sender = jid_to_zulip(msg["from"]) recipient = jid_to_zulip(msg["to"]) zulip_message = dict( sender = sender, type = "private", to = recipient, content = msg["body"], ) ret = self.zulip.send_message(zulip_message) if ret.get("status") != "success": logging.error(ret) def group(self, msg): if msg.get_mucnick() == self.nick or msg["thread"] == u'\u1B80': return subject = msg["subject"] if len(subject) == 0: subject = "(no topic)" stream = room_to_stream(msg.get_mucroom()) jid = self.nickname_to_jid(msg.get_mucroom(), msg.get_mucnick()) sender = jid_to_zulip(jid) zulip_message = dict( forged = "yes", sender = sender, type = "stream", subject = subject, to = stream, content = msg["body"], ) ret = self.zulip.send_message(zulip_message) if ret.get("status") != "success": logging.error(ret) def nickname_to_jid(self, room, nick): jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid") if (jid is None or jid == ''): return nick.replace(' ', '') + "@" + options.jabber_domain else: return jid class ZulipToJabberBot(zulip.Client): def __init__(self, email, api_key): zulip.Client.__init__(self, email, api_key, client="jabber_mirror", site=options.zulip_server, verbose=False) self.jabber = None self.email = email def set_jabber_client(self, client): self.jabber = client def process_message(self, event): try: if event['type'] != 'message': return message = event["message"] if message['sender_email'] != self.email: return if message['type'] == 'stream': self.stream_message(message) elif message['type'] == 'private': self.private_message(message) except: logging.exception("Exception forwarding Zulip => Jabber") def stream_message(self, msg): jabber_recipient = "%s@%s" % (msg['display_recipient'], options.conference_domain) outgoing = self.jabber.make_message( mto = jabber_recipient, mbody = msg['content'], mtype = 'groupchat') outgoing['thread'] = u'\u1B80' outgoing.send() def private_message(self, msg): for recipient in msg['display_recipient']: if recipient["email"] == self.email: continue recip_email = recipient['email'] username = recip_email[:recip_email.rfind(options.zulip_domain)] jabber_recipient = username + options.jabber_domain outgoing = self.jabber.make_message( mto = jabber_recipient, mbody = msg['content'], mtype = 'chat') outgoing['thread'] = u'\u1B80' outgoing.send() if __name__ == '__main__': logging.basicConfig(level=logging.INFO, format='%(levelname)-8s %(message)s') parser = optparse.OptionParser() parser.add_option('--openfire', default=False, action='store_true', help="Set if Jabber server is an OpenFire server") parser.add_option('--password', default=None, action='store', help="Your Jabber password") parser.add_option('--jabber-domain', default=None, action='store', help="Your Jabber server") parser.add_option('--stream-mirror', default=False, action='store_true') parser.add_option('--no-use-tls', default=False, action='store_true') parser.add_option('--zulip-server', default="https://api.zulip.com", action='store', help="Your Zulip API server (only needed for Zulip Enterprise)") parser.add_option('--conference-domain', default=None, action='store', help="Your Jabber conference domain (E.g. conference.jabber.example.com)") (options, args) = parser.parse_args() if len(args) < 2: sys.exit("Usage: %s EMAIL ZULIP_APIKEY" % (sys.argv[0],)); email = args[0] ZULIP_API_KEY = args[1] if options.password is None: options.password = getpass.getpass("Jabber password: ") if options.jabber_domain is None: sys.exit("Must specify a Jabber server") (username, options.zulip_domain) = email.split("@") jabber_username = username + '@' + options.jabber_domain zulip = ZulipToJabberBot(email=email, api_key=ZULIP_API_KEY); rooms = [s['name'] for s in zulip.get_streams()['streams']] xmpp = JabberToZulipBot(jabber_username, options.password, rooms, openfire=options.openfire) xmpp.connect(use_tls=not options.no_use_tls) xmpp.process(block=False) xmpp.set_zulip_client(zulip) zulip.set_jabber_client(xmpp) try: logging.info("Connecting to Zulip.") zulip.call_on_each_event(zulip.process_message) zulip.session_start() except BaseException as e: logging.exception("Exception in main loop") xmpp.abort()