161 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Lua
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			161 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Lua
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env lua
 | |
| --
 | |
| -- ZyklonB seen plugin
 | |
| --
 | |
| -- Copyright 2016 Přemysl Eric Janouch <p@janouch.name>
 | |
| -- 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: <name>")
 | |
| 	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
 |