otr-bot.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/python
  2. import sys
  3. import jabberbot
  4. import xmpp
  5. import potr
  6. import logging
  7. from argparse import ArgumentParser
  8. class OtrContext(potr.context.Context):
  9. def __init__(self, account, peer):
  10. super(OtrContext, self).__init__(account, peer)
  11. def getPolicy(self, key):
  12. return True
  13. def inject(self, msg, appdata = None):
  14. mess = appdata["base_reply"]
  15. mess.setBody(msg)
  16. appdata["send_raw_message_fn"](mess)
  17. class BotAccount(potr.context.Account):
  18. def __init__(self, jid, keyFilePath):
  19. protocol = 'xmpp'
  20. max_message_size = 10*1024
  21. super(BotAccount, self).__init__(jid, protocol, max_message_size)
  22. self.keyFilePath = keyFilePath
  23. def loadPrivkey(self):
  24. with open(self.keyFilePath, 'rb') as keyFile:
  25. return potr.crypt.PK.parsePrivateKey(keyFile.read())[0]
  26. class OtrContextManager:
  27. def __init__(self, jid, keyFilePath):
  28. self.account = BotAccount(jid, keyFilePath)
  29. self.contexts = {}
  30. def start_context(self, other):
  31. if not other in self.contexts:
  32. self.contexts[other] = OtrContext(self.account, other)
  33. return self.contexts[other]
  34. def get_context_for_user(self, other):
  35. return self.start_context(other)
  36. class OtrBot(jabberbot.JabberBot):
  37. PING_FREQUENCY = 60
  38. def __init__(self, account, password, otr_key_path,
  39. connect_server = None, log_file = None):
  40. self.__connect_server = connect_server
  41. self.__password = password
  42. self.__log_file = log_file
  43. super(OtrBot, self).__init__(account, password)
  44. self.__otr_manager = OtrContextManager(account, otr_key_path)
  45. self.send_raw_message_fn = super(OtrBot, self).send_message
  46. self.__default_otr_appdata = {
  47. "send_raw_message_fn": self.send_raw_message_fn
  48. }
  49. def __otr_appdata_for_mess(self, mess):
  50. appdata = self.__default_otr_appdata.copy()
  51. appdata["base_reply"] = mess
  52. return appdata
  53. # Unfortunately Jabberbot's connect() is not very friendly to
  54. # overriding in subclasses so we have to re-implement it
  55. # completely (copy-paste mostly) in order to add support for using
  56. # an XMPP "Connect Server".
  57. def connect(self):
  58. logging.basicConfig(filename = self.__log_file,
  59. level = logging.DEBUG)
  60. if not self.conn:
  61. conn = xmpp.Client(self.jid.getDomain(), debug=[])
  62. if self.__connect_server:
  63. try:
  64. conn_server, conn_port = self.__connect_server.split(":", 1)
  65. except ValueError:
  66. conn_server = self.__connect_server
  67. conn_port = 5222
  68. conres = conn.connect((conn_server, int(conn_port)))
  69. else:
  70. conres = conn.connect()
  71. if not conres:
  72. return None
  73. authres = conn.auth(self.jid.getNode(), self.__password, self.res)
  74. if not authres:
  75. return None
  76. self.conn = conn
  77. self.conn.sendInitPresence()
  78. self.roster = self.conn.Roster.getRoster()
  79. for (handler, callback) in self.handlers:
  80. self.conn.RegisterHandler(handler, callback)
  81. return self.conn
  82. # Wrap OTR encryption around Jabberbot's most low-level method for
  83. # sending messages.
  84. def send_message(self, mess):
  85. body = mess.getBody().encode('utf-8')
  86. user = mess.getTo().getStripped().encode('utf-8')
  87. otrctx = self.__otr_manager.get_context_for_user(user)
  88. if otrctx.state == potr.context.STATE_ENCRYPTED:
  89. otrctx.sendMessage(potr.context.FRAGMENT_SEND_ALL, body,
  90. appdata = self.__otr_appdata_for_mess(mess))
  91. else:
  92. self.send_raw_message_fn(mess)
  93. # Wrap OTR decryption around Jabberbot's callback mechanism.
  94. def callback_message(self, conn, mess):
  95. body = mess.getBody().encode('utf-8')
  96. user = mess.getFrom().getStripped().encode('utf-8')
  97. otrctx = self.__otr_manager.get_context_for_user(user)
  98. if mess.getType() == "chat":
  99. try:
  100. appdata = self.__otr_appdata_for_mess(mess.buildReply())
  101. decrypted_body, tlvs = otrctx.receiveMessage(body,
  102. appdata = appdata)
  103. otrctx.processTLVs(tlvs)
  104. except potr.context.NotEncryptedError:
  105. otrctx.authStartV2(appdata = appdata)
  106. return
  107. except (potr.context.UnencryptedMessage, potr.context.NotOTRMessage):
  108. decrypted_body = body
  109. else:
  110. decrypted_body = body
  111. if decrypted_body == None:
  112. return
  113. if mess.getType() == "groupchat":
  114. bot_prefix = self.jid.getNode() + ": "
  115. if decrypted_body.startswith(bot_prefix):
  116. decrypted_body = decrypted_body[len(bot_prefix):]
  117. else:
  118. return
  119. mess.setBody(decrypted_body)
  120. super(OtrBot, self).callback_message(conn, mess)
  121. # Override Jabberbot quitting on keep alive failure.
  122. def on_ping_timeout(self):
  123. self.__lastping = None
  124. @jabberbot.botcmd
  125. def ping(self, mess, args):
  126. """Why not just test it?"""
  127. return "pong"
  128. @jabberbot.botcmd
  129. def say(self, mess, args):
  130. """Unleash my inner parrot"""
  131. return args
  132. @jabberbot.botcmd
  133. def clear_say(self, mess, args):
  134. """Make me speak in the clear even if we're in an OTR chat"""
  135. self.send_raw_message_fn(mess.buildReply(args))
  136. return ""
  137. @jabberbot.botcmd
  138. def start_otr(self, mess, args):
  139. """Make me *initiate* (but not refresh) an OTR session"""
  140. if mess.getType() == "groupchat":
  141. return
  142. return "?OTRv2?"
  143. @jabberbot.botcmd
  144. def end_otr(self, mess, args):
  145. """Make me gracefully end the OTR session if there is one"""
  146. if mess.getType() == "groupchat":
  147. return
  148. user = mess.getFrom().getStripped().encode('utf-8')
  149. self.__otr_manager.get_context_for_user(user).disconnect(appdata =
  150. self.__otr_appdata_for_mess(mess.buildReply()))
  151. return ""
  152. if __name__ == '__main__':
  153. parser = ArgumentParser()
  154. parser.add_argument("account",
  155. help = "the user account, given as user@domain")
  156. parser.add_argument("password",
  157. help = "the user account's password")
  158. parser.add_argument("otr_key_path",
  159. help = "the path to the account's OTR key file")
  160. parser.add_argument("-c", "--connect-server", metavar = 'ADDRESS',
  161. help = "use a Connect Server, given as host[:port] " +
  162. "(port defaults to 5222)")
  163. parser.add_argument("-j", "--auto-join", nargs = '+', metavar = 'ROOMS',
  164. help = "auto-join multi-user chatrooms on start")
  165. parser.add_argument("-l", "--log-file", metavar = 'LOGFILE',
  166. help = "Log to file instead of stderr")
  167. args = parser.parse_args()
  168. otr_bot_opt_args = dict()
  169. if args.connect_server:
  170. otr_bot_opt_args["connect_server"] = args.connect_server
  171. if args.log_file:
  172. otr_bot_opt_args["log_file"] = args.log_file
  173. otr_bot = OtrBot(args.account, args.password, args.otr_key_path,
  174. **otr_bot_opt_args)
  175. if args.auto_join:
  176. for room in args.auto_join:
  177. otr_bot.join_room(room)
  178. otr_bot.serve_forever()