class Logger(Callback): formatters = { "NICK": nickfmt, "QUIT": quitfmt, "PART": partfmt, "NOTICE": noticefmt, "PRIVMSG": msgfmt, "JOIN": joinfmt } def __init__(self, server): self.lower = server.lower self.logpath = server.get_config_dir("log.txt") self.dbpath = server.get_config_dir("log.db") self.sedchans = set() self.db = Database("sqlite:///" + self.dbpath, cache_limit=None) self.db.create_all(Base.metadata) if os.path.exists(self.logpath): # Perform migration self.sql_migrate(lower=server.lower) os.rename(self.logpath, self.logpath + ".old") # Initialise db and shit super().__init__(server) def cache_event(self, session, event): if event.sender_nick is None: return cached_event = { 'timestamp': event.timestamp, 'nick': event.sender_nick, 'context': event.context, 'data': event.data } if event.type == 'PRIVMSG' and event.context.startswith("#"): query = session.query(LastSpokeCache).filter( LastSpokeCache.nick == event.sender_nick, LastSpokeCache.context == event.context) if query.count(): query.update(cached_event) else: session.add(LastSpokeCache(**cached_event)) if event.type == 'NICK': cached_event['newnick'] = event.payload_lower[1:] else: cached_event['newnick'] = None if event.type in HAS_SENDER: query = session.query(LastEventCache).filter( LastEventCache.nick == event.sender_nick, LastEventCache.context == event.context) if query.count(): query.update(cached_event) else: session.add(LastEventCache(**cached_event)) def sql_migrate(self, logpath=None, lower=str.lower): """ Migrate existing logs to the new SQL database """ if logpath is None: logpath = self.logpath with open(logpath) as logfile: while True: with self.db() as session: finished = True for line in islice(logfile, None, 100): finished = False try: line = line.rstrip("\n").rstrip("\r") timestamp, text = line.split(" ", 1) event = make_event( text, timestamp=datetime.utcfromtimestamp( float(timestamp))) session.add(event) self.cache_event(session, event) except: print("[Logger] Warning: Could not parse %s" % line) raise if finished: break @Callback.background @command("log_migrate", "(.+)", admin=True) def partial_migration(self, server, message, path): yield "Migration started..." self.sql_migrate(logpath=path, lower=server.lower) yield "Migration complete." def traceuser(self, hostmask, timestamp): # Load the logs into memory to compute graph # FIXME: Paginate log loading start_nick, x = hostmask.split("!", 1) start_ident, start_mask = x.split("@", 1) userinf = {(start_nick, start_ident, start_mask)} with self.db() as session: logs = session.query(Event).filter( Event.type.in_(['NICK', 'JOIN']), Event.timestamp >= timestamp, ).order_by(Event.timestamp).all() for log in logs: nick, x = log.sender.split("!", 1) ident, mask = x.split("@", 1) if log.type == "NICK" and (nick, ident, mask) in userinf: userinf.add((log.payload[1:], ident, mask)) elif log.type == "JOIN" and any( log.sender_nick == self.lower(n) or (ident, mask) == (i, m) for n, i, m in userinf): userinf.add((nick, ident, mask)) return userinf @Callback.inline def log(self, server, line) -> "ALL": timestamp = datetime.utcnow() event = make_event(line, timestamp=timestamp) self.db.add(event) def cache_update(self): with self.db() as session: last_entry = session.query(func.max( LastEventCache.timestamp)).first()[0] or 0 events = session.query(Event).filter( Event.timestamp > last_entry, Event.type.in_(HAS_SENDER)).all() for event in events: self.cache_event(session, event) @Callback.background def flush(self, server, line) -> "ALL": if len(self.db.cache) > 32: self.db.flush() self.cache_update() @command("seen lastseen", r"(\S+)") def seen(self, server, msg, user): if server.eq(user, msg.address.nick): return "04⎟ You're right there!" context = server.lower(msg.context) nick = server.lower(user) # Don't allow seen for pms, for confidentiality if not context.startswith("#"): return self.cache_update() with self.db() as session: last = session.query(LastEventCache).filter( (LastEventCache.context == context) | (LastEventCache.context == None), (LastEventCache.nick == nick) | (LastEventCache.newnick == nick)).order_by( LastEventCache.timestamp.desc()).first() if last is None: return "04⎟ I haven't seen %s yet." % user event = make_event(last.data, timestamp=last.timestamp, key=server.lower) message = self.formatters[event.type](event) timestamp = last.timestamp host = event.sender if server.isIn(user, server.channels.get(context)): status = " · \x0312online now" else: for nick, _, _ in self.traceuser(host, timestamp): if server.isIn(nick, server.channels.get(context)): status = " · \x0312online as %s" % nick break else: status = "" return "%s · \x1d%s%s" % (message, timefmt(timestamp), status) @command("last lastspoke lastmsg", r"(\S+)") def lastspoke(self, server, msg, user): if server.eq(user, msg.address.nick): return "04⎟ You just spoke!" context = server.lower(msg.context) nick = server.lower(user) # Don't allow seen for pms, for confidentiality if not context.startswith("#"): return self.cache_update() with self.db() as session: last = session.query(LastSpokeCache).filter( LastSpokeCache.context == context, LastSpokeCache.nick == nick).first() if last is None: return "04⎟ I haven't seen %s speak yet." % user event = make_event(last.data, timestamp=last.timestamp) return "%s · \x1d%s" % (msgfmt(event), timefmt(last.timestamp)) @command("sedon", rank="@") def sedon(self, server, msg): self.sedchans.add(server.lower(msg.context)) return "04⎟ Turned on sed." @command("sedoff", rank="@") def sedoff(self, server, msg): self.sedchans.remove(server.lower(msg.context)) return "04⎟ Turned off sed." # @msghandler def substitute(self, server, msg): raise NotImplementedError if not server.isIn(msg.context, self.sedchans): return match = re.match(r"^(\S+:\s+)?s(\W)(.*?)\2(.*?)(\2g?)?$", msg.text) if match: target, sep, pattern, sub, flags = match.groups() if target is not None: target = target.rstrip(": ") if flags is not None: flags = set(flags[1:]) pattern = re.escape(pattern) for timestamp, line in reversed(self.logs): # TODO: implement real regular expressions # also self-regex try: evt = IRCEvent(line) if (evt.type == "PRIVMSG" and server.eq(msg.context, evt.args[0]) and (target is None or server.eq(target, evt.sender.nick)) and not re.match( r"^(\S+:\s+)?s(\W)(.*?)\2(.*?)(\2g?)?$", evt.args[1]) and re.search( pattern, evt.args[1], flags=re.IGNORECASE)): evt.args[1] = re.sub(pattern, "\x1f%s\x1f" % sub, evt.args[1], count=0 if 'g' in flags else 1, flags=re.IGNORECASE) return msgfmt(evt) except: print("[Logger] Warning: Could not parse %s" % line) return "04⎟ No matches found." def __destroy__(self, *_): self.db.flush()
class Logger(Callback): formatters = {"NICK": nickfmt, "QUIT": quitfmt, "PART": partfmt, "NOTICE": noticefmt, "PRIVMSG": msgfmt, "JOIN": joinfmt} def __init__(self, server): self.lower = server.lower self.logpath = server.get_config_dir("log.txt") self.dbpath = server.get_config_dir("log.db") self.sedchans = set() self.db = Database("sqlite:///" + self.dbpath, cache_limit=None) self.db.create_all(Base.metadata) if os.path.exists(self.logpath): # Perform migration self.sql_migrate(lower=server.lower) os.rename(self.logpath, self.logpath + ".old") # Initialise db and shit super().__init__(server) def cache_event(self, session, event): if event.sender_nick is None: return cached_event = { 'timestamp': event.timestamp, 'nick': event.sender_nick, 'context': event.context, 'data': event.data } if event.type == 'PRIVMSG' and event.context.startswith("#"): query = session.query(LastSpokeCache).filter( LastSpokeCache.nick == event.sender_nick, LastSpokeCache.context == event.context ) if query.count(): query.update(cached_event) else: session.add(LastSpokeCache(**cached_event)) if event.type == 'NICK': cached_event['newnick'] = event.payload_lower[1:] else: cached_event['newnick'] = None if event.type in HAS_SENDER: query = session.query(LastEventCache).filter( LastEventCache.nick == event.sender_nick, LastEventCache.context == event.context ) if query.count(): query.update(cached_event) else: session.add(LastEventCache(**cached_event)) def sql_migrate(self, logpath=None, lower=str.lower): """ Migrate existing logs to the new SQL database """ if logpath is None: logpath = self.logpath with open(logpath) as logfile: while True: with self.db() as session: finished = True for line in islice(logfile, None, 100): finished = False try: line = line.rstrip("\n").rstrip("\r") timestamp, text = line.split(" ", 1) event = make_event( text, timestamp=datetime.utcfromtimestamp( float(timestamp) ) ) session.add(event) self.cache_event(session, event) except: print("[Logger] Warning: Could not parse %s" % line) raise if finished: break @Callback.background @command("log_migrate", "(.+)", admin=True) def partial_migration(self, server, message, path): yield "Migration started..." self.sql_migrate(logpath=path, lower=server.lower) yield "Migration complete." def traceuser(self, hostmask, timestamp): # Load the logs into memory to compute graph # FIXME: Paginate log loading start_nick, x = hostmask.split("!", 1) start_ident, start_mask = x.split("@", 1) userinf = {(start_nick, start_ident, start_mask)} with self.db() as session: logs = session.query( Event ).filter( Event.type.in_(['NICK', 'JOIN']), Event.timestamp >= timestamp, ).order_by(Event.timestamp).all() for log in logs: nick, x = log.sender.split("!", 1) ident, mask = x.split("@", 1) if log.type == "NICK" and (nick, ident, mask) in userinf: userinf.add((log.payload[1:], ident, mask)) elif log.type == "JOIN" and any( log.sender_nick == self.lower(n) or (ident, mask) == (i, m) for n, i, m in userinf ): userinf.add((nick, ident, mask)) return userinf @Callback.inline def log(self, server, line) -> "ALL": timestamp = datetime.utcnow() event = make_event(line, timestamp=timestamp) self.db.add(event) def cache_update(self): with self.db() as session: last_entry = session.query( func.max(LastEventCache.timestamp) ).first()[0] or 0 events = session.query(Event).filter( Event.timestamp > last_entry, Event.type.in_(HAS_SENDER) ).all() for event in events: self.cache_event(session, event) @Callback.background def flush(self, server, line) -> "ALL": if len(self.db.cache) > 32: self.db.flush() self.cache_update() @command("seen lastseen", r"(\S+)") def seen(self, server, msg, user): if server.eq(user, msg.address.nick): return "04⎟ You're right there!" context = server.lower(msg.context) nick = server.lower(user) # Don't allow seen for pms, for confidentiality if not context.startswith("#"): return self.cache_update() with self.db() as session: last = session.query( LastEventCache ).filter( (LastEventCache.context == context) | (LastEventCache.context == None), (LastEventCache.nick == nick) | (LastEventCache.newnick == nick) ).order_by( LastEventCache.timestamp.desc() ).first() if last is None: return "04⎟ I haven't seen %s yet." % user event = make_event( last.data, timestamp=last.timestamp, key=server.lower ) message = self.formatters[event.type](event) timestamp = last.timestamp host = event.sender if server.isIn(user, server.channels.get(context)): status = " · \x0312online now" else: for nick, _, _ in self.traceuser(host, timestamp): if server.isIn(nick, server.channels.get(context)): status = " · \x0312online as %s" % nick break else: status = "" return "%s · \x1d%s%s" % (message, timefmt(timestamp), status) @command("last lastspoke lastmsg", r"(\S+)") def lastspoke(self, server, msg, user): if server.eq(user, msg.address.nick): return "04⎟ You just spoke!" context = server.lower(msg.context) nick = server.lower(user) # Don't allow seen for pms, for confidentiality if not context.startswith("#"): return self.cache_update() with self.db() as session: last = session.query(LastSpokeCache).filter( LastSpokeCache.context == context, LastSpokeCache.nick == nick ).first() if last is None: return "04⎟ I haven't seen %s speak yet." % user event = make_event(last.data, timestamp=last.timestamp) return "%s · \x1d%s" % (msgfmt(event), timefmt(last.timestamp)) @command("sedon", rank="@") def sedon(self, server, msg): self.sedchans.add(server.lower(msg.context)) return "04⎟ Turned on sed." @command("sedoff", rank="@") def sedoff(self, server, msg): self.sedchans.remove(server.lower(msg.context)) return "04⎟ Turned off sed." # @msghandler def substitute(self, server, msg): raise NotImplementedError if not server.isIn(msg.context, self.sedchans): return match = re.match(r"^(\S+:\s+)?s(\W)(.*?)\2(.*?)(\2g?)?$", msg.text) if match: target, sep, pattern, sub, flags = match.groups() if target is not None: target = target.rstrip(": ") if flags is not None: flags = set(flags[1:]) pattern = re.escape(pattern) for timestamp, line in reversed(self.logs): # TODO: implement real regular expressions # also self-regex try: evt = IRCEvent(line) if ( evt.type == "PRIVMSG" and server.eq(msg.context, evt.args[0]) and ( target is None or server.eq(target, evt.sender.nick) ) and not re.match( r"^(\S+:\s+)?s(\W)(.*?)\2(.*?)(\2g?)?$", evt.args[1] ) and re.search(pattern, evt.args[1], flags=re.IGNORECASE) ): evt.args[1] = re.sub( pattern, "\x1f%s\x1f" % sub, evt.args[1], count=0 if 'g' in flags else 1, flags=re.IGNORECASE ) return msgfmt(evt) except: print("[Logger] Warning: Could not parse %s" % line) return "04⎟ No matches found." def __destroy__(self, *_): self.db.flush()