hybridbot.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #!/usr/bin/env python3
  2. import asyncio
  3. import re
  4. import signal
  5. import sys
  6. import time
  7. import slixmpp
  8. from configparser import ConfigParser
  9. from irc.bot import SingleServerIRCBot
  10. class IRCBot:
  11. def __init__(self, opts, inter):
  12. self.client = SingleServerIRCBot([(opts['server'], opts['port'])],
  13. opts['nick'], opts['realname'])
  14. self.conn = self.client.connection
  15. self.nick = opts['nick']
  16. self.pure_nick = self.nick
  17. self.chan = opts['chan']
  18. # Support customizing the authentication bot, the commands that
  19. # will be sent to it, the general messages' bytes and rate limits,
  20. # besides supplying fallback values for most of these cases. An
  21. # authentication command is required with no fallback since most
  22. # IRC networks deny bots without registry under their service
  23. # database.
  24. self.auth_bot_name = opts['auth_bot_name']
  25. self.auth_bot_privmsg = opts['auth_bot_privmsg']
  26. self.msg_part_max_bytes = opts['msg_part_max_bytes']
  27. self.msg_multipart_sleep_seconds = opts['msg_multipart_sleep_seconds']
  28. self.inter = inter
  29. self.loop = None
  30. def register_handlers(self):
  31. self.conn.add_global_handler('welcome', self.on_session_start)
  32. # Attempt to solve the problem noted on (on_session_start). Needs
  33. # testing.
  34. self.conn.add_global_handler('disconnect', self.on_session_start)
  35. self.conn.add_global_handler('pubmsg', self.on_message)
  36. self.conn.add_global_handler('action', self.on_message)
  37. self.conn.add_global_handler('join', self.on_presence)
  38. self.conn.add_global_handler('part', self.on_presence)
  39. self.conn.add_global_handler('namreply', self.on_namreply)
  40. self.conn.add_global_handler('kick', self.on_kick)
  41. self.conn.add_global_handler('nicknameinuse', self.on_nicknameinuse)
  42. def join_chan(self):
  43. self.conn.part(self.chan, message='Replaced by new connection')
  44. self.conn.join(self.chan)
  45. def on_session_start(self, conn, event):
  46. print('Connected to IRC')
  47. # FIXME: Reauthenticate and rejoin channels after connection
  48. # loss.
  49. self.conn.privmsg(
  50. self.auth_bot_name,
  51. self.auth_bot_privmsg
  52. )
  53. self.join_chan()
  54. def on_message(self, conn, event):
  55. nick = event.source.split('!')[0]
  56. body = ''.join(event.arguments)
  57. typ = event.type
  58. if typ == 'action':
  59. body = '/me ' + body
  60. self.call_outside(self.inter.relay_message, 'irc', nick, body)
  61. def on_presence(self, conn, event):
  62. try:
  63. typ = event.type
  64. nick = event.source.nick
  65. if typ == 'part':
  66. if nick != self.nick:
  67. if nick in self.inter.get_irc_users():
  68. self.call_outside(self.inter.remove_irc_user, nick)
  69. else:
  70. if nick != self.nick:
  71. if nick not in self.inter.get_irc_users():
  72. self.call_outside(self.inter.append_irc_user, nick)
  73. except Exception as e:
  74. print(str(e), file=sys.stderr)
  75. def on_namreply(self, conn, event):
  76. for nick in event.arguments[2].split():
  77. if nick != conn.get_nickname():
  78. self.call_outside(self.inter.append_irc_user, nick)
  79. def on_kick(self, conn, event):
  80. self.nick = self.pure_nick
  81. conn.nick(self.nick)
  82. time.sleep(0.5)
  83. self.join_chan()
  84. def on_nicknameinuse(self, conn, event):
  85. self.nick = self.nick + '_'
  86. conn.nick(self.nick)
  87. def send_message(self, msg, lead_prefix='', prefix=''):
  88. # IRC standard says that maximum message size is 512 bytes,
  89. # including: channel/nick prefix, command, arguments, and the
  90. # CR+LF line ending.
  91. # Furthermore, tests made with the Python module reveal that
  92. # 460 is a good maximum size, since that module may be adding
  93. # other information before sending. With this in mind, the
  94. # possible message parts mentioned here are joined by spaces,
  95. # measured and subtracted from the starting limit, thus giving
  96. # the maximum buffer size of the message, stored in buf.
  97. # self.nick and self.chan are both used as prefixes since it's
  98. # unknown which, or if both, will be used.
  99. buf = min(460, self.msg_part_max_bytes) - len(' '.join(['PRIVMSG', ':' + self.nick, ':' + self.chan])) - len(b'\r\n')
  100. result = []
  101. try:
  102. for line in msg:
  103. # Since the bot itself may add lead_prefix or prefix,
  104. # always deduce the greatest from buf when splitting
  105. # lines, both when computing step for range function,
  106. # and when reading parts of line.
  107. for i in range(0, len(line), buf - max(len(prefix), len(lead_prefix))):
  108. result.append(line[i:i + buf - max(len(prefix), len(lead_prefix))])
  109. self.conn.privmsg(self.chan, lead_prefix + result[0])
  110. for r in result[1:]:
  111. time.sleep(max(0.5, self.msg_multipart_sleep_seconds))
  112. self.conn.privmsg(self.chan, prefix + r)
  113. except Exception as e:
  114. print(str(e), file=sys.stderr)
  115. def call_outside(self, func, *args):
  116. assert self.loop
  117. self.loop.call_soon_threadsafe(func, *args)
  118. async def run(self):
  119. self.loop = asyncio.get_event_loop()
  120. self.register_handlers()
  121. await self.loop.run_in_executor(None, self.client.start)
  122. class XMPPBot:
  123. def __init__(self, opts, inter, timeout=5.0):
  124. self.client = slixmpp.ClientXMPP(opts['jid'], opts['passwd'])
  125. self.client.register_plugin('xep_0199') # XMPP Ping.
  126. self.client_ping = self.client.plugin['xep_0199']
  127. self.client.register_plugin('xep_0045') # XMPP MUC.
  128. self.client_muc = self.client.plugin['xep_0045']
  129. self.muc_is_joined = False
  130. self.client_ping.timeout = self.timeout = timeout
  131. self.client_ping.keepalive = True
  132. self.nick = opts['nick']
  133. self.pure_nick = self.nick
  134. self.muc = opts['muc']
  135. self.inter = inter
  136. def register_handlers(self):
  137. self.client.add_event_handler('failed_all_auth',
  138. self.on_failed_all_auth)
  139. self.client.add_event_handler('session_start', self.on_session_start)
  140. self.client.add_event_handler('got_online', self.on_got_online)
  141. self.client.add_event_handler('disconnected', self.on_disconnected)
  142. self.client.add_event_handler('reconnect_delay', self.on_disconnected)
  143. self.client.add_event_handler('groupchat_message', self.on_message)
  144. self.client.add_event_handler('muc::%s::presence' % self.muc,
  145. self.on_presence)
  146. def connect(self):
  147. self.client.connect()
  148. def join_muc(self):
  149. async def loop_cycle():
  150. if self._join_muc_block > 0:
  151. return
  152. self._join_muc_block += 1
  153. while not self.muc_is_joined:
  154. self.client_muc.join_muc(self.muc, self.nick)
  155. await asyncio.sleep(self.timeout)
  156. self._join_muc_block -= 1
  157. asyncio.ensure_future(loop_cycle())
  158. _join_muc_block = 0
  159. def on_failed_all_auth(event):
  160. # print('could not connect!', file=sys.stderr)
  161. print('Could not connect to the server, or password mismatch!',
  162. file=sys.stderr)
  163. sys.exit(1)
  164. def on_session_start(self, event):
  165. # print('connected with %s' %con, file=sys.stderr)
  166. print('Connected to XMPP')
  167. self.client.get_roster()
  168. self.client.send_presence()
  169. def on_got_online(self, event):
  170. self.join_muc()
  171. async def on_disconnected(self, event):
  172. self.muc_is_joined = False
  173. print('Connection lost, reattempting in %d seconds' % self.timeout)
  174. await asyncio.sleep(self.timeout)
  175. self.connect()
  176. def on_message(self, event):
  177. body = event['body']
  178. nick = event['mucnick']
  179. self.inter.relay_message('xmpp', nick, body)
  180. async def on_presence(self, event):
  181. try:
  182. muc_plugin = self.client.plugin['xep_0045']
  183. typ = event['muc']['type']
  184. nick = event['muc']['nick']
  185. if not typ:
  186. typ = event['type']
  187. if not nick:
  188. nick = muc_plugin.get_nick(self.muc, event['from'])
  189. if typ == 'available':
  190. self.muc_is_joined = True
  191. if nick != self.nick:
  192. if nick not in self.inter.get_xmpp_users():
  193. self.inter.append_xmpp_user(nick)
  194. elif typ == 'error':
  195. self.muc_is_joined = False
  196. if event['error']['code'] == '409':
  197. self.nick += '_'
  198. self.join_muc()
  199. elif typ == 'unavailable':
  200. if nick != self.nick:
  201. if nick in self.inter.get_xmpp_users():
  202. self.inter.remove_xmpp_user(nick)
  203. else:
  204. self.muc_is_joined = False
  205. self.nick = self.pure_nick
  206. await asyncio.sleep(0.5)
  207. self.join_muc()
  208. else:
  209. if nick != self.nick:
  210. if nick not in self.inter.get_xmpp_users():
  211. self.inter.append_xmpp_user(nick)
  212. except Exception as e:
  213. print(str(e), file=sys.stderr)
  214. def send_message(self, msg, prefix=''):
  215. try:
  216. msg[0] = prefix + msg[0]
  217. result = '\n'.join(msg)
  218. self.client.send_message(mto=self.muc, mbody=result,
  219. mtype='groupchat')
  220. except Exception as e:
  221. print(str(e), file=sys.stderr)
  222. async def run(self):
  223. self.register_handlers()
  224. self.connect()
  225. class Intermedia:
  226. def __init__(self, shared_opts, irc_chan, xmpp_muc):
  227. self.irc_chan = irc_chan
  228. self.xmpp_muc = xmpp_muc
  229. self.ircbot = None
  230. self.xmppbot = None
  231. self.irc_users = []
  232. self.xmpp_users = []
  233. self.prefix = shared_opts['prefix']
  234. self.owner = shared_opts['owner']
  235. def set_bots(self, ircbot, xmppbot):
  236. self.ircbot = ircbot
  237. self.xmppbot = xmppbot
  238. def to_irc(self, msg, lead_prefix='', prefix=''):
  239. if self.ircbot:
  240. self.ircbot.send_message(msg, lead_prefix, prefix)
  241. def to_xmpp(self, msg, prefix=''):
  242. if self.xmppbot:
  243. self.xmppbot.send_message(msg, prefix)
  244. def relay_message(self, from_net, nick, body):
  245. if not self.ircbot or not self.xmppbot:
  246. return
  247. if from_net != 'irc' and from_net != 'xmpp':
  248. return
  249. if from_net == 'irc' and nick == self.ircbot.nick or \
  250. from_net == 'xmpp' and nick == self.xmppbot.nick:
  251. return
  252. if not body or len(body) <= 0:
  253. return
  254. try:
  255. msg = body.replace('\r\n', '\n').replace('\r', '\n').split('\n')
  256. if msg and len(msg) > 0:
  257. if len(msg) == 1 and msg[0] == self.prefix + 'users':
  258. irc_users = ', '.join(self.get_irc_users())
  259. xmpp_users = ', '.join(self.get_xmpp_users())
  260. if irc_users:
  261. irc_users = '[ IRC Users ] ' + irc_users
  262. if xmpp_users:
  263. xmpp_users = '[ XMPP Users ] ' + xmpp_users
  264. if from_net == 'irc':
  265. for answer in [xmpp_users]:
  266. self.to_irc([answer])
  267. elif from_net == 'xmpp':
  268. for answer in [irc_users]:
  269. self.to_xmpp([answer])
  270. elif len(msg) == 1 and msg[0] == self.prefix + 'help':
  271. answer = 'The only command I have is \'' + self.prefix + \
  272. 'users\'. Also, my owner is ' + self.owner + '.'
  273. if from_net == 'irc':
  274. self.to_irc([answer])
  275. elif from_net == 'xmpp':
  276. self.to_xmpp([answer])
  277. else:
  278. nick_prefix = '[' + nick + '] '
  279. nick_prefix_me = '***' + nick + ' '
  280. if (not re.match('^/me .+$', msg[0])):
  281. nick_prefix_lead = nick_prefix
  282. else:
  283. msg[0] = re.split('^/me ', msg[0])[1]
  284. nick_prefix_lead = nick_prefix_me
  285. if from_net == 'irc':
  286. self.to_xmpp(msg, prefix=nick_prefix_lead)
  287. elif from_net == 'xmpp':
  288. self.to_irc(msg,
  289. lead_prefix=nick_prefix_lead,
  290. prefix=nick_prefix)
  291. except Exception as e:
  292. print(str(e), file=sys.stderr)
  293. def get_irc_users(self):
  294. return self.irc_users
  295. def append_irc_user(self, user):
  296. self.irc_users.append(user)
  297. def remove_irc_user(self, user):
  298. self.irc_users.remove(user)
  299. def get_xmpp_users(self):
  300. return self.xmpp_users
  301. def append_xmpp_user(self, user):
  302. self.xmpp_users.append(user)
  303. def remove_xmpp_user(self, user):
  304. self.xmpp_users.remove(user)
  305. if __name__ == '__main__':
  306. config = ConfigParser()
  307. shared_opts = {}
  308. xmpp_opts = {}
  309. irc_opts = {}
  310. if len(sys.argv) > 1:
  311. config.read(sys.argv[1])
  312. else:
  313. config.read('config.ini')
  314. if not config.sections():
  315. print('Error: Configuration file does not exist or is empty.',
  316. file=sys.stderr)
  317. sys.exit(1)
  318. shared_opts['prefix'] = config.get('Shared', 'prefix')
  319. shared_opts['owner'] = config.get('Shared', 'owner')
  320. irc_opts['chan'] = config.get('IRC', 'channel')
  321. irc_opts['nick'] = config.get('IRC', 'nick')
  322. # Support realname, a short string that can have spaces and describes
  323. # the bot to others on channel or when WHOIS/WHOWAS is used.
  324. irc_opts['realname'] = config.get('IRC', 'realname', fallback = irc_opts['nick'])
  325. irc_opts['server'] = config.get('IRC', 'server')
  326. irc_opts['port'] = int(config.get('IRC', 'port'))
  327. # Support customizing the authentication bot, the commands that will
  328. # be sent to it, the general messages' bytes and rate limits,
  329. # besides supplying fallback values for most of these cases. An
  330. # authentication command is required with no fallback since most IRC
  331. # networks deny bots without registry under their service database.
  332. irc_opts['auth_bot_name'] = config.get('IRC', 'auth_bot_name', fallback = 'NickServ')
  333. irc_opts['auth_bot_privmsg'] = config.get('IRC', 'auth_bot_privmsg')
  334. irc_opts['msg_part_max_bytes'] = int(config.get('IRC', 'msg_part_max_bytes', fallback = 460))
  335. irc_opts['msg_multipart_sleep_seconds'] = float(config.get('IRC', 'msg_multipart_sleep_seconds', fallback = 0.5))
  336. xmpp_opts['jid'] = config.get('XMPP', 'jid')
  337. xmpp_opts['passwd'] = config.get('XMPP', 'password')
  338. xmpp_opts['muc'] = config.get('XMPP', 'muc')
  339. xmpp_opts['nick'] = config.get('XMPP', 'nick')
  340. signal.signal(signal.SIGINT, signal.SIG_DFL)
  341. loop = asyncio.get_event_loop()
  342. inter = Intermedia(shared_opts, irc_opts['chan'], xmpp_opts['muc'])
  343. ircbot = IRCBot(irc_opts, inter)
  344. xmppbot = XMPPBot(xmpp_opts, inter)
  345. inter.set_bots(ircbot, xmppbot)
  346. asyncio.ensure_future(xmppbot.run())
  347. asyncio.ensure_future(ircbot.run())
  348. loop.run_forever()