class IRCLogger(irclib.SimpleIRCClient): def __init__(self, server, port, channels, nick, db, loop=None): irclib.SimpleIRCClient.__init__(self) # tornado ioloop if loop is None: loop = ioloop.IOLoop.instance() self.loop = loop self._last_connected = time.time() self._reconnect_interval = 30 #IRC details self.server = server self.port = port self.channels = channels self.names = {} for channel in self.channels: self.names[channel] = set() self.nick = nick #DB details self.db = IRCDatabase( db ) #Regexes self.nick_reg = re.compile("^" + nick + "[:,](?iu)") #Message Cache self.message_cache = [] #messages are stored here before getting pushed to the db #Disconnect Countdown self.disconnect_countdown = 5 self._connect() self.last_ping = time.time() self.ping_callback = pc = ioloop.PeriodicCallback(self._no_ping, 60000, self.loop) pc.start() # failure = ioloop.PeriodicCallback(self._FAKE_DISCONNECT, 25000, self.loop) # failure.start() def _FAKE_DISCONNECT(self): """fake disconnect callback for debugging""" logging.critical("DISCONNECTING MYSELF") if self.connection.is_connected(): self.connection.disconnect() def _check_connect(self): """Connect to a new server, possibly disconnecting from the current. The bot will skip to next server in the server_list each time jump_server is called. """ raise RuntimeError("_check_connect should not be used!") logging.debug("checking connection") if self.connection.is_connected(): return def _connect(self): logging.info("IRC:connecting to %s:%i %s as %s...", self.server, self.port, self.channels, self.nick) irclib.SimpleIRCClient.connect(self, self.server, self.port, self.nick) self._connection_fd = self.connection.socket.fileno() self.loop.add_handler(self._connection_fd, self._handle_message, self.loop.READ) self._last_connect = time.time() def _handle_message(self, fd, events): logging.debug("dispatching message") self.connection.process_data() def _no_ping(self): elapsed = time.time() - self.last_ping logging.debug("last ping: %is ago", elapsed) if elapsed >= 1200: logging.critical("No ping in %is: reconnecting", elapsed) self.loop.stop() def _dispatcher(self, c, e): """dispatch events""" etype = e.eventtype() logging.debug(u"dispatch: %s: %r : %r : %r", etype, e.target(), e.source(), e.arguments()) if etype in ('topic', 'part', 'join', 'action', 'quit', 'nick', 'pubmsg'): try: source = cast_unicode(e.source().split("!")[0]) except IndexError: source = u'' try: text = cast_unicode(e.arguments()[0]) except IndexError: text = u'' # update names lists if etype == 'join': channel = e.target() logging.info("%s joined %s", source, channel) self.names[channel].add(source) elif etype == 'part': channel = e.target() logging.info("%s parted %s", source, channel) self.names[channel].remove(source) # Prepare a message for the buffer message_dict = {"channel": e.target() or u'', "name": source, "message": text, "type": etype, "time": datetime.datetime.utcnow() } if etype == "nick": message_dict["message"] = e.target() before = source after = e.target() found = [] for channel, nameset in self.names.items(): if before in nameset: nameset.remove(before) nameset.add(after) logging.info("%s renamed to %s in %s", before, after, channel) md = dict(message_dict) md['channel'] = channel self.message_cache.append( md ) elif etype == "quit": name = source logging.info("%s quit", name) for channel, nameset in self.names.items(): if name in nameset: nameset.remove(name) md = dict(message_dict) md['channel'] = channel self.message_cache.append( md ) else: # Most of the events are pushed to the buffer. self.message_cache.append( message_dict ) m = "on_" + etype if hasattr(self, m): getattr(self, m)(c, e) def on_nicknameinuse(self, c, e): logging.error("nick in use: %s", c.get_nickname()) c.nick(c.get_nickname() + "_") def on_welcome(self, connection, event): logging.info("welcome") for channel in self.channels: connection.join(channel) def on_disconnect(self, connection, event): logging.warn("disconnect") self.on_ping(connection, event) self.loop.remove_handler(self._connection_fd) logging.warn("connection lost, triggering reconnect") if (time.time() - self._last_connect) < 600: # short-lived connection, throttle reconnect interval = self._reconnect_interval if interval > 600: # tried slowing down to 10 minutes, still failed: logging.critical("reconnecting doesn't seem to be working, giving up") self.loop.stop() return else: # exponentially throttle reconnects self._reconnect_interval = interval * 2 logging.error("last reconnect seems to have failed, next try in %is", interval) else: # last connect seemed to go fine, start with 30s check interval interval = self._reconnect_interval = interval * 2 logging.info("last connect was okay, back to %is check", interval) self.loop.add_timeout(time.time() + interval, self._connect) def on_namreply(self, connection, event): owner = cast_unicode(event.arguments()[0]) channel = cast_unicode(event.arguments()[1]) names = cast_unicode(event.arguments()[-1]).split() logging.info("got names for %s: %s", channel, names) nameset = self.names[channel] for name in names: nameset.add(name.lstrip(owner)) def on_ping(self, connection, event): logging.info("ping") logging.debug("Current channel members: %s", self.names) self.last_ping = time.time() self.save_messages() def save_messages(self): if not self.message_cache: logging.debug("no messages to save") return else: logging.info("saving %i messages" % len(self.message_cache)) try: for message in self.message_cache: self.db.insert_line(message["channel"], message["name"], message["time"], message["message"], message["type"] ) self.db.commit() if self.disconnect_countdown < 5: self.disconnect_countdown = self.disconnect_countdown + 1 # clear the cache self.message_cache = [] except Exception: logging.error("Couldn't commit to db: %s", self.db.fname, exc_info=True) if self.disconnect_countdown <= 0: self.loop.stop() # connection.privmsg(self.channel, "Database connection lost! " + str(self.disconnect_countdown) + " retries until I give up entirely!" ) self.disconnect_countdown = self.disconnect_countdown - 1 def on_pubmsg(self, connection, event): try: text = cast_unicode(event.arguments()[0]) except IndexError: text = u'' channel = cast_unicode(event.target() or '') logging.info("pubmsg: %s", text) # If you talk to the bot, this is how he responds. if self.nick_reg.search(text): logging.debug("bloop") if text.split(" ")[1] and text.split(" ")[1] == "quit": connection.privmsg(channel, "Goodbye.") self.on_ping( connection, event ) sys.exit( 0 ) if text.split(" ")[1] and text.split(" ")[1] == "ping": connection.privmsg(channel, "Pong.") self.on_ping(connection, event) return def start_saving(self, interval=5000): pc = ioloop.PeriodicCallback(self.save_messages, interval, self.loop) pc.start() def start(self): self.start_saving() self.loop.start()