Line 0
Link Here
|
|
|
1 |
%%%---------------------------------------------------------------------- |
2 |
%%% File : mod_muc_log.erl |
3 |
%%% Author : Badlop |
4 |
%%% Purpose : MUC room logging |
5 |
%%% Created : |
6 |
%%% Id : |
7 |
%%%---------------------------------------------------------------------- |
8 |
|
9 |
-module(mod_muc_log). |
10 |
-author(''). |
11 |
-vsn(''). |
12 |
|
13 |
%-behaviour(gen_mod). |
14 |
|
15 |
-export([start/1, |
16 |
stop/0, |
17 |
init_log/1, |
18 |
check_access_log/2, |
19 |
add_to_log/3]). |
20 |
|
21 |
-include("ejabberd.hrl"). |
22 |
-include("jlib.hrl"). |
23 |
-define(T(Text), translate:translate("Lang", Text)). |
24 |
|
25 |
-define(PROCNAME, ejabberd_mod_muc_log). |
26 |
-record(room, {jid, title, subject, subject_author, config}). |
27 |
|
28 |
%Copied from mod_muc_room.erl |
29 |
-define(DICT, dict). |
30 |
-record(config, {title = "", |
31 |
allow_change_subj = true, |
32 |
allow_query_users = true, |
33 |
allow_private_messages = true, |
34 |
public = true, |
35 |
public_list = true, |
36 |
persistent = false, |
37 |
moderated = false, % TODO |
38 |
members_by_default = true, |
39 |
members_only = false, |
40 |
allow_user_invites = false, |
41 |
password_protected = false, |
42 |
password = "", |
43 |
anonymous = true, |
44 |
logging = false |
45 |
}). |
46 |
-record(user, {jid, |
47 |
nick, |
48 |
role, |
49 |
last_presence}). |
50 |
-record(state, {room, |
51 |
host, |
52 |
server_host, |
53 |
access, |
54 |
jid, |
55 |
config = #config{}, |
56 |
users = ?DICT:new(), |
57 |
affiliations = ?DICT:new(), |
58 |
history = lqueue_new(20), |
59 |
subject = "", |
60 |
subject_author = "", |
61 |
just_created = false}). |
62 |
|
63 |
|
64 |
%%---------------------------------------------------------------------- |
65 |
%% Module control |
66 |
|
67 |
start(Opts) -> |
68 |
case gen_mod:get_opt(allow_room_log, Opts, true) of |
69 |
true -> |
70 |
OutDir = gen_mod:get_opt(outdir, Opts, "www/muc"), |
71 |
DirType = gen_mod:get_opt(dirtype, Opts, subdirs), |
72 |
CSSFile = gen_mod:get_opt(cssfile, Opts, false), |
73 |
CSSFile = gen_mod:get_opt(cssfile, Opts, false), |
74 |
AccessLog = gen_mod:get_opt(access_log, Opts, muc_admin), |
75 |
case lists:member(?PROCNAME, registered()) of |
76 |
false -> |
77 |
register(?PROCNAME, spawn(?MODULE, init_log, [{OutDir, DirType, CSSFile, AccessLog}])); |
78 |
true -> |
79 |
ok |
80 |
end; |
81 |
false -> |
82 |
ok |
83 |
end. |
84 |
|
85 |
init_log(O) -> |
86 |
loop_log(O). |
87 |
|
88 |
loop_log(Options) -> |
89 |
receive |
90 |
{log, Data} -> |
91 |
add_message_to_log2(Data, Options), |
92 |
loop_log(Options); |
93 |
{check_access_log, ServerHost, From, Pid} -> |
94 |
{_, _, _, AccessLog} = Options, |
95 |
Pid ! acl:match_rule(ServerHost, AccessLog, From), |
96 |
loop_log(Options); |
97 |
stop -> |
98 |
% TODO |
99 |
ok; |
100 |
_ -> |
101 |
loop_log(Options) |
102 |
end. |
103 |
|
104 |
stop() -> |
105 |
?PROCNAME ! stop. |
106 |
|
107 |
check_access_log(ServerHost, From) -> |
108 |
case whereis(?PROCNAME) of |
109 |
undefined -> false; |
110 |
_ -> |
111 |
?PROCNAME ! {check_access_log, ServerHost, From, self()}, |
112 |
receive A -> A end |
113 |
end. |
114 |
|
115 |
|
116 |
%%---------------------------------------------------------------------- |
117 |
%% Frontend |
118 |
|
119 |
% Check if this room is configured is logged |
120 |
add_to_log(A, D, StateData) -> |
121 |
case (StateData#state.config)#config.logging of |
122 |
true -> add_to_log(A, D); |
123 |
false -> ok |
124 |
end. |
125 |
|
126 |
% Check if MUC logging is allowed |
127 |
add_to_log(A, D) -> |
128 |
case whereis(?PROCNAME) of |
129 |
undefined -> ok; |
130 |
_ -> add_to_log2(A, D) |
131 |
end. |
132 |
|
133 |
add_to_log2(text, {Nick, Packet, StateData}) -> |
134 |
case {xml:get_subtag(Packet, "subject"), xml:get_subtag(Packet, "body")} of |
135 |
{false, false} -> |
136 |
ok; |
137 |
{false, SubEl} -> |
138 |
Message = {body, htmlize(xml:get_tag_cdata(SubEl))}, |
139 |
add_message_to_log(Nick, Message, StateData); |
140 |
{SubEl, _false} -> |
141 |
Message = {subject, htmlize(xml:get_tag_cdata(SubEl))}, |
142 |
add_message_to_log(Nick, Message, StateData) |
143 |
end; |
144 |
|
145 |
add_to_log2(roomconfig_change, {XData, StateData}) -> |
146 |
add_message_to_log("eeeaaae", {roomconfig_change, XData}, StateData); |
147 |
|
148 |
add_to_log2(nickchange, {OldNick, NewNick, StateData}) -> |
149 |
add_message_to_log(NewNick, {nickchange, OldNick}, StateData); |
150 |
|
151 |
add_to_log2(join, {Nick, StateData}) -> |
152 |
add_message_to_log(Nick, {join}, StateData); |
153 |
|
154 |
add_to_log2(leave, {LJID, Reason, StateData}) -> |
155 |
{ok, #user{nick = Nick}} = |
156 |
?DICT:find(LJID, StateData#state.users), |
157 |
case Reason of |
158 |
"" -> add_message_to_log(Nick, {leave}, StateData); |
159 |
_ -> add_message_to_log(Nick, {leave, Reason}, StateData) |
160 |
end; |
161 |
|
162 |
add_to_log2(kickban, {LJID, Reason, Code, StateData}) -> |
163 |
{ok, #user{nick = Nick}} = |
164 |
?DICT:find(LJID, StateData#state.users), |
165 |
add_message_to_log(Nick, {kickban, Code, Reason}, StateData). |
166 |
|
167 |
|
168 |
%%---------------------------------------------------------------------- |
169 |
%% Core |
170 |
|
171 |
add_message_to_log(Nick, Message, StateData) -> |
172 |
?PROCNAME ! {log, {Nick, Message, StateData}}. |
173 |
|
174 |
add_message_to_log2({Nick, Message, StateData}, Options) -> |
175 |
{OutDir, DirType, CSSFile, _AccessLog} = Options, |
176 |
Room = get_room_info(StateData), |
177 |
|
178 |
% Timestamp, Date and Time |
179 |
TimeStamp = jlib:timestamp_to_iso(calendar:now_to_universal_time(now())), |
180 |
Year = string:substr(TimeStamp, 1, 4), |
181 |
Month = string:substr(TimeStamp, 5, 2), |
182 |
Day = string:substr(TimeStamp, 7, 2), |
183 |
A1 = string:concat(Year, "-"), |
184 |
A2 = string:concat(A1, Month), |
185 |
A3 = string:concat(A2, "-"), |
186 |
Date = string:concat(A3, Day), |
187 |
|
188 |
% Directory and file names |
189 |
{Dir, Filename} = case DirType of |
190 |
subdirs -> |
191 |
{filename:join(Year, Month), Day}; |
192 |
plain -> |
193 |
{"", Date} |
194 |
end, |
195 |
Fd = filename:join([OutDir, Room#room.jid, Dir]), |
196 |
Fn = filename:join([Fd, string:concat(Filename, ".html")]), |
197 |
|
198 |
% Open file, create if it does not exist, create parent dirs if needed |
199 |
case file:read_file_info(Fn) of |
200 |
{ok, _} -> |
201 |
{ok, F} = file:open(Fn, [append]); |
202 |
{error, enoent} -> |
203 |
make_dir_rec(Fd), |
204 |
{ok, F} = file:open(Fn, [append]), |
205 |
Datestring = get_dateweek(date()), |
206 |
put_header(F, Room, Datestring, CSSFile) |
207 |
end, |
208 |
|
209 |
% Build message |
210 |
Text = case Message of |
211 |
{roomconfig_change, XData} -> |
212 |
RoomConfig = roomconfigwire_to_string(XData), |
213 |
put_room_config(F, RoomConfig), |
214 |
io_lib:format("<font class=\"system\">~s</font><br/>", |
215 |
[?T("Chatroom configuration modified")]); |
216 |
{join} -> |
217 |
io_lib:format("<font class=\"system\">~s ~s</font><br/>", |
218 |
[Nick, ?T("joins the room")]); |
219 |
{leave} -> |
220 |
io_lib:format("<font class=\"system\">~s ~s</font><br/>", |
221 |
[Nick, ?T("leaves the room")]); |
222 |
{leave, Reason} -> |
223 |
io_lib:format("<font class=\"system\">~s ~s: ~s</font><br/>", |
224 |
[Nick, ?T("leaves the room"), Reason]); |
225 |
{kickban, "307", ""} -> |
226 |
io_lib:format("<font class=\"system\">~s ~s</font><br/>", |
227 |
[Nick, ?T("has been kicked")]); |
228 |
{kickban, "307", Reason} -> |
229 |
io_lib:format("<font class=\"system\">~s ~s: ~s</font><br/>", |
230 |
[Nick, ?T("has been kicked"), Reason]); |
231 |
{kickban, "301", ""} -> |
232 |
io_lib:format("<font class=\"system\">~s ~s</font><br/>", |
233 |
[Nick, ?T("has been banned")]); |
234 |
{kickban, "301", Reason} -> |
235 |
io_lib:format("<font class=\"system\">~s ~s: ~s</font><br/>", |
236 |
[Nick, ?T("has been banned"), Reason]); |
237 |
{nickchange, OldNick} -> |
238 |
io_lib:format("<font class=\"system\">~s ~s ~s</font><br/>", |
239 |
[OldNick, ?T("is now known as"), Nick]); |
240 |
{subject, T} -> |
241 |
io_lib:format("<font class=\"system\">~s~s~s</font><br/>", |
242 |
[Nick, ?T(" has set the subject to: "), T]); |
243 |
{body, T} -> |
244 |
case regexp:first_match(T, "^/me\s") of |
245 |
{match, _, _} -> |
246 |
io_lib:format("<font class=\"emote\">~s ~s</font><br/>", |
247 |
[Nick, string:substr(T, 5)]); |
248 |
nomatch -> |
249 |
io_lib:format("<font class=\"normal\"><~s></font> ~s<br/>", |
250 |
[Nick, T]) |
251 |
end |
252 |
end, |
253 |
Time = string:substr(TimeStamp, 10, 8), |
254 |
|
255 |
% Write message |
256 |
file:write(F, io_lib:format("<a name=\"~s\" href=\"#~s\" class=\"ts\">[~s]</a> ~s~n", |
257 |
[Time, Time, Time, Text])), |
258 |
|
259 |
% Close file |
260 |
file:close(F), |
261 |
|
262 |
ok. |
263 |
|
264 |
|
265 |
%%---------------------------------------------------------------------- |
266 |
%% Utilities |
267 |
|
268 |
get_dateweek(Dt) -> |
269 |
Weekday = case calendar:day_of_the_week(Dt) of |
270 |
1 -> ?T("Monday"); |
271 |
2 -> ?T("Tuesday"); |
272 |
3 -> ?T("Wednesday"); |
273 |
4 -> ?T("Thursday"); |
274 |
5 -> ?T("Friday"); |
275 |
6 -> ?T("Saturday"); |
276 |
7 -> ?T("Sunday") |
277 |
end, |
278 |
{Y, M, D} = Dt, |
279 |
Month = case M of |
280 |
1 -> ?T("January"); |
281 |
2 -> ?T("February"); |
282 |
3 -> ?T("March"); |
283 |
4 -> ?T("April"); |
284 |
5 -> ?T("May"); |
285 |
6 -> ?T("June"); |
286 |
7 -> ?T("July"); |
287 |
8 -> ?T("August"); |
288 |
9 -> ?T("September"); |
289 |
10 -> ?T("October"); |
290 |
11 -> ?T("November"); |
291 |
12 -> ?T("December") |
292 |
end, |
293 |
case ?MYLANG of |
294 |
"en" -> io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y]); |
295 |
"es" -> io_lib:format("~s ~w de ~s de ~w", [Weekday, D, Month, Y]); |
296 |
_ -> io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y]) |
297 |
end. |
298 |
|
299 |
make_dir_rec(Dir) -> |
300 |
case file:read_file_info(Dir) of |
301 |
{ok, _} -> |
302 |
ok; |
303 |
{error, enoent} -> |
304 |
DirS = filename:split(Dir), |
305 |
DirR = lists:sublist(DirS, length(DirS)-1), |
306 |
make_dir_rec(filename:join(DirR)), |
307 |
file:make_dir(Dir) |
308 |
end. |
309 |
|
310 |
fw(F, S, O) -> file:write(F, io_lib:format(S++"~n", O)). |
311 |
fw(F, S) -> fw(F, S, []). |
312 |
|
313 |
put_header(F, Room, Date, CSSFile) -> |
314 |
fw(F, "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"), |
315 |
fw(F, "<html>"), |
316 |
%fw(F, "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">"), |
317 |
fw(F, "<head>"), |
318 |
%fw(F, "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />"), |
319 |
fw(F, "<title>~s - ~s</title>", [Room#room.title, Date]), |
320 |
put_header_css(F, CSSFile), |
321 |
put_header_script(F), |
322 |
fw(F, "</head>"), |
323 |
fw(F, "<body>"), |
324 |
fw(F, "<div class=\"mucname\">ejabberd/mod_muc log</div>"), |
325 |
fw(F, "<div class=\"roomtitle\">~s</div>", [Room#room.title]), |
326 |
fw(F, "<div class=\"roomjid\">~s</div>", [Room#room.jid]), |
327 |
fw(F, "<div class=\"logdate\">~s</div>", [Date]), |
328 |
case {Room#room.subject_author, Room#room.subject} of |
329 |
{"", ""} -> ok; |
330 |
{SuA, Su} -> fw(F, "<div class=\"roomsubject\">~s~s~s</div>", [SuA, ?T(" has set the subject to: "), Su]) |
331 |
end, |
332 |
RoomConfig = roomconfig_to_string(Room#room.config), |
333 |
put_room_config(F, RoomConfig), |
334 |
fw(F, "<br/>"). |
335 |
|
336 |
put_header_css(F, false) -> |
337 |
fw(F, "<style type=\"text/css\">"), |
338 |
fw(F, "<!--"), |
339 |
fw(F, ".ts {color: #AAAAAA; text-decoration: none;}"), |
340 |
fw(F, ".system {color: #009900; font-weight: bold;}"), |
341 |
fw(F, ".emote {color: #AA0099;}"), |
342 |
fw(F, ".self {color: #CC0000;}"), |
343 |
fw(F, ".normal {color: #0000AA;}"), |
344 |
fw(F, "div.mucname {color: #AAAAAA; text-align: right; font-family: monospace; letter-spacing: 3px;}"), |
345 |
fw(F, "div.roomtitle {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; border-bottom: #224466 solid 3pt; letter-spacing: 3px; margin-left: 20pt;}"), |
346 |
fw(F, "div.roomjid {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt;}"), |
347 |
fw(F, "div.logdate {color: #663399; font-size: 20px; font-weight: bold; font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt; margin-left:80pt; margin-top:40px;}"), |
348 |
fw(F, "div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif; margin-left: 80pt; margin-bottom: 10px;}"), |
349 |
fw(F, "div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 60%; text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699; border-right: 4px solid #336699;}"), |
350 |
fw(F, "div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px;}"), |
351 |
fw(F, "div.rcos {padding-right: 10px;}"), |
352 |
fw(F, "div.rcoe {color: green;}"), |
353 |
fw(F, "div.rcod {color: red;}"), |
354 |
fw(F, "div.rcoe:after {content: \": v\";}"), |
355 |
fw(F, "div.rcod:after {content: \": x\";}"), |
356 |
fw(F, "div.rcot:after {}"), |
357 |
fw(F, "//-->"), |
358 |
fw(F, "</style>"); |
359 |
|
360 |
put_header_css(F, CSSFile) -> |
361 |
fw(F, "<link rel=\"stylesheet\" type=\"text/css\" href=\"~s\" media=\"all\">", [CSSFile]). |
362 |
|
363 |
put_header_script(F) -> |
364 |
fw(F, "<script type=\"text/javascript\">"), |
365 |
fw(F, "function sh(e) // Show/Hide an element"), |
366 |
fw(F, "{if(document.getElementById(e).style.display=='none')"), |
367 |
fw(F, "{document.getElementById(e).style.display='block';}"), |
368 |
fw(F, "else {document.getElementById(e).style.display='none';}}"), |
369 |
fw(F, "</script>"). |
370 |
|
371 |
put_room_config(F, RoomConfig) -> |
372 |
{_, Now2, _} = now(), |
373 |
fw(F, "<div class=\"rc\">"), |
374 |
fw(F, "<div class=\"rct\" onClick=\"sh('a~p');return false;\">~s</div>", [Now2, ?T("Room Configuration")]), |
375 |
fw(F, "<div class=\"rcos\" id=\"a~p\" style=\"display: none;\" ><br/>~s</div>", [Now2, RoomConfig]), |
376 |
fw(F, "</div>"). |
377 |
|
378 |
htmlize(S1) -> |
379 |
S2_list = string:tokens(S1, "\n"), |
380 |
lists:foldl( |
381 |
fun(Si, Res) -> |
382 |
Si2 = htmlize2(Si), |
383 |
case Res of |
384 |
"" -> Si2; |
385 |
_ -> Res ++ "<br/>" ++ Si2 |
386 |
end |
387 |
end, |
388 |
"", |
389 |
S2_list). |
390 |
|
391 |
htmlize2(S1) -> |
392 |
S2 = element(2, regexp:gsub(S1, "<", "\\<")), |
393 |
S3 = element(2, regexp:gsub(S2, ">", "\\>")), |
394 |
S4 = element(2, regexp:gsub(S3, "(http|ftp)://.[^ ]*", "<a href=\"&\">&</a>")), |
395 |
element(2, regexp:gsub(S4, " ", "\\ ")). |
396 |
|
397 |
get_room_info(StateData) -> |
398 |
Room1 = element(2, StateData), |
399 |
Room2 = element(3, StateData), |
400 |
Room3 = string:concat(Room1, "@"), |
401 |
RoomJID = string:concat(Room3, Room2), |
402 |
#room{ |
403 |
jid = RoomJID, |
404 |
title = (StateData#state.config)#config.title, |
405 |
subject = StateData#state.subject, |
406 |
subject_author = StateData#state.subject_author, |
407 |
config = StateData#state.config |
408 |
}. |
409 |
|
410 |
roomconfig_to_string(RoomConfig) -> |
411 |
Options = [ |
412 |
{RoomConfig#config.allow_change_subj, allow_change_subj}, |
413 |
{RoomConfig#config.allow_query_users, allow_query_users}, |
414 |
{RoomConfig#config.allow_private_messages, allow_private_messages}, |
415 |
{RoomConfig#config.public, public}, |
416 |
{RoomConfig#config.public_list, public_list}, |
417 |
{RoomConfig#config.persistent, persistent}, |
418 |
{RoomConfig#config.moderated, moderated}, |
419 |
{RoomConfig#config.members_by_default, members_by_default}, |
420 |
{RoomConfig#config.members_only, members_only}, |
421 |
{RoomConfig#config.allow_user_invites, allow_user_invites}, |
422 |
{RoomConfig#config.password_protected, password_protected}, |
423 |
{RoomConfig#config.anonymous, anonymous} |
424 |
], |
425 |
rc_to_string(Options). |
426 |
|
427 |
roomconfigwire_to_string(XData) -> |
428 |
Options = [ |
429 |
{case Value_str of "0" -> false; "1" -> true; [] -> false; T -> {text, T} end, list_to_atom(Name_str)} |
430 |
|| {Name_str, [Value_str]} <- XData ], |
431 |
rc_to_string(Options). |
432 |
|
433 |
rc_to_string(Options) -> |
434 |
% Get title, if available |
435 |
Title = case lists:keysearch(title, 2, Options) of |
436 |
{value, Tuple} -> [Tuple]; |
437 |
false -> [] |
438 |
end, |
439 |
|
440 |
% Remove title from list |
441 |
Os1 = lists:keydelete(title, 2, Options), |
442 |
|
443 |
% Order list |
444 |
Os2 = lists:keysort(2, Os1), |
445 |
|
446 |
% Add title to ordered list |
447 |
Options2 = Title ++ Os2, |
448 |
|
449 |
lists:foldl( |
450 |
fun({RC, O}, R) -> |
451 |
O2 = ?T(get_roomconfig_text(O)), |
452 |
R2 = case RC of |
453 |
false -> "<div class=\"rcod\">" ++ O2 ++ "</div>"; |
454 |
true -> "<div class=\"rcoe\">" ++ O2 ++ "</div>"; |
455 |
{text, T} -> |
456 |
case O of |
457 |
password -> []; % Don't print passwords on log |
458 |
title -> "<div class=\"rcot\">" ++ ?T("Room title") ++ ": \"" ++ T ++ "\"</div>"; |
459 |
_ -> "\"" ++ T ++ "\"" |
460 |
end |
461 |
end, |
462 |
R ++ R2 |
463 |
end, |
464 |
"", |
465 |
Options2). |
466 |
|
467 |
get_roomconfig_text(title) -> "Room title"; |
468 |
get_roomconfig_text(persistent) -> "Make room persistent"; |
469 |
get_roomconfig_text(public) -> "Make room public searchable"; |
470 |
get_roomconfig_text(public_list) -> "Make participants list public"; |
471 |
get_roomconfig_text(password_protected) -> "Make room password protected"; |
472 |
get_roomconfig_text(password) -> "Password"; |
473 |
get_roomconfig_text(anonymous) -> "Make room semianonymous"; |
474 |
get_roomconfig_text(members_only) -> "Make room members-only"; |
475 |
get_roomconfig_text(moderated) -> "Make room moderated"; |
476 |
get_roomconfig_text(members_by_default) -> "Default users as participants"; |
477 |
get_roomconfig_text(allow_change_subj) -> "Allow users to change subject"; |
478 |
get_roomconfig_text(allow_private_messages) -> "Allow users to send private messages"; |
479 |
get_roomconfig_text(allow_query_users) -> "Allow users to query other users"; |
480 |
get_roomconfig_text(allow_user_invites) -> "Allow users to send "; |
481 |
get_roomconfig_text(logging) -> "Enable logging". |