otr-bot.py 4.76 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
#!/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):
        mess = xmpp.protocol.Message(to = recipient, body = message)
        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()

    def __otr_callbacks(self):
        return (self.__otr_callback_store, self.__opdata)

    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(
            self.__otr_callbacks(), self.__get_otr_user_context(user),
            encrypted_body, otr.OTRL_FRAGMENT_SEND_ALL)

    # 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(
            self.__otr_ustate, self.__otr_callbacks(), self.__account,
            self.__protocol, user, body)
        context = self.__get_otr_user_context(user)
        if context.msgstate == otr.OTRL_MSGSTATE_FINISHED:
            otr.otrl_context_force_plaintext(context)
        if is_internal:
            return
        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
115
        """Make me speak in the clear even if we're in an OTR chat"""
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
        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"""
        return "?OTRv2?"

    @jabberbot.botcmd
    def end_otr(self, mess, args):
        """Make me gracefully end the OTR session if there is one"""
        user = str(mess.getFrom().getStripped())
        otr.otrl_message_disconnect(
            self.__otr_ustate, self.__otr_callbacks(), self.__account,
            self.__protocol, user)
        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()