예제 #1
0
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()
예제 #2
0
파일: logger.py 프로젝트: svkampen/Karkat
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()