From d97f28e7f7b76b0e2d458baea5f46bc6cb9be0d5 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Sun, 24 Apr 2016 21:05:53 +0200
Subject: ZyklonB: add a seen plugin
---
plugins/zyklonb/seen | 160 +++++++++++++++++++++++++++++++++
plugins/zyklonb/seen-import-degesch.pl | 39 ++++++++
2 files changed, 199 insertions(+)
create mode 100755 plugins/zyklonb/seen
create mode 100755 plugins/zyklonb/seen-import-degesch.pl
diff --git a/plugins/zyklonb/seen b/plugins/zyklonb/seen
new file mode 100755
index 0000000..3f49ea8
--- /dev/null
+++ b/plugins/zyklonb/seen
@@ -0,0 +1,160 @@
+#!/usr/bin/env lua
+--
+-- ZyklonB seen plugin
+--
+-- Copyright 2016 Přemysl Janouch
+-- See the file LICENSE for licensing information.
+--
+
+function parse (line)
+ local msg = { params = {} }
+ line = line:match ("[^\r]*")
+ for start, word in line:gmatch ("()([^ ]+)") do
+ local colon = word:match ("^:(.*)")
+ if start == 1 and colon then
+ msg.prefix = colon
+ elseif not msg.command then
+ msg.command = word
+ elseif colon then
+ table.insert (msg.params, line:sub (start + 1))
+ break
+ elseif start ~= #line then
+ table.insert (msg.params, word)
+ end
+ end
+ return msg
+end
+
+function get_config (name)
+ io.write ("ZYKLONB get_config :", name, "\r\n")
+ return parse (io.read ()).params[1]
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+io.output ():setvbuf ('line')
+local prefix = get_config ('prefix')
+io.write ("ZYKLONB register\r\n")
+
+local db = {}
+local db_filename = "seen.db"
+local db_garbage = 0
+
+function remember (who, where, when, what)
+ if not db[who] then db[who] = {} end
+ if db[who][where] then db_garbage = db_garbage + 1 end
+ db[who][where] = { tonumber (when), what }
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+local db_file, e = io.open (db_filename, "a+")
+if not db_file then error ("cannot open database: " .. e, 0) end
+
+function db_store (who, where, when, what)
+ db_file:write (string.format
+ (":%s %s %s %s :%s\n", who, "PRIVMSG", where, when, what))
+end
+
+function db_compact ()
+ db_file:close ()
+
+ -- Unfortunately, default Lua doesn't have anything like mkstemp()
+ local db_tmpname = db_filename .. "." .. os.time ()
+ db_file, e = io.open (db_tmpname, "a+")
+ if not db_file then error ("cannot save database: " .. e, 0) end
+
+ for who, places in pairs (db) do
+ for where, data in pairs (places) do
+ db_store (who, where, data[1], data[2])
+ end
+ end
+ db_file:flush ()
+
+ local ok, e = os.rename (db_tmpname, db_filename)
+ if not ok then error ("cannot save database: " .. e, 0) end
+ db_garbage = 0
+end
+
+for line in db_file:lines () do
+ local msg = parse (line)
+ remember (msg.prefix, table.unpack (msg.params))
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+function seen (who, where, args)
+ local respond = function (...)
+ local privmsg = function (target, ...)
+ io.write ("PRIVMSG ", target, " :", table.concat { ... }, "\r\n")
+ end
+ if where:match ("^[#&!+]") then
+ privmsg (where, who, ": ", ...)
+ else
+ privmsg (who, ...)
+ end
+ end
+
+ local whom, e, garbage = args:match ("^(%S+)()%s*(.*)")
+ if not whom or #garbage ~= 0 then
+ return respond ("usage: ")
+ elseif who:lower () == whom:lower () then
+ return respond ("I can see you right now.")
+ end
+
+ local top = {}
+ -- That is, * acts like a wildcard, otherwise everything is escaped
+ local pattern = "^" .. whom:gsub ("[%^%$%(%)%%%.%[%]%+%-%?]", "%%%0")
+ :gsub ("%*", ".*"):lower () .. "$"
+ for name, places in pairs (db) do
+ if places[where] and name:lower ():match (pattern) then
+ local when, what = table.unpack (places[where])
+ table.insert (top, { name = name, when = when, what = what })
+ end
+ end
+ if #top == 0 then
+ return respond ("I have not seen \x02" .. whom .. "\x02 here.")
+ end
+
+ -- Get all matching nicknames ordered from the most recently active
+ -- and make the list case insensitive (remove older duplicates)
+ table.sort (top, function (a, b) return a.when > b.when end)
+ for i = #top, 2, -1 do
+ if top[i - 1].name:lower () == top[i].name:lower () then
+ table.remove (top, i)
+ end
+ end
+
+ -- Hopefully the formatting mess will disrupt highlights in clients
+ for i = 1, math.min (#top, 3) do
+ local name = top[i].name:gsub ("^.", "%0\x02\x02")
+ respond (string.format ("\x02%s\x02 -> %s -> %s",
+ name, os.date ("%c", top[i].when), top[i].what))
+ end
+end
+
+function handle (msg)
+ local who = msg.prefix:match ("^[^!@]*")
+ local where, what = table.unpack (msg.params)
+ local when = os.time ()
+
+ local what_log = what:gsub ("^\x01ACTION", "*"):gsub ("\x01$", "")
+ remember (who, where, when, what_log)
+ db_store (who, where, when, what_log)
+
+ -- Comment out to reduce both disk load and reliability
+ db_file:flush ()
+
+ if db_garbage > 5000 then db_compact () end
+
+ if what:sub (1, #prefix) == prefix then
+ local command = what:sub (#prefix + 1)
+ local name, e = command:match ("^(%S+)%s*()")
+ if name == 'seen' then seen (who, where, command:sub (e)) end
+ end
+end
+
+for line in io.lines () do
+ local msg = parse (line)
+ if msg.command == "PRIVMSG" then handle (msg) end
+end
diff --git a/plugins/zyklonb/seen-import-degesch.pl b/plugins/zyklonb/seen-import-degesch.pl
new file mode 100755
index 0000000..ddef6be
--- /dev/null
+++ b/plugins/zyklonb/seen-import-degesch.pl
@@ -0,0 +1,39 @@
+#!/usr/bin/env perl
+# Creates a database for the "seen" plugin from logs for degesch.
+# The results may not be completely accurate but are good for jumpstarting.
+# Usage: ./seen-import-degesch.pl LOG-FILE... > seen.db
+
+use strict;
+use warnings;
+use File::Basename;
+use Time::Piece;
+
+my $db = {};
+for (@ARGV) {
+ my $where = (basename($_) =~ /\.(.*).log/)[0];
+ unless ($where) {
+ print STDERR "Invalid filename: $_\n";
+ next;
+ }
+
+ open my $fh, '<', $_ or die "Failed to open log file: $!";
+ while (<$fh>) {
+ my ($when, $who, $who_action, $what) =
+ /^(.{19}) (?:<[~&@%+]*(.*?)>| \* (\S+)) (.*)/;
+ next unless $when;
+
+ if ($who_action) {
+ $who = $who_action;
+ $what = "* $what";
+ }
+ $db->{$who}->{$where} =
+ [Time::Piece->strptime($when, "%Y-%m-%d %T")->epoch, $what];
+ }
+}
+
+while (my ($who, $places) = each %$db) {
+ while (my ($where, $data) = each %$places) {
+ my ($when, $what) = @$data;
+ print ":$who PRIVMSG $where $when :$what\n";
+ }
+}
--
cgit v1.2.3-70-g09d2