otr-bot.py 5.77 KB
Newer Older
1 2 3 4 5
#!/usr/bin/python
import sys
import jabberbot
import xmpp
import otr
Tails developers's avatar
Tails developers committed
6
from argparse import ArgumentParser
7 8 9 10 11 12

# 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):
13 14 15
        mess = opdata["message"]
        mess.setTo(recipient)
        mess.setBody(message)
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
        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()

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

    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(
87 88 89
            self.__otr_callbacks({"message": mess}),
            self.__get_otr_user_context(user), encrypted_body,
            otr.OTRL_FRAGMENT_SEND_ALL)
90 91 92 93 94 95

    # 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(
96 97
            self.__otr_ustate, self.__otr_callbacks({"message": mess}),
            self.__account, self.__protocol, user, body)
98 99 100 101 102
        context = self.__get_otr_user_context(user)
        if context.msgstate == otr.OTRL_MSGSTATE_FINISHED:
            otr.otrl_context_force_plaintext(context)
        if is_internal:
            return
103 104 105 106 107 108
        if mess.getType() == "groupchat":
            bot_prefix = self.__account + ": "
            if decrypted_body.startswith(bot_prefix):
                decrypted_body = decrypted_body[len(bot_prefix):]
            else:
                return
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
        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
128
        """Make me speak in the clear even if we're in an OTR chat"""
129 130 131 132 133 134
        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"""
135 136
        if mess.getType() == "groupchat":
            return
137 138 139 140 141
        return "?OTRv2?"

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

if __name__ == '__main__':
Tails developers's avatar
Tails developers committed
151 152 153 154 155 156 157 158 159 160 161 162 163 164
    parser = ArgumentParser()
    parser.add_argument("account",
                        help = "the user account, given as user@domain")
    parser.add_argument("password",
                        help = "the user account's password")
    parser.add_argument("otr_key_path",
                        help = "the path to the account's OTR key file")
    parser.add_argument("-j", "--auto-join", nargs = '+', metavar = 'ROOMS',
                        help = "auto-join multi-user chatrooms on start")
    args = parser.parse_args()
    otr_bot = OtrBot(args.account, args.password, args.otr_key_path)
    if args.auto_join:
        for room in args.auto_join:
            otr_bot.join_room(room)
165
    otr_bot.serve_forever()