diff --git a/bots/jabber_mirror.py b/bots/jabber_mirror.py index a39063f..f4a71e3 100755 --- a/bots/jabber_mirror.py +++ b/bots/jabber_mirror.py @@ -1,7 +1,5 @@ -#!/usr/bin/python -# -# Copyright (C) 2013 Permabit, Inc. -# Copyright (C) 2013--2014 Zulip, Inc. +#!/usr/bin/env python +# Copyright (C) 2014 Zulip, Inc. # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -23,402 +21,34 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -# The following is a table showing which kinds of messages are handled by the -# mirror in each mode: -# -# Message origin/type --> | Jabber | Zulip -# Mode/sender-, +-----+----+--------+---- -# V | MUC | PM | stream | PM -# --------------+-------------+-----+----+--------+---- -# | other sender| | x | | -# personal mode +-------------+-----+----+--------+---- -# | self sender | | x | x | x -# ------------- +-------------+-----+----+--------+---- -# | other sender| x | | | -# public mode +-------------+-----+----+--------+---- -# | self sender | | | | +import sys +import subprocess +import os +import traceback +import signal +from zulip import RandomExponentialBackoff -import logging -import threading -import optparse +def die(signal, frame): + # We actually want to exit, so run os._exit (so as not to be caught and restarted) + os._exit(1) -from sleekxmpp import ClientXMPP, InvalidJID, JID -from sleekxmpp.exceptions import IqError, IqTimeout -from ConfigParser import SafeConfigParser -import os, sys, zulip, getpass -import re +signal.signal(signal.SIGINT, die) -__version__ = "1.1" +args = [os.path.join(os.path.dirname(sys.argv[0]), "jabber_mirror_backend.py")] +args.extend(sys.argv[1:]) -def room_to_stream(room): - return room + "/xmpp" - -def stream_to_room(stream): - return stream.lower().rpartition("/xmpp")[0] - -def jid_to_zulip(jid): - suffix = '' - if not jid.username.endswith("-bot"): - suffix = options.zulip_email_suffix - return "%s%s@%s" % (jid.username, suffix, options.zulip_domain) - -def zulip_to_jid(email, jabber_domain): - jid = JID(email, domain=jabber_domain) - if (options.zulip_email_suffix - and options.zulip_email_suffix in jid.username - and not jid.username.endswith("-bot")): - jid.username = jid.username.rpartition(options.zulip_email_suffix)[0] - return jid - -class JabberToZulipBot(ClientXMPP): - def __init__(self, jid, password, rooms): - if jid.resource: - self.nick = jid.resource - else: - self.nick = jid.username - jid.resource = "zulip" - ClientXMPP.__init__(self, jid, password) - self.rooms = set() - self.rooms_to_join = rooms - self.add_event_handler("session_start", self.session_start) - self.add_event_handler("message", self.message) - self.zulip = None - self.use_ipv6 = False - - self.register_plugin('xep_0045') # Jabber chatrooms - self.register_plugin('xep_0199') # XMPP Ping - - def set_zulip_client(self, client): - self.zulip = client - - def session_start(self, event): - self.get_roster() - self.send_presence() - for room in self.rooms_to_join: - self.join_muc(room) - - def join_muc(self, room): - if room in self.rooms: - return - logging.debug("Joining " + room) - self.rooms.add(room) - muc_jid = JID(local=room, domain=options.conference_domain) - xep0045 = self.plugin['xep_0045'] - try: - xep0045.joinMUC(muc_jid, self.nick, wait=True) - except InvalidJID: - logging.error("Could not join room: " + str(muc_jid)) - return - - # Configure the room. Really, we should only do this if the room is - # newly created. - form = None - try: - form = xep0045.getRoomConfig(muc_jid) - except ValueError: - pass - if form: - xep0045.configureRoom(muc_jid, form) - else: - logging.error("Could not configure room: " + str(muc_jid)) - - def leave_muc(self, room): - if room not in self.rooms: - return - logging.debug("Leaving " + room) - self.rooms.remove(room) - muc_jid = JID(local=room, domain=options.conference_domain) - self.plugin['xep_0045'].leaveMUC(muc_jid, 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 options.mode == 'public' or msg['thread'] == u'\u1FFFE': - 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.client.send_message(zulip_message) - if ret.get("result") != "success": - logging.error(ret) - - def group(self, msg): - if options.mode == 'personal' or msg["thread"] == u'\u1FFFE': - return - - subject = msg["subject"] - if len(subject) == 0: - subject = "(no topic)" - stream = room_to_stream(msg['from'].local) - sender_nick = msg.get_mucnick() - if not sender_nick: - # Messages from the room itself have no nickname. We should not try - # to mirror these - return - jid = self.nickname_to_jid(msg.get_mucroom(), sender_nick) - sender = jid_to_zulip(jid) - zulip_message = dict( - forged = "yes", - sender = sender, - type = "stream", - subject = subject, - to = stream, - content = msg["body"], - ) - ret = self.zulip.client.send_message(zulip_message) - if ret.get("result") != "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 JID(local=nick.replace(' ', ''), domain=self.boundjid.domain) - else: - return jid - -class ZulipToJabberBot(object): - def __init__(self, zulip_client): - self.client = zulip_client - self.jabber = None - - def set_jabber_client(self, client): - self.jabber = client - - def process_event(self, event): - if event['type'] == 'message': - message = event["message"] - if message['sender_email'] != self.client.email: - return - - try: - if message['type'] == 'stream': - self.stream_message(message) - elif message['type'] == 'private': - self.private_message(message) - except: - logging.exception("Exception forwarding Zulip => Jabber") - elif event['type'] == 'subscription': - self.process_subscription(event) - elif event['type'] == 'stream': - self.process_stream(event) - - def stream_message(self, msg): - stream = msg['display_recipient'] - if not stream.endswith("/xmpp"): - return - - room = stream_to_room(stream) - jabber_recipient = JID(local=room, domain=options.conference_domain) - outgoing = self.jabber.make_message( - mto = jabber_recipient, - mbody = msg['content'], - mtype = 'groupchat') - outgoing['thread'] = u'\u1FFFE' - outgoing.send() - - def private_message(self, msg): - for recipient in msg['display_recipient']: - if recipient["email"] == self.client.email: - continue - recip_email = recipient['email'] - jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain) - outgoing = self.jabber.make_message( - mto = jabber_recipient, - mbody = msg['content'], - mtype = 'chat') - outgoing['thread'] = u'\u1FFFE' - outgoing.send() - - def process_subscription(self, event): - if event['op'] == 'add': - streams = [s['name'].lower() for s in event['subscriptions']] - streams = [s for s in streams if s.endswith("/xmpp")] - for stream in streams: - self.jabber.join_muc(stream_to_room(stream)) - if event['op'] == 'remove': - streams = [s['name'].lower() for s in event['subscriptions']] - streams = [s for s in streams if s.endswith("/xmpp")] - for stream in streams: - self.jabber.leave_muc(stream_to_room(stream)) - - def process_stream(self, event): - if event['op'] == 'occupy': - streams = [s['name'].lower() for s in event['streams']] - streams = [s for s in streams if s.endswith("/xmpp")] - for stream in streams: - self.jabber.join_muc(stream_to_room(stream)) - if event['op'] == 'vacate': - streams = [s['name'].lower() for s in event['streams']] - streams = [s for s in streams if s.endswith("/xmpp")] - for stream in streams: - self.jabber.leave_muc(stream_to_room(stream)) - -def get_rooms(zulip): - def get_stream_infos(key, method): - ret = method() - if ret.get("result") != "success": - logging.error(ret) - sys.exit("Could not get initial list of Zulip %s" % (key,)) - return ret[key] - - if options.mode == 'public': - stream_infos = get_stream_infos("streams", zulip.client.get_streams) - else: - stream_infos = get_stream_infos("subscriptions", zulip.client.list_subscriptions) - - rooms = [] - for stream_info in stream_infos: - stream = stream_info['name'] - if stream.endswith("/xmpp"): - rooms.append(stream_to_room(stream)) - return rooms - -if __name__ == '__main__': - parser = optparse.OptionParser(epilog= -'''Most general and Jabber configuration options may also be specified in the -zulip configuration file under the jabber_mirror section (exceptions are noted -in their help sections). Keys have the same name as options with hyphens -replaced with underscores. Zulip configuration options go in the api section, -as normal.'''.replace("\n", " ") -) - parser.add_option('--mode', - default=None, - action='store', - help= \ -'''Which mode to run in. Valid options are "personal" and "public". In -"personal" mode, the mirror uses an individual users' credentials and mirrors -all messages they send on Zulip to Jabber and all private Jabber messages to -Zulip. In "public" mode, the mirror uses the credentials for a dedicated mirror -user and mirrors messages sent to Jabber rooms to Zulip. Defaults to -"personal"'''.replace("\n", " ")) - parser.add_option('--zulip-email-suffix', - default=None, - action='store', - help= \ -'''Add the specified suffix to the local part of email addresses constructed -from JIDs and nicks before sending requests to the Zulip server, and remove the -suffix before sending requests to the Jabber server. For example, specifying -"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to -be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This -option does not affect login credentials.'''.replace("\n", " ")) - parser.add_option('-d', '--debug', - help='set logging to DEBUG. Can not be set via config file.', - action='store_const', - dest='log_level', - const=logging.DEBUG, - default=logging.INFO) - - jabber_group = optparse.OptionGroup(parser, "Jabber configuration") - jabber_group.add_option('--jid', - default=None, - action='store', - help="Your Jabber JID. If a resource is specified, " - + "it will be used as the nickname when joining MUCs. " - + "Specifying the nickname is mostly useful if you want " - + "to run the public mirror from a regular user instead of " - + "from a dedicated account.") - jabber_group.add_option('--jabber-password', - default=None, - action='store', - help="Your Jabber password") - jabber_group.add_option('--conference-domain', - default=None, - action='store', - help="Your Jabber conference domain (E.g. conference.jabber.example.com). " - + "If not specifed, \"conference.\" will be prepended to your JID's domain.") - jabber_group.add_option('--no-use-tls', - default=None, - action='store_true') - - parser.add_option_group(jabber_group) - parser.add_option_group(zulip.generate_option_group(parser, "zulip-")) - (options, args) = parser.parse_args() - - logging.basicConfig(level=options.log_level, - format='%(levelname)-8s %(message)s') - - if options.zulip_config_file is None: - config_file = zulip.get_default_config_filename() - else: - config_file = options.zulip_config_file - - config = SafeConfigParser() +backoff = RandomExponentialBackoff() +while backoff.keep_going(): + print "Starting Jabber mirroring bot" try: - with file(config_file, 'r') as f: - config.readfp(f, config_file) - except IOError: - pass - for option in ("jid", "jabber_password", "conference_domain", "mode", "zulip_email_suffix"): - if (getattr(options, option) is None - and config.has_option("jabber_mirror", option)): - setattr(options, option, config.get("jabber_mirror", option)) + ret = subprocess.call(args) + except: + traceback.print_exc() + backoff.fail() - for option in ("no_use_tls",): - if getattr(options, option) is None: - if config.has_option("jabber_mirror", option): - setattr(options, option, config.getboolean("jabber_mirror", option)) - else: - setattr(options, option, False) - - if options.mode is None: - options.mode = "personal" - - if options.zulip_email_suffix is None: - options.zulip_email_suffix = '' - - if options.mode not in ('public', 'personal'): - sys.exit("Bad value for --mode: must be one of 'public' or 'personal'") - - if None in (options.jid, options.jabber_password): - sys.exit("You must specify your Jabber JID and Jabber password either " - + "in the Zulip configuration file or on the commandline") - - zulip = ZulipToJabberBot(zulip.init_from_options(options, "JabberMirror/" + __version__)) - # This won't work for open realms that don't have a consistent domain - options.zulip_domain = zulip.client.email.partition('@')[-1] - - try: - jid = JID(options.jid) - except InvalidJID as e: - sys.exit("Bad JID: %s: %s" % (options.jid, e.message)) - - if options.conference_domain is None: - options.conference_domain = "conference.%s" % (jid.domain,) - - xmpp = JabberToZulipBot(jid, options.jabber_password, get_rooms(zulip)) - - if not xmpp.connect(use_tls=not options.no_use_tls): - sys.exit("Unable to connect to Jabber server") - - xmpp.set_zulip_client(zulip) - zulip.set_jabber_client(xmpp) - - xmpp.process(block=False) - if options.mode == 'public': - event_types = ['stream'] - else: - event_types = ['message', 'subscription'] - - try: - logging.info("Connecting to Zulip.") - zulip.client.call_on_each_event(zulip.process_event, - event_types=event_types) - except BaseException as e: - logging.exception("Exception in main loop") - xmpp.abort() +print "" +print "" +print "ERROR: The Jabber mirroring bot is unable to continue mirroring Jabber." +print "Please contact support@zulip.com if you need assistence." +print "" +sys.exit(1) diff --git a/bots/jabber_mirror_backend.py b/bots/jabber_mirror_backend.py new file mode 100755 index 0000000..a39063f --- /dev/null +++ b/bots/jabber_mirror_backend.py @@ -0,0 +1,424 @@ +#!/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. + +# The following is a table showing which kinds of messages are handled by the +# mirror in each mode: +# +# Message origin/type --> | Jabber | Zulip +# Mode/sender-, +-----+----+--------+---- +# V | MUC | PM | stream | PM +# --------------+-------------+-----+----+--------+---- +# | other sender| | x | | +# personal mode +-------------+-----+----+--------+---- +# | self sender | | x | x | x +# ------------- +-------------+-----+----+--------+---- +# | other sender| x | | | +# public mode +-------------+-----+----+--------+---- +# | self sender | | | | + +import logging +import threading +import optparse + +from sleekxmpp import ClientXMPP, InvalidJID, JID +from sleekxmpp.exceptions import IqError, IqTimeout +from ConfigParser import SafeConfigParser +import os, sys, zulip, getpass +import re + +__version__ = "1.1" + +def room_to_stream(room): + return room + "/xmpp" + +def stream_to_room(stream): + return stream.lower().rpartition("/xmpp")[0] + +def jid_to_zulip(jid): + suffix = '' + if not jid.username.endswith("-bot"): + suffix = options.zulip_email_suffix + return "%s%s@%s" % (jid.username, suffix, options.zulip_domain) + +def zulip_to_jid(email, jabber_domain): + jid = JID(email, domain=jabber_domain) + if (options.zulip_email_suffix + and options.zulip_email_suffix in jid.username + and not jid.username.endswith("-bot")): + jid.username = jid.username.rpartition(options.zulip_email_suffix)[0] + return jid + +class JabberToZulipBot(ClientXMPP): + def __init__(self, jid, password, rooms): + if jid.resource: + self.nick = jid.resource + else: + self.nick = jid.username + jid.resource = "zulip" + ClientXMPP.__init__(self, jid, password) + self.rooms = set() + self.rooms_to_join = rooms + self.add_event_handler("session_start", self.session_start) + self.add_event_handler("message", self.message) + self.zulip = None + self.use_ipv6 = False + + self.register_plugin('xep_0045') # Jabber chatrooms + self.register_plugin('xep_0199') # XMPP Ping + + def set_zulip_client(self, client): + self.zulip = client + + def session_start(self, event): + self.get_roster() + self.send_presence() + for room in self.rooms_to_join: + self.join_muc(room) + + def join_muc(self, room): + if room in self.rooms: + return + logging.debug("Joining " + room) + self.rooms.add(room) + muc_jid = JID(local=room, domain=options.conference_domain) + xep0045 = self.plugin['xep_0045'] + try: + xep0045.joinMUC(muc_jid, self.nick, wait=True) + except InvalidJID: + logging.error("Could not join room: " + str(muc_jid)) + return + + # Configure the room. Really, we should only do this if the room is + # newly created. + form = None + try: + form = xep0045.getRoomConfig(muc_jid) + except ValueError: + pass + if form: + xep0045.configureRoom(muc_jid, form) + else: + logging.error("Could not configure room: " + str(muc_jid)) + + def leave_muc(self, room): + if room not in self.rooms: + return + logging.debug("Leaving " + room) + self.rooms.remove(room) + muc_jid = JID(local=room, domain=options.conference_domain) + self.plugin['xep_0045'].leaveMUC(muc_jid, 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 options.mode == 'public' or msg['thread'] == u'\u1FFFE': + 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.client.send_message(zulip_message) + if ret.get("result") != "success": + logging.error(ret) + + def group(self, msg): + if options.mode == 'personal' or msg["thread"] == u'\u1FFFE': + return + + subject = msg["subject"] + if len(subject) == 0: + subject = "(no topic)" + stream = room_to_stream(msg['from'].local) + sender_nick = msg.get_mucnick() + if not sender_nick: + # Messages from the room itself have no nickname. We should not try + # to mirror these + return + jid = self.nickname_to_jid(msg.get_mucroom(), sender_nick) + sender = jid_to_zulip(jid) + zulip_message = dict( + forged = "yes", + sender = sender, + type = "stream", + subject = subject, + to = stream, + content = msg["body"], + ) + ret = self.zulip.client.send_message(zulip_message) + if ret.get("result") != "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 JID(local=nick.replace(' ', ''), domain=self.boundjid.domain) + else: + return jid + +class ZulipToJabberBot(object): + def __init__(self, zulip_client): + self.client = zulip_client + self.jabber = None + + def set_jabber_client(self, client): + self.jabber = client + + def process_event(self, event): + if event['type'] == 'message': + message = event["message"] + if message['sender_email'] != self.client.email: + return + + try: + if message['type'] == 'stream': + self.stream_message(message) + elif message['type'] == 'private': + self.private_message(message) + except: + logging.exception("Exception forwarding Zulip => Jabber") + elif event['type'] == 'subscription': + self.process_subscription(event) + elif event['type'] == 'stream': + self.process_stream(event) + + def stream_message(self, msg): + stream = msg['display_recipient'] + if not stream.endswith("/xmpp"): + return + + room = stream_to_room(stream) + jabber_recipient = JID(local=room, domain=options.conference_domain) + outgoing = self.jabber.make_message( + mto = jabber_recipient, + mbody = msg['content'], + mtype = 'groupchat') + outgoing['thread'] = u'\u1FFFE' + outgoing.send() + + def private_message(self, msg): + for recipient in msg['display_recipient']: + if recipient["email"] == self.client.email: + continue + recip_email = recipient['email'] + jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain) + outgoing = self.jabber.make_message( + mto = jabber_recipient, + mbody = msg['content'], + mtype = 'chat') + outgoing['thread'] = u'\u1FFFE' + outgoing.send() + + def process_subscription(self, event): + if event['op'] == 'add': + streams = [s['name'].lower() for s in event['subscriptions']] + streams = [s for s in streams if s.endswith("/xmpp")] + for stream in streams: + self.jabber.join_muc(stream_to_room(stream)) + if event['op'] == 'remove': + streams = [s['name'].lower() for s in event['subscriptions']] + streams = [s for s in streams if s.endswith("/xmpp")] + for stream in streams: + self.jabber.leave_muc(stream_to_room(stream)) + + def process_stream(self, event): + if event['op'] == 'occupy': + streams = [s['name'].lower() for s in event['streams']] + streams = [s for s in streams if s.endswith("/xmpp")] + for stream in streams: + self.jabber.join_muc(stream_to_room(stream)) + if event['op'] == 'vacate': + streams = [s['name'].lower() for s in event['streams']] + streams = [s for s in streams if s.endswith("/xmpp")] + for stream in streams: + self.jabber.leave_muc(stream_to_room(stream)) + +def get_rooms(zulip): + def get_stream_infos(key, method): + ret = method() + if ret.get("result") != "success": + logging.error(ret) + sys.exit("Could not get initial list of Zulip %s" % (key,)) + return ret[key] + + if options.mode == 'public': + stream_infos = get_stream_infos("streams", zulip.client.get_streams) + else: + stream_infos = get_stream_infos("subscriptions", zulip.client.list_subscriptions) + + rooms = [] + for stream_info in stream_infos: + stream = stream_info['name'] + if stream.endswith("/xmpp"): + rooms.append(stream_to_room(stream)) + return rooms + +if __name__ == '__main__': + parser = optparse.OptionParser(epilog= +'''Most general and Jabber configuration options may also be specified in the +zulip configuration file under the jabber_mirror section (exceptions are noted +in their help sections). Keys have the same name as options with hyphens +replaced with underscores. Zulip configuration options go in the api section, +as normal.'''.replace("\n", " ") +) + parser.add_option('--mode', + default=None, + action='store', + help= \ +'''Which mode to run in. Valid options are "personal" and "public". In +"personal" mode, the mirror uses an individual users' credentials and mirrors +all messages they send on Zulip to Jabber and all private Jabber messages to +Zulip. In "public" mode, the mirror uses the credentials for a dedicated mirror +user and mirrors messages sent to Jabber rooms to Zulip. Defaults to +"personal"'''.replace("\n", " ")) + parser.add_option('--zulip-email-suffix', + default=None, + action='store', + help= \ +'''Add the specified suffix to the local part of email addresses constructed +from JIDs and nicks before sending requests to the Zulip server, and remove the +suffix before sending requests to the Jabber server. For example, specifying +"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to +be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This +option does not affect login credentials.'''.replace("\n", " ")) + parser.add_option('-d', '--debug', + help='set logging to DEBUG. Can not be set via config file.', + action='store_const', + dest='log_level', + const=logging.DEBUG, + default=logging.INFO) + + jabber_group = optparse.OptionGroup(parser, "Jabber configuration") + jabber_group.add_option('--jid', + default=None, + action='store', + help="Your Jabber JID. If a resource is specified, " + + "it will be used as the nickname when joining MUCs. " + + "Specifying the nickname is mostly useful if you want " + + "to run the public mirror from a regular user instead of " + + "from a dedicated account.") + jabber_group.add_option('--jabber-password', + default=None, + action='store', + help="Your Jabber password") + jabber_group.add_option('--conference-domain', + default=None, + action='store', + help="Your Jabber conference domain (E.g. conference.jabber.example.com). " + + "If not specifed, \"conference.\" will be prepended to your JID's domain.") + jabber_group.add_option('--no-use-tls', + default=None, + action='store_true') + + parser.add_option_group(jabber_group) + parser.add_option_group(zulip.generate_option_group(parser, "zulip-")) + (options, args) = parser.parse_args() + + logging.basicConfig(level=options.log_level, + format='%(levelname)-8s %(message)s') + + if options.zulip_config_file is None: + config_file = zulip.get_default_config_filename() + else: + config_file = options.zulip_config_file + + config = SafeConfigParser() + try: + with file(config_file, 'r') as f: + config.readfp(f, config_file) + except IOError: + pass + for option in ("jid", "jabber_password", "conference_domain", "mode", "zulip_email_suffix"): + if (getattr(options, option) is None + and config.has_option("jabber_mirror", option)): + setattr(options, option, config.get("jabber_mirror", option)) + + for option in ("no_use_tls",): + if getattr(options, option) is None: + if config.has_option("jabber_mirror", option): + setattr(options, option, config.getboolean("jabber_mirror", option)) + else: + setattr(options, option, False) + + if options.mode is None: + options.mode = "personal" + + if options.zulip_email_suffix is None: + options.zulip_email_suffix = '' + + if options.mode not in ('public', 'personal'): + sys.exit("Bad value for --mode: must be one of 'public' or 'personal'") + + if None in (options.jid, options.jabber_password): + sys.exit("You must specify your Jabber JID and Jabber password either " + + "in the Zulip configuration file or on the commandline") + + zulip = ZulipToJabberBot(zulip.init_from_options(options, "JabberMirror/" + __version__)) + # This won't work for open realms that don't have a consistent domain + options.zulip_domain = zulip.client.email.partition('@')[-1] + + try: + jid = JID(options.jid) + except InvalidJID as e: + sys.exit("Bad JID: %s: %s" % (options.jid, e.message)) + + if options.conference_domain is None: + options.conference_domain = "conference.%s" % (jid.domain,) + + xmpp = JabberToZulipBot(jid, options.jabber_password, get_rooms(zulip)) + + if not xmpp.connect(use_tls=not options.no_use_tls): + sys.exit("Unable to connect to Jabber server") + + xmpp.set_zulip_client(zulip) + zulip.set_jabber_client(xmpp) + + xmpp.process(block=False) + if options.mode == 'public': + event_types = ['stream'] + else: + event_types = ['message', 'subscription'] + + try: + logging.info("Connecting to Zulip.") + zulip.client.call_on_each_event(zulip.process_event, + event_types=event_types) + except BaseException as e: + logging.exception("Exception in main loop") + xmpp.abort()