otr-bot.py 5.31 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
#!/usr/bin/python
import sys
import jabberbot
import xmpp
import otr

# Minimal implementation of the OTR callback store that only does what
# we absolutely need.
class OtrCallbackStore():

    def inject_message(self, opdata, accountname, protocol, recipient, message):
12 13 14
        mess = opdata["message"]
        mess.setTo(recipient)
        mess.setBody(message)
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
        opdata["send_raw_message_fn"](mess)

    def policy(self, opdata, context):
        return opdata["default_policy"]

    def create_privkey(self, **kwargs):
        raise Exception(
            "We should have loaded a key already! Most likely the 'name' " +
            "and/or 'protocol' fields are wrong in the key you provided.")

    def account_name(self, opdata, account, protocol):
        return account

    def protocol_name(self, opdata, protocol):
        return protocol

    def is_logged_in(self, **kwargs):
        return 1

    def max_message_size(self, **kwargs):
        return 0

    def display_otr_message(self, **kwargs):
        return 0

    # The rest we don't care at all about
    def write_fingerprints(self, **kwargs): pass
    def notify(self, **kwargs): pass
    def update_context_list(self, **kwargs): pass
    def new_fingerprint(self, **kwargs): pass
    def gone_secure(self, **kwargs): pass
    def gone_insecure(self, **kwargs): pass
    def still_secure(self, **kwargs): pass
    def log_message(self, **kwargs): pass

class OtrBot(jabberbot.JabberBot):

    PING_FREQUENCY = 60

    def __init__(self, username, password, otr_key_path):
        super(OtrBot, self).__init__(username, password)
        self.__account = self.jid.getNode()
        self.__protocol = "xmpp"
        self.__otr_ustate = otr.otrl_userstate_create()
        otr.otrl_privkey_read(self.__otr_ustate, otr_key_path)
        self.__opdata = {
            "send_raw_message_fn": super(OtrBot, self).send_message,
            "default_policy": otr.OTRL_POLICY_MANUAL
            }
        self.__otr_callback_store = OtrCallbackStore()

66 67 68 69 70
    def __otr_callbacks(self, more_data = None):
        opdata = self.__opdata.copy()
        if more_data:
            opdata.update(more_data)
        return (self.__otr_callback_store, opdata)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

    def __get_otr_user_context(self, user):
        context, _ = otr.otrl_context_find(
            self.__otr_ustate, user, self.__account, self.__protocol, 1)
        return context

    # Wrap OTR encryption around Jabberbot's most low-level method for
    # sending messages.
    def send_message(self, mess):
        body = str(mess.getBody())
        user = str(mess.getTo().getStripped())
        encrypted_body = otr.otrl_message_sending(
            self.__otr_ustate, self.__otr_callbacks(), self.__account,
            self.__protocol, user, body, None)
        otr.otrl_message_fragment_and_send(
86 87 88
            self.__otr_callbacks({"message": mess}),
            self.__get_otr_user_context(user), encrypted_body,
            otr.OTRL_FRAGMENT_SEND_ALL)
89 90 91 92 93 94

    # Wrap OTR decryption around Jabberbot's callback mechanism.
    def callback_message(self, conn, mess):
        body = str(mess.getBody())
        user = str(mess.getFrom().getStripped())
        is_internal, decrypted_body, _ = otr.otrl_message_receiving(
95 96
            self.__otr_ustate, self.__otr_callbacks({"message": mess}),
            self.__account, self.__protocol, user, body)
97 98 99 100 101
        context = self.__get_otr_user_context(user)
        if context.msgstate == otr.OTRL_MSGSTATE_FINISHED:
            otr.otrl_context_force_plaintext(context)
        if is_internal:
            return
102 103 104 105 106 107
        if mess.getType() == "groupchat":
            bot_prefix = self.__account + ": "
            if decrypted_body.startswith(bot_prefix):
                decrypted_body = decrypted_body[len(bot_prefix):]
            else:
                return
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
        mess.setBody(decrypted_body)
        super(OtrBot, self).callback_message(conn, mess)

    # Override Jabberbot quitting on keep alive failure.
    def on_ping_timeout(self):
        self.__lastping = None

    @jabberbot.botcmd
    def ping(self, mess, args):
        """Why not just test it?"""
        return "pong"

    @jabberbot.botcmd
    def say(self, mess, args):
        """Unleash my inner parrot"""
        return args

    @jabberbot.botcmd
    def clear_say(self, mess, args):
Tails developers's avatar
Tails developers committed
127
        """Make me speak in the clear even if we're in an OTR chat"""
128 129 130 131 132 133
        self.__opdata["send_raw_message_fn"](mess.buildReply(args))
        return ""

    @jabberbot.botcmd
    def start_otr(self, mess, args):
        """Make me *initiate* (but not refresh) an OTR session"""
134 135
        if mess.getType() == "groupchat":
            return
136 137 138 139 140
        return "?OTRv2?"

    @jabberbot.botcmd
    def end_otr(self, mess, args):
        """Make me gracefully end the OTR session if there is one"""
141 142
        if mess.getType() == "groupchat":
            return
143 144
        user = str(mess.getFrom().getStripped())
        otr.otrl_message_disconnect(
145 146
            self.__otr_ustate, self.__otr_callbacks({"message": mess}),
            self.__account, self.__protocol, user)
147 148 149 150 151 152 153 154 155 156
        return ""

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print >> sys.stderr, \
            "Usage: %s <user@domain> <password> <otr_key_file>" % sys.argv[0]
        sys.exit(1)
    username, password, otr_key_path = sys.argv[1:]
    otr_bot = OtrBot(username, password, otr_key_path)
    otr_bot.serve_forever()