summaryrefslogtreecommitdiff
path: root/plugins/zyklonb/seen
blob: b9655784232e4af2b730a9b147ed3dede82c3a38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env lua
--
-- ZyklonB seen plugin
--
-- Copyright 2016 Přemysl 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