From d97f28e7f7b76b0e2d458baea5f46bc6cb9be0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 24 Apr 2016 21:05:53 +0200 Subject: [PATCH] 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"; + } +}