diff --git a/Logger/__init__.py b/Logger/__init__.py new file mode 100644 index 0000000..655b8ea --- /dev/null +++ b/Logger/__init__.py @@ -0,0 +1,33 @@ + +""" +An SQLite3 channel logger +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +__author__ = supybot.Author("Owen", "ShadowNinja", "shadowninja@minetest.net") + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +from . import config +from . import storage +from . import plugin +from imp import reload +# In case we're being reloaded. +reload(config) +reload(storage) +reload(plugin) + +if world.testing: + from . import test + +Class = plugin.Class +configure = config.configure + diff --git a/Logger/config.py b/Logger/config.py new file mode 100644 index 0000000..948f447 --- /dev/null +++ b/Logger/config.py @@ -0,0 +1,13 @@ +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Logger', True) + + +Logger = conf.registerPlugin('Logger') + +conf.registerChannelValue(Logger, "enable", + registry.Boolean(True, """Determines whether logging is enabled.""")) + diff --git a/Logger/plugin.py b/Logger/plugin.py new file mode 100644 index 0000000..04b78ed --- /dev/null +++ b/Logger/plugin.py @@ -0,0 +1,128 @@ + +import supybot.callbacks as callbacks +import supybot.conf as conf +import supybot.utils as utils +import supybot.ircmsgs as ircmsgs +import supybot.world as world +import supybot.irclib as irclib +from supybot.commands import * + +import time + +from .storage import LogDB, MessageType + +filename = conf.supybot.directories.data.dirize("Log.sqlite") + +class Logger(callbacks.Plugin): + """ + An SQLite3 channel logger + """ + + noIgnore = True + threaded = True + + lastMsgs = {} + lastStates = {} + + def __init__(self, irc): + self.__parent = super(Logger, self) + self.__parent.__init__(irc) + self.db = LogDB(filename) + world.flushers.append(self.flush) + + def __call__(self, irc, msg): + try: + self.__parent.__call__(irc, msg) + if irc in self.lastMsgs: + if irc not in self.lastStates: + self.lastStates[irc] = irc.state.copy() + self.lastStates[irc].addMsg(irc, self.lastMsgs[irc]) + finally: + self.lastMsgs[irc] = msg + + def reset(self): + self.lastMsgs.clear() + self.lastStates.clear() + + def die(self): + self.__parent.die() + world.flushers.remove(self.flush) + del self.db + + def flush(self): + self.db.commit() + + def shouldLog(self, irc, msg, msgtype): + if not self.registryValue("enable"): + return False + if msgtype == MessageType.privmsg or msgtype == MessageType.notice: + if not irc.isChannel(msg.args[0]): + return False + if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): + return False + if msgtype == MessageType.mode and msg.args[0] == irc.nick: + return False + return True + + def _add(self, irc, msg, msgtype): + if not self.shouldLog(irc, msg, msgtype): + return + + channel = msg.args[0] + if not self.registryValue("enable", channel): + return + + text = None + if len(msg.args) > 1: + text = msg.args[1] + + if msgtype == MessageType.mode: + text = " ".join(msg.args[1:]) + elif ircmsgs.isAction(msg): + msgtype = MessageType.action + text = msg.args[1][8:-1] + + self.db.add(msgtype, irc.network, channel, msg, text) + self.db.commit() + + def doPrivmsg(self, irc, msg): self._add(irc, msg, MessageType.privmsg) + def doNotice (self, irc, msg): self._add(irc, msg, MessageType.notice) + def doJoin (self, irc, msg): self._add(irc, msg, MessageType.join) + def doPart (self, irc, msg): self._add(irc, msg, MessageType.part) + def doKick (self, irc, msg): self._add(irc, msg, MessageType.kick) + def doMode (self, irc, msg): self._add(irc, msg, MessageType.mode) + def doTopic (self, irc, msg): self._add(irc, msg, MessageType.topic) + + def doNick(self, irc, msg): + if not self.shouldLog(irc, msg, MessageType.nick): + return + oldNick = msg.nick + newNick = msg.args[0] + for (channel, chan) in irc.state.channels.items(): + if newNick in chan.users: + self.db.add(MessageType.nick, + irc.network, + channel, + msg, + newNick) + self.db.commit() + + def doQuit(self, irc, msg): + if not self.shouldLog(irc, msg, MessageType.quit): + return + reason = None + if len(msg.args) == 1: + reason = msg.args[0] + if not isinstance(irc, irclib.Irc): + irc = irc.getRealIrc() + for (channel, chan) in self.lastStates[irc].channels.items(): + if msg.nick in chan.users: + self.db.add(MessageType.quit, + irc.network, + channel, + msg, + reason) + self.db.commit() + +Class = Logger + diff --git a/Logger/storage.py b/Logger/storage.py new file mode 100644 index 0000000..3f84fe7 --- /dev/null +++ b/Logger/storage.py @@ -0,0 +1,172 @@ + +import time +import sqlite3 +from threading import RLock + +class MessageType: + privmsg = 0 + notice = 1 + action = 2 + join = 3 + part = 4 + quit = 5 + kick = 6 + nick = 7 + mode = 8 + topic = 9 + +class LogDB: + def __init__(self, filename): + self.conn = sqlite3.connect(filename, 5, 0, None, False) + self.conn.row_factory = sqlite3.Row + self.cur = self.conn.cursor() + self.cur.executescript(""" + CREATE TABLE IF NOT EXISTS sender ( + id INTEGER NOT NULL, + nick VARCHAR, + user VARCHAR, + host VARCHAR, + PRIMARY KEY (id) + ); + CREATE TABLE IF NOT EXISTS network ( + id INTEGER NOT NULL, + name VARCHAR, + PRIMARY KEY (id) + ); + CREATE TABLE IF NOT EXISTS buffer ( + id INTEGER NOT NULL, + networkid INTEGER NOT NULL, + name VARCHAR, + PRIMARY KEY (id), + FOREIGN KEY(networkid) REFERENCES network (id) + ); + CREATE TABLE IF NOT EXISTS log ( + id INTEGER NOT NULL, + type INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + bufferid INTEGER NOT NULL, + senderid INTEGER NOT NULL, + message VARCHAR, + PRIMARY KEY (id), + FOREIGN KEY(bufferid) REFERENCES buffer (id), + FOREIGN KEY(senderid) REFERENCES sender (id) + ); + """) + + self.lock = RLock() + + def __del__(self): + with self.lock: + self.cur.close() + self.conn.commit() + self.conn.close() + + def commit(self): + with self.lock: + self.conn.commit() + + def _getNetwork(self, network): + return self.cur.execute(""" + SELECT * FROM network WHERE name=? + """, (network,)).fetchone() + + def getNetwork(self, network, create=True): + with self.lock: + row = self._getNetwork(network) + if row is None and create: + self.cur.execute(""" + INSERT INTO network (name) VALUES (?) + """, (network,)) + self.conn.commit() + row = self._getNetwork(network) + return row + + def _getBuffer(self, network, name): + return self.cur.execute(""" + SELECT * FROM buffer + INNER JOIN network ON network.id=buffer.networkid + WHERE + network.name=? AND + buffer.name=? + """, (network, name)).fetchone() + + + def getBuffer(self, network, name=None, create=True): + with self.lock: + row = self._getBuffer(network, name) + if row is None and create: + self.cur.execute(""" + INSERT INTO buffer (name, networkid) VALUES (?, ?) + """, (name, self.getNetwork(network)["id"])) + self.conn.commit() + row = self._getBuffer(network, name) + return row + + def _getSender(self, msg): + return self.cur.execute(""" + SELECT * FROM sender + WHERE + nick=? AND + user=? AND + host=? + """, (msg.nick, msg.user, msg.host)).fetchone() + + def getSender(self, msg, create=True): + with self.lock: + row = self._getSender(msg) + if row is None and create: + self.cur.execute(""" + INSERT INTO sender (nick, user, host) VALUES (?, ?, ?) + """, (msg.nick, msg.user, msg.host)) + self.conn.commit() + row = self._getSender(msg) + return row + + def add(self, msgtype, network, channel, msg, text): + with self.lock: + self.cur.execute(""" + INSERT INTO log (type, timestamp, bufferid, senderid, message) + VALUES (?, ?, ?, ?, ?) + """, ( + msgtype, + int(time.time()), + self.getBuffer(network, channel)["id"], + self.getSender(msg)["id"], + text + )) + + def get(self, bufid, starttime, timelen): + with self.lock: + return self.cur.execute(""" + SELECT * FROM log + INNER JOIN buffer ON buffer.id=log.bufferid + INNER JOIN network ON network.id=buffer.networkid + INNER JOIN sender ON sender.id=log.senderid + WHERE + log.bufferid=? AND + log.timestamp BETWEEN ? AND ? + ORDER BY id ASC + """, (bufid, starttime, starttime + timelen)).fetchall() + + def getBuffers(self): + with self.lock: + return self.cur.execute(""" + SELECT * FROM buffer + INNER JOIN network ON network.id=buffer.networkid + ORDER BY id ASC + """).fetchall() + + def getNetworks(self): + with self.lock: + return self.cur.execute(""" + SELECT * FROM network + ORDER BY id ASC + """).fetchall() + + def getSenders(self): + with self.lock: + return self.cur.execute(""" + SELECT * FROM serder + ORDER BY id ASC + """).fetchall() + diff --git a/Logger/test.py b/Logger/test.py new file mode 100644 index 0000000..e69de29 diff --git a/Logger/totext.py b/Logger/totext.py new file mode 100644 index 0000000..741bf88 --- /dev/null +++ b/Logger/totext.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import sys +import time +from storage import LogDB + +if len(sys.argv) != 5: + print("""Usage: + %s