#!/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