./ct_report/coverage/mod_muc_log.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_muc_log.erl
3 %%% Author : Badlop@process-one.net
4 %%% Purpose : MUC room logging
5 %%% Created : 12 Mar 2006 by Alexey Shchepin <alexey@process-one.net>
6 %%%
7 %%%
8 %%% ejabberd, Copyright (C) 2002-2011 ProcessOne
9 %%%
10 %%% This program is free software; you can redistribute it and/or
11 %%% modify it under the terms of the GNU General Public License as
12 %%% published by the Free Software Foundation; either version 2 of the
13 %%% License, or (at your option) any later version.
14 %%%
15 %%% This program is distributed in the hope that it will be useful,
16 %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17 %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 %%% General Public License for more details.
19 %%%
20 %%% You should have received a copy of the GNU General Public License
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 %%%
24 %%%----------------------------------------------------------------------
25
26 -module(mod_muc_log).
27 -author('badlop@process-one.net').
28
29 -behaviour(gen_server).
30 -behaviour(gen_mod).
31 -behaviour(mongoose_module_metrics).
32
33 %% API
34 -export([start_link/2,
35 start/2,
36 stop/1,
37 supported_features/0,
38 check_access_log/3,
39 add_to_log/5,
40 set_room_occupants/4]).
41
42 %% Config callbacks
43 -export([config_spec/0,
44 process_top_link/1]).
45
46 %% gen_server callbacks
47 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
48 terminate/2, code_change/3]).
49
50 -ignore_xref([start_link/2]).
51
52 -include("mongoose.hrl").
53 -include("jlib.hrl").
54 -include("mod_muc_room.hrl").
55 -include("mongoose_config_spec.hrl").
56
57 -define(T(Text), translate:translate(Lang, Text)).
58 -define(PROCNAME, ejabberd_mod_muc_log).
59
60 -record(room, {jid, title, subject, subject_author, config}).
61 -type room() :: #room{}.
62
63 -type command() :: 'join'
64 | 'kickban'
65 | 'leave'
66 | 'nickchange'
67 | 'room_existence'
68 | 'roomconfig_change'
69 | 'roomconfig_change_enabledlogging'
70 | 'text'.
71
72 -type jid_nick_role() :: {jid:jid(), mod_muc:nick(), mod_muc:role()}.
73 -type jid_nick() :: {jid:jid(), mod_muc:nick()}.
74 -type dir_type() :: 'plain' | 'subdirs'.
75 -type dir_name() :: 'room_jid' | 'room_name'.
76 -type file_format() :: 'html' | 'plaintext'.
77
78 -record(logstate, {host_type :: mongooseim:host_type(),
79 out_dir :: binary(),
80 dir_type :: dir_type(),
81 dir_name :: dir_name(),
82 file_format :: file_format(),
83 css_file :: binary() | false,
84 access,
85 lang :: ejabberd:lang(),
86 timezone,
87 spam_prevention,
88 top_link,
89 occupants = #{} :: #{RoomJID :: binary() => [jid_nick_role()]},
90 room_monitors = #{} :: #{reference() => RoomJID :: binary()}
91 }).
92 -type logstate() :: #logstate{}.
93
94 %%====================================================================
95 %% API
96 %%====================================================================
97
98 %% @doc Starts the server
99 -spec start_link(mongooseim:host_type(), _) -> 'ignore' | {'error', _} | {'ok', pid()}.
100 start_link(Host, Opts) ->
101 27 Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
102 27 gen_server:start_link({local, Proc}, ?MODULE, {Host, Opts}, []).
103
104 -spec start(mongooseim:host_type(), map()) -> {'error', _}
105 | {'ok', 'undefined' | pid()}
106 | {'ok', 'undefined' | pid(), _}.
107 start(Host, Opts) ->
108 27 Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
109 27 ChildSpec =
110 {Proc,
111 {?MODULE, start_link, [Host, Opts]},
112 temporary,
113 1000,
114 worker,
115 [?MODULE]},
116 27 ejabberd_sup:start_child(ChildSpec).
117
118 -spec stop(jid:server()) -> 'ok'
119 | {'error', 'not_found' | 'restarting' | 'running' | 'simple_one_for_one'}.
120 stop(Host) ->
121 27 Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
122 27 gen_server:call(Proc, stop),
123 27 ejabberd_sup:stop_child(Proc).
124
125 -spec supported_features() -> [atom()].
126 supported_features() ->
127
:-(
[dynamic_domains].
128
129 -spec config_spec() -> mongoose_config_spec:config_section().
130 config_spec() ->
131 106 #section{
132 items = #{<<"outdir">> => #option{type = string,
133 validate = dirname},
134 <<"access_log">> => #option{type = atom,
135 validate = access_rule},
136 <<"dirtype">> => #option{type = atom,
137 validate = {enum, [subdirs, plain]}},
138 <<"dirname">> => #option{type = atom,
139 validate = {enum, [room_jid, room_name]}},
140 <<"file_format">> => #option{type = atom,
141 validate = {enum, [html, plaintext]}},
142 <<"css_file">> => #option{type = binary,
143 validate = non_empty},
144 <<"timezone">> => #option{type = atom,
145 validate = {enum, [local, universal]}},
146 <<"top_link">> => top_link_config_spec(),
147 <<"spam_prevention">> => #option{type = boolean}
148 },
149 defaults = defaults()
150 }.
151
152 defaults() ->
153 106 #{<<"outdir">> => "www/muc",
154 <<"access_log">> => muc_admin,
155 <<"dirtype">> => subdirs,
156 <<"dirname">> => room_jid,
157 <<"file_format">> => html,
158 <<"css_file">> => false,
159 <<"timezone">> => local,
160 <<"top_link">> => {"/", "Home"},
161 <<"spam_prevention">> => true}.
162
163 top_link_config_spec() ->
164 106 #section{
165 items = #{<<"target">> => #option{type = string,
166 validate = url},
167 <<"text">> => #option{type = string,
168 validate = non_empty}},
169 required = all,
170 process = fun ?MODULE:process_top_link/1
171 }.
172
173 process_top_link(#{target := Target, text := Text}) ->
174
:-(
{Target, Text}.
175
176 -spec add_to_log(mongooseim:host_type(), Type :: any(), Data :: any(), mod_muc:room(),
177 list()) -> 'ok'.
178 add_to_log(HostType, Type, Data, Room, Opts) ->
179 9 gen_server:cast(get_proc_name(HostType),
180 {add_to_log, Type, Data, Room, Opts}).
181
182
183 -spec check_access_log(mongooseim:host_type(), jid:lserver(), jid:jid()) -> any().
184 check_access_log(HostType, ServerHost, From) ->
185 13 case catch gen_server:call(get_proc_name(HostType),
186 {check_access_log, HostType, ServerHost, From}) of
187 {'EXIT', _Error} ->
188
:-(
deny;
189 Res ->
190 13 Res
191 end.
192
193 -spec set_room_occupants(mongooseim:host_type(), RoomPID :: pid(), RoomJID :: jid:jid(),
194 Occupants :: [mod_muc_room:user()]) -> ok.
195 set_room_occupants(HostType, RoomPID, RoomJID, Occupants) ->
196 2957 gen_server:cast(get_proc_name(HostType), {set_room_occupants, RoomPID, RoomJID, Occupants}).
197
198 %%====================================================================
199 %% gen_server callbacks
200 %%====================================================================
201
202 %%--------------------------------------------------------------------
203 %% Function: init(Args) -> {ok, State} |
204 %% {ok, State, Timeout} |
205 %% ignore |
206 %% {stop, Reason}
207 %% Description: Initiates the server
208 %%--------------------------------------------------------------------
209 -spec init({HostType :: mongooseim:host_type(), map()}) -> {ok, logstate()}.
210 init({HostType, Opts}) ->
211 27 #{
212 access_log := AccessLog,
213 css_file := CSSFile,
214 dirname := DirName,
215 dirtype := DirType,
216 file_format := FileFormat,
217 outdir := OutDir,
218 spam_prevention := NoFollow,
219 timezone := Timezone,
220 top_link := TopLink
221 } = Opts,
222 27 {ok, #logstate{host_type = HostType,
223 out_dir = OutDir,
224 dir_type = DirType,
225 dir_name = DirName,
226 file_format = FileFormat,
227 css_file = CSSFile,
228 access = AccessLog,
229 lang = ?MYLANG,
230 timezone = Timezone,
231 spam_prevention = NoFollow,
232 top_link = TopLink}}.
233
234 %%--------------------------------------------------------------------
235 %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
236 %% {reply, Reply, State, Timeout} |
237 %% {noreply, State} |
238 %% {noreply, State, Timeout} |
239 %% {stop, Reason, Reply, State} |
240 %% {stop, Reason, State}
241 %% Description: Handling call messages
242 %%--------------------------------------------------------------------
243 -spec handle_call('stop'
244 | {'check_access_log', mongooseim:host_type(), 'global' | jid:server(), jid:jid()},
245 From :: any(), logstate()) -> {'reply', 'allow' | 'deny', logstate()}
246 | {'stop', 'normal', 'ok', _}.
247 handle_call({check_access_log, HostType, ServerHost, FromJID}, _From, State) ->
248 13 Reply = acl:match_rule(HostType, ServerHost, State#logstate.access, FromJID),
249 13 {reply, Reply, State};
250 handle_call(stop, _From, State) ->
251 27 {stop, normal, ok, State}.
252
253 %%--------------------------------------------------------------------
254 %% Function: handle_cast(Msg, State) -> {noreply, State} |
255 %% {noreply, State, Timeout} |
256 %% {stop, Reason, State}
257 %% Description: Handling cast messages
258 %%--------------------------------------------------------------------
259 -spec handle_cast
260 ({add_to_log, any(), any(), mod_muc:room(), list()}, logstate()) -> {'noreply', logstate()};
261 ({set_room_occupants, pid(), jid:jid(), [mod_muc_room:user()]}, logstate()) ->
262 {noreply, logstate()}.
263 handle_cast({add_to_log, Type, Data, Room, Opts}, State) ->
264 9 try
265 9 add_to_log2(Type, Data, Room, Opts, State)
266 catch Class:Reason:Stacktrace ->
267 3 ?LOG_ERROR(#{what => muc_add_to_log_failed, room => Room,
268 class => Class, reason => Reason, stacktrace => Stacktrace,
269
:-(
log_type => Type, log_data => Data})
270 end,
271 9 {noreply, State};
272 handle_cast({set_room_occupants, RoomPID, RoomJID, Users}, State) ->
273 2941 #logstate{occupants = OldOccupantsMap, room_monitors = OldMonitors} = State,
274 2941 RoomJIDBin = jid:to_binary(RoomJID),
275 2941 Monitors =
276 case maps:is_key(RoomJIDBin, OldOccupantsMap) of
277 2546 true -> OldMonitors;
278 false ->
279 395 MonitorRef = monitor(process, RoomPID),
280 395 maps:put(MonitorRef, RoomJIDBin, OldMonitors)
281 end,
282 2941 Occupants = [{U#user.jid, U#user.nick, U#user.role} || U <- Users],
283 2941 OccupantsMap = maps:put(RoomJIDBin, Occupants, OldOccupantsMap),
284 2941 {noreply, State#logstate{occupants = OccupantsMap, room_monitors = Monitors}};
285 handle_cast(_Msg, State) ->
286
:-(
{noreply, State}.
287
288 %%--------------------------------------------------------------------
289 %% Function: handle_info(Info, State) -> {noreply, State} |
290 %% {noreply, State, Timeout} |
291 %% {stop, Reason, State}
292 %% Description: Handling all non call/cast messages
293 %%--------------------------------------------------------------------
294 handle_info({'DOWN', MonitorRef, process, Pid, Info}, State) ->
295 395 #logstate{occupants = OldOccupantsMap, room_monitors = OldMonitors} = State,
296 395 case maps:find(MonitorRef, OldMonitors) of
297 error ->
298
:-(
?LOG_WARNING(#{what => muc_unknown_monitor,
299 text => <<"Unknown monitored process is now down">>,
300
:-(
monitor_ref => MonitorRef, monitor_pid => Pid, reason => Info}),
301
:-(
{noreply, State};
302 {ok, RoomJID} ->
303 395 Monitors = maps:remove(MonitorRef, OldMonitors),
304 395 OccupantsMap = maps:remove(RoomJID, OldOccupantsMap),
305 395 {noreply, State#logstate{occupants = OccupantsMap, room_monitors = Monitors}}
306 end;
307 handle_info(_Info, State) ->
308
:-(
{noreply, State}.
309
310 %%--------------------------------------------------------------------
311 %% Function: terminate(Reason, State) -> void()
312 %% Description: This function is called by a gen_server when it is about to
313 %% terminate. It should be the opposite of Module:init/1 and do any necessary
314 %% cleaning up. When it returns, the gen_server terminates with Reason.
315 %% The return value is ignored.
316 %%--------------------------------------------------------------------
317 terminate(_Reason, _State) ->
318 27 ok.
319
320 %%--------------------------------------------------------------------
321 %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
322 %% Description: Convert process state when code is changed
323 %%--------------------------------------------------------------------
324 code_change(_OldVsn, State, _Extra) ->
325
:-(
{ok, State}.
326
327 %%--------------------------------------------------------------------
328 %%% Internal functions
329 %%--------------------------------------------------------------------
330 -spec add_to_log2(command(), {mod_muc:nick(), mod_muc:packet()}, mod_muc:room(),
331 list(), logstate()) -> 'ok'.
332 add_to_log2(text, {Nick, Packet}, Room, Opts, State) ->
333 1 case {xml:get_subtag(Packet, <<"subject">>), xml:get_subtag(Packet, <<"body">>)} of
334 {false, false} ->
335
:-(
ok;
336 {false, SubEl} ->
337 1 Message = {body, xml:get_tag_cdata(SubEl)},
338 1 add_message_to_log(Nick, Message, Room, Opts, State);
339 {SubEl, _} ->
340
:-(
Message = {subject, xml:get_tag_cdata(SubEl)},
341
:-(
add_message_to_log(Nick, Message, Room, Opts, State)
342 end;
343 add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) ->
344 1 add_message_to_log(<<"">>, roomconfig_change, Room, Opts, State);
345 add_to_log2(roomconfig_change_enabledlogging, Occupants, Room, Opts, State) ->
346 1 add_message_to_log(<<"">>, {roomconfig_change, Occupants}, Room, Opts, State);
347 add_to_log2(room_existence, NewStatus, Room, Opts, State) ->
348 4 add_message_to_log(<<"">>, {room_existence, NewStatus}, Room, Opts, State);
349 add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, State) ->
350
:-(
add_message_to_log(NewNick, {nickchange, OldNick}, Room, Opts, State);
351 add_to_log2(join, Nick, Room, Opts, State) ->
352 1 add_message_to_log(Nick, join, Room, Opts, State);
353 add_to_log2(leave, {Nick, Reason}, Room, Opts, State) ->
354 1 case Reason of
355
:-(
<<"">> -> add_message_to_log(Nick, leave, Room, Opts, State);
356 1 _ -> add_message_to_log(Nick, {leave, Reason}, Room, Opts, State)
357 end;
358 add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, State) ->
359
:-(
add_message_to_log(Nick, {kickban, Code, Reason}, Room, Opts, State).
360
361
362 %%----------------------------------------------------------------------
363 %% Core
364
365 -spec build_filename_string(calendar:datetime(), OutDir :: binary(),
366 RoomJID :: jid:literal_jid(), dir_type(), dir_name(), file_format())
367 -> {binary(), binary(), binary()}.
368 build_filename_string(TimeStamp, OutDir, RoomJID, DirType, DirName, FileFormat) ->
369 13 {{Year, Month, Day}, _Time} = TimeStamp,
370
371 %% Directory and file names
372 13 {Dir, Filename, Rel} =
373 case DirType of
374 subdirs ->
375 13 SYear = list_to_binary(lists:flatten(io_lib:format("~4..0w", [Year]))),
376 13 SMonth = list_to_binary(lists:flatten(io_lib:format("~2..0w", [Month]))),
377 13 SDay = list_to_binary(lists:flatten(io_lib:format("~2..0w", [Day]))),
378 13 {filename:join(SYear, SMonth), SDay, <<"../..">>};
379 plain ->
380
:-(
Date = list_to_binary(lists:flatten(
381 io_lib:format("~4..0w-~2..0w-~2..0w",
382 [Year, Month, Day]))),
383
:-(
{<<"">>, Date, <<".">>}
384 end,
385
386 13 RoomString = case DirName of
387 13 room_jid -> RoomJID;
388
:-(
room_name -> get_room_name(RoomJID)
389 end,
390 13 Extension = case FileFormat of
391 13 html -> <<".html">>;
392
:-(
plaintext -> <<".txt">>
393 end,
394 13 Fd = filename:join([OutDir, RoomString, Dir]),
395 13 Fn = filename:join([Fd, <<Filename/binary, Extension/binary>>]),
396 13 Fnrel = filename:join([Rel, Dir, <<Filename/binary, Extension/binary>>]),
397 13 {Fd, Fn, Fnrel}.
398
399
400 -spec get_room_name(jid:literal_jid()) -> mod_muc:room().
401 get_room_name(RoomJID) ->
402
:-(
JID = jid:from_binary(RoomJID),
403
:-(
JID#jid.luser.
404
405
406 %% @doc calculate day before
407 -spec get_timestamp_daydiff(calendar:datetime(), integer()) -> calendar:datetime().
408 get_timestamp_daydiff(TimeStamp, Daydiff) ->
409 4 {Date1, HMS} = TimeStamp,
410 4 Date2 = calendar:gregorian_days_to_date(
411 calendar:date_to_gregorian_days(Date1) + Daydiff),
412 4 {Date2, HMS}.
413
414
415 %% @doc Try to close the previous day log, if it exists
416 -spec close_previous_log(binary(), any(), file_format()) -> 'ok' | {'error', atom()}.
417 close_previous_log(Fn, ImagesDir, FileFormat) ->
418
:-(
case file:read_file_info(Fn) of
419 {ok, _} ->
420
:-(
{ok, F} = file:open(Fn, [append]),
421
:-(
write_last_lines(F, ImagesDir, FileFormat),
422
:-(
file:close(F);
423
:-(
_ -> ok
424 end.
425
426
427 -spec write_last_lines(file:io_device(), binary(), file_format()) -> 'ok'.
428 write_last_lines(_, _, plaintext) ->
429
:-(
ok;
430 write_last_lines(F, ImagesDir, _FileFormat) ->
431
:-(
fw(F, <<"<div class=\"legend\">">>),
432
:-(
fw(F, <<" <a href=\"http://www.ejabberd.im\"><img style=\"border:0\" src=\"", ImagesDir/binary,
433 "/powered-by-ejabberd.png\" alt=\"Powered by ejabberd\"/></a>">>),
434
:-(
fw(F, <<" <a href=\"http://www.erlang.org/\"><img style=\"border:0\" src=\"", ImagesDir/binary,
435 "/powered-by-erlang.png\" alt=\"Powered by Erlang\"/></a>">>),
436
:-(
fw(F, <<"<span class=\"w3c\">">>),
437
:-(
fw(F, <<" <a href=\"http://validator.w3.org/check?uri=referer\">"
438 "<img style=\"border:0;width:88px;height:31px\" src=\"", ImagesDir/binary,
439 "/valid-xhtml10.png\" alt=\"Valid XHTML 1.0 Transitional\" /></a>">>),
440
:-(
fw(F, <<" <a href=\"http://jigsaw.w3.org/css-validator/\">"
441 "<img style=\"border:0;width:88px;height:31px\" src=\"", ImagesDir/binary,
442 "/vcss.png\" alt=\"Valid CSS!\"/></a>">>),
443
:-(
fw(F, <<"</span></div></body></html>">>).
444
445
446 -spec add_message_to_log(mod_muc:nick(), Message :: atom() | tuple(),
447 RoomJID :: jid:simple_jid() | jid:jid(), Opts :: list(), State :: logstate()) -> ok.
448 add_message_to_log(Nick1, Message, RoomJID, Opts, State) ->
449 9 #logstate{out_dir = OutDir,
450 dir_type = DirType,
451 dir_name = DirName,
452 file_format = FileFormat,
453 css_file = CSSFile,
454 lang = Lang,
455 timezone = Timezone,
456 spam_prevention = NoFollow,
457 top_link = TopLink,
458 occupants = OccupantsMap} = State,
459 9 Room = get_room_info(RoomJID, Opts),
460 9 Nick = htmlize(Nick1, FileFormat),
461 9 Nick2 = htmlize(<<"<", Nick1/binary, ">">>, FileFormat),
462 9 Now = erlang:timestamp(),
463 9 TimeStamp = case Timezone of
464 9 local -> calendar:now_to_local_time(Now);
465
:-(
universal -> calendar:now_to_universal_time(Now)
466 end,
467 9 {Fd, Fn, _Dir} = build_filename_string(TimeStamp, OutDir, Room#room.jid,
468 DirType, DirName, FileFormat),
469 9 {Date, Time} = TimeStamp,
470
471 %% Open file, create if it does not exist, create parent dirs if needed
472 9 case file:read_file_info(Fn) of
473 {ok, _} ->
474 7 {ok, F} = file:open(Fn, [append]);
475 {error, enoent} ->
476 2 make_dir_rec(Fd),
477 2 {ok, F} = file:open(Fn, [append]),
478 2 Datestring = get_dateweek(Date, Lang),
479
480 2 TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1),
481 2 {_FdYesterday, FnYesterday, DatePrev} =
482 build_filename_string(
483 TimeStampYesterday, OutDir, Room#room.jid, DirType, DirName, FileFormat),
484
485 2 TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1),
486 2 {_FdTomorrow, _FnTomorrow, DateNext} =
487 build_filename_string(
488 TimeStampTomorrow, OutDir, Room#room.jid, DirType, DirName, FileFormat),
489
490 2 HourOffset = calc_hour_offset(TimeStamp),
491 2 put_header(F, Room, Datestring, CSSFile, Lang,
492 HourOffset, DatePrev, DateNext, TopLink, FileFormat, OccupantsMap),
493
494
:-(
ImagesDir = <<OutDir/binary, "images">>,
495
:-(
file:make_dir(ImagesDir),
496
:-(
create_image_files(ImagesDir),
497
:-(
ImagesUrl = case DirType of
498
:-(
subdirs -> <<"../../../images">>;
499
:-(
plain -> <<"../images">>
500 end,
501
:-(
close_previous_log(FnYesterday, ImagesUrl, FileFormat)
502 end,
503
504 %% Build message
505 7 Text
506 = case Message of
507 roomconfig_change ->
508 1 RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
509 1 put_room_config(F, RoomConfig, Lang, FileFormat),
510 1 <<"<font class=\"mrcm\">", (?T(<<"Chatroom configuration modified">>))/binary,
511 "</font><br/>">>;
512 {roomconfig_change, Occupants} ->
513
:-(
RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
514
:-(
put_room_config(F, RoomConfig, Lang, FileFormat),
515
:-(
RoomOccupants = roomoccupants_to_binary(Occupants, FileFormat),
516
:-(
put_room_occupants(F, RoomOccupants, Lang, FileFormat),
517
:-(
<<"<font class=\"mrcm\">", (?T(<<"Chatroom configuration modified">>))/binary,
518 "</font><br/>">>;
519 join ->
520 1 <<"<font class=\"mj\">", Nick/binary, " ", (?T(<<"joins the room">>))/binary,
521 "</font><br/>">>;
522 leave ->
523
:-(
<<"<font class=\"mj\">", Nick/binary, " ", (?T(<<"leaves the room">>))/binary,
524 "</font><br/>">>;
525 {leave, Reason} ->
526 1 <<"<font class=\"ml\">", Nick/binary, " ", (?T(<<"leaves the room">>))/binary, ": ",
527 (htmlize(Reason, NoFollow, FileFormat))/binary, ": ~s</font><br/>">>;
528 {kickban, "301", ""} ->
529
:-(
<<"<font class=\"mb\">", Nick/binary, " ", (?T(<<"has been banned">>))/binary,
530 "</font><br/>">>;
531 {kickban, "301", Reason} ->
532
:-(
<<"<font class=\"mb\">", Nick/binary, " ", (?T(<<"has been banned">>))/binary, ": ",
533 (htmlize(Reason, FileFormat))/binary, "</font><br/>">>;
534 {kickban, "307", ""} ->
535
:-(
<<"<font class=\"mk\">", Nick/binary, " ", (?T(<<"has been kicked">>))/binary,
536 "</font><br/>">>;
537 {kickban, "307", Reason} ->
538
:-(
<<"<font class=\"mk\">", Nick/binary, " ", (?T(<<"has been kicked">>))/binary, ": ",
539 (htmlize(Reason, FileFormat))/binary, "</font><br/>">>;
540 {kickban, "321", ""} ->
541
:-(
<<"<font class=\"mk\">", Nick/binary, " ",
542 (?T(<<"has been kicked because of an affiliation change">>))/binary,
543 "</font><br/>">>;
544 {kickban, "322", ""} ->
545
:-(
<<"<font class=\"mk\">", Nick/binary, " ",
546 (?T(<<"has been kicked because the room has been changed to"
547 " members-only">>))/binary, "</font><br/>">>;
548 {kickban, "332", ""} ->
549
:-(
<<"<font class=\"mk\">", Nick/binary, " ",
550 (?T(<<"has been kicked because of a system shutdown">>))/binary, "</font><br/>">>;
551 {nickchange, OldNick} ->
552
:-(
<<"<font class=\"mnc\">", (htmlize(OldNick, FileFormat))/binary, " ",
553 (?T(<<"is now known as">>))/binary, " ", Nick/binary, "</font><br/>">>;
554 {subject, T} ->
555
:-(
<<"<font class=\"msc\">", Nick/binary, (?T(<<" has set the subject to: ">>))/binary,
556 (htmlize(T, NoFollow, FileFormat))/binary, "</font><br/>">>;
557 {body, T} ->
558 1 case {re:run(T, <<"^/me\s">>, [{capture, none}]), Nick} of
559 {_, ""} ->
560
:-(
<<"<font class=\"msm\">", (htmlize(T, NoFollow, FileFormat))/binary,
561 "</font><br/>">>;
562 {match, _} ->
563 %% Delete "/me " from the beginning.
564
:-(
<<_Pref:32, SubStr/binary>> = htmlize(T, FileFormat),
565
:-(
<<"<font class=\"mne\">", Nick/binary, " ", SubStr/binary, "</font><br/>">>;
566 {nomatch, _} ->
567 1 <<"<font class=\"mn\">", Nick2/binary, "</font> ",
568 (htmlize(T, NoFollow, FileFormat))/binary, "<br/>">>
569 end;
570 {room_existence, RoomNewExistence} ->
571 3 <<"<font class=\"mrcm\">", (get_room_existence_string(RoomNewExistence, Lang))/binary,
572 "</font><br/>">>
573 end,
574 7 {Hour, Minute, Second} = Time,
575 7 STime = lists:flatten(
576 io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second])),
577 7 {_, _, Microsecs} = Now,
578 7 STimeUnique = list_to_binary(lists:flatten(io_lib:format("~s.~w", [STime, Microsecs]))),
579
580 %% Write message
581 7 fw(F, <<"<a id=\"", STimeUnique/binary, "\" name=\"", STimeUnique/binary,
582 "\" href=\"#", STimeUnique/binary, "\" class=\"ts\">[",
583 (list_to_binary(STime))/binary, "]</a> ", Text/binary>>, FileFormat),
584
585 %% Close file
586 6 file:close(F),
587 6 ok.
588
589
590 %%----------------------------------------------------------------------
591 %% Utilities
592
593 -spec get_room_existence_string('created' | 'destroyed' | 'started' | 'stopped',
594 binary()) -> binary().
595
:-(
get_room_existence_string(created, Lang) -> ?T(<<"Chatroom is created">>);
596 1 get_room_existence_string(destroyed, Lang) -> ?T(<<"Chatroom is destroyed">>);
597 1 get_room_existence_string(started, Lang) -> ?T(<<"Chatroom is started">>);
598 1 get_room_existence_string(stopped, Lang) -> ?T(<<"Chatroom is stopped">>).
599
600
601 -spec get_dateweek(calendar:date(), binary()) -> binary().
602 get_dateweek(Date, Lang) ->
603 2 Weekday = case calendar:day_of_the_week(Date) of
604
:-(
1 -> ?T(<<"Monday">>);
605
:-(
2 -> ?T(<<"Tuesday">>);
606
:-(
3 -> ?T(<<"Wednesday">>);
607 2 4 -> ?T(<<"Thursday">>);
608
:-(
5 -> ?T(<<"Friday">>);
609
:-(
6 -> ?T(<<"Saturday">>);
610
:-(
7 -> ?T(<<"Sunday">>)
611 end,
612 2 {Y, M, D} = Date,
613 2 Month = case M of
614
:-(
1 -> ?T(<<"January">>);
615
:-(
2 -> ?T(<<"February">>);
616 2 3 -> ?T(<<"March">>);
617
:-(
4 -> ?T(<<"April">>);
618
:-(
5 -> ?T(<<"May">>);
619
:-(
6 -> ?T(<<"June">>);
620
:-(
7 -> ?T(<<"July">>);
621
:-(
8 -> ?T(<<"August">>);
622
:-(
9 -> ?T(<<"September">>);
623
:-(
10 -> ?T(<<"October">>);
624
:-(
11 -> ?T(<<"November">>);
625
:-(
12 -> ?T(<<"December">>)
626 end,
627 2 case Lang of
628 2 <<"en">> -> list_to_binary(
629 lists:flatten(io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y])));
630
:-(
<<"es">> -> list_to_binary(
631 lists:flatten(io_lib:format("~s ~w de ~s de ~w", [Weekday, D, Month, Y])));
632
:-(
_ -> list_to_binary(
633 lists:flatten(io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y])))
634 end.
635
636
637 -spec make_dir_rec(binary()) -> 'ok' | {'error', atom()}.
638 make_dir_rec(Dir) ->
639 9 case file:read_file_info(Dir) of
640 {ok, _} ->
641 2 ok;
642 {error, enoent} ->
643 7 DirS = filename:split(Dir),
644 7 DirR = lists:sublist(DirS, length(DirS)-1),
645 7 make_dir_rec(filename:join(DirR)),
646 7 file:make_dir(Dir)
647 end.
648
649
650 %% {ok, F1}=file:open("valid-xhtml10.png", [read]).
651 %% {ok, F1b}=file:read(F1, 1000000).
652 %% c("../../ejabberd/src/jlib.erl").
653 %% jlib:encode_base64(F1b).
654
655 image_base64(<<"powered-by-erlang.png">>) ->
656
:-(
<<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoAAADN0lEQVRo3u1a"
657 "P0waURz+rjGRRQ+nUyRCYmJyDPTapDARaSIbTUjt1gVSh8ZW69aBAR0cWLSx"
658 "CXWp59LR1jbdqKnGxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu"
659 "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOHo8HkiQxDBXEOjg9"
660 "PcHc3BxuUSqsI8jR0REAUFGsCCoKFYWCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJ"
661 "ut9hMQlvdNbUhKWshLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm"
662 "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1MdG8rSKKXlJ7lHr"
663 "fjouCut2IrC82BDPbe/gc+xlXez7KxEz63H4lmIN473Rh8Si1BKhRY6aEJI8"
664 "pLmbjSPN0xOnBBILmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG"
665 "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFOTYznzpPO+IQi+IK"
666 "+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN3Olvk4ge/IU98spne+FfGtlslm16nn"
667 "a8fduntfDscoVjGJqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5"
668 "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizgwwu5SLK8jllu1t9"
669 "ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y1m+hSj13WjqOiJKK0qpXKhSFxJAYBv"
670 "KYaZ9TjWRu4SiWi2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g"
671 "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJvHsdL+utNYrm2uf"
672 "ZDVZSkzPKaQkW8kthpyS297BvRdRzR6DdTurJbPy9Ov1K6xr3HBPQuIMowR3"
673 "asegUyDuU9SuUG+dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q"
674 "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafIYrizbfESoB+dd9/"
675 "KF/d/wX3cJvREzl1vAAAAABJRU5ErkJggg==">>;
676 image_base64(<<"valid-xhtml10.png">>) ->
677
:-(
<<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEAAACiFBMVEUAAADe"
678 "5+fOezmtra3ejEKlhELvvWO9WlrehELOe3vepaWclHvetVLGc3PerVKcCAj3"
679 "vVqUjHOUe1JjlL0xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY"
680 "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoICACEazEhGAgIAACE"
681 "YzFra2utjELWcznGnEr/7+9jY2POazHOYzGta2NShLVrlL05OUqctdacCADG"
682 "a2ucAADGpVqUtc61ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe"
683 "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7/fGxsa9pVqEOSkp"
684 "Y5xznL29tZxahLXOpVr/99ZrY1L/79ZjUiljSikAOYTvxmMAMYScezmchFqU"
685 "czGtlFp7c2utjFqUlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC"
686 "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1mtCMRhzY1L/zmvn"
687 "vVpSQiHOpVJrUinntVr3zmOEc1L3xmNaWlq1nFo5QkrGWim1lFoISpRSUlK1"
688 "zt4hWpwASoz///////8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3"
689 "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUlHtKORh7a1IxIRCU"
690 "jHtaSiHWrVIpIQhzWinvvVpaQiH/1mPWpVKMe1L/zmP/xmNrUiGErc4YGBj/"
691 "73PG1ucQWpT/53O9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS"
692 "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////iP9sSf//dP//////"
693 "//////////////////////////////////////////8M////////////ef//"
694 "////////////////////////////////////////////////////////////"
695 "//////////////////////9d////////////////////////////////////"
696 "AP//////////////CP//RP//////////////////////////////////////"
697 "//////////////////////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba"
698 "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpIvGQGzokvTTA85VH"
699 "KTpbRoeJnPno/p1+M7t3txj20e/Nzu7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSA"
700 "NeBDxVmxZZSwEUYkGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb"
701 "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6VbdtBblesOs+MjmEmz"
702 "JKNIJWFEfEQTCWNPFKvcKEymjLO1b8bwYQd1hCiiDCl5KsrDCIlhj4fSuvcp"
703 "fSpgJmyv6dzeZv+nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk"
704 "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDNE9uzA1YD/5MhnzW"
705 "1KRqF7mYG8jFYXLcfLpjOe2LA0fuGqQrQHl10sdK0sFcFSOSlzF0BgXQH9h3"
706 "QZDBI0ccNEhftjXuippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7"
707 "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woWSRJ0m2ewhvAOUiY"
708 "YtZtaZL0CZZmtmVOQttLfr/dbveLZodrfrL7W75wG/JjqkQxoNTtNsTKELQp"
709 "QL6/D5loaSmyTT8TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+"
710 "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qqmj5bHqVZlJKZIRk"
711 "1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p1V76DeIs0MX+s3GxRlA1vtw83KhgNp"
712 "hc1nyErLO5zcvbOsrq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N"
713 "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4Fsvjmuv662jsWxPZ"
714 "x2xg890bYMYnTgya7bjmCiEY0qgJ0vMF3c+NoFdPyzxz6V3Uxs3AOWCDchRv"
715 "OsQtBrbFsrT2fhHEc7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1"
716 "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3e4HFmYPDl/thYcR"
717 "Vi8qPEoXVUWBl6FTBFTtnqmKKg5wnlF4wZ1yeLv7TiwXKektE+iDBNicWEyL"
718 "pnFhfDkpJc3q2khSPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx"
719 "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdiMOlEOJHw1jOQ4sW"
720 "t5rQopRkXZhMEi7pmeDCVWBlfUKwhMZ7rsF6elKsvbwiKxgxIdewa3ErsaYo"
721 "mCVZFYJb0GUu3JqGUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+"
722 "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx17XhBuDCaOerbt5"
723 "38erta7TFktLvdHghZcCbcPQO33zIJG9kxF5hoVXnzTzRz0r5js8oTj6uyPk"
724 "GRf346HOLcasgFexueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl"
725 "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1juvBSWNFmNWGZy8Lv"
726 "zUl5PN4JCwyNDzbQ0aAj4Zrjz0FatGJJYhvq4j7mGSpvytGFlZtHf2C4o/28"
727 "Zu8z7wo7eYPfXysnF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO"
728 "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+faCSJPJ1bmy6Thyvp"
729 "ivBmYWgjxPDPAp7JTemY/yGKFEiRt/jG/2P79s8KCwoLCgoLC/khUBA5F0Sf"
730 "QZ+RYfpNE/4Xosmq7jsZAJsAAAAASUVORK5CYII=">>;
731 image_base64(<<"vcss.png">>) ->
732
:-(
<<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSAAABKVBMVEUAAAAj"
733 "Ix8MR51ZVUqAdlmdnZ3ejEWLDAuNjY1kiMG0n2d9fX19Ghfrp1FtbW3y39+3"
734 "Ph6lIRNdXV2qJBFcVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd"
735 "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T09RRDwsJBG+vr73"
736 "wV6fkG6eCQRFcLSurq6/X1+ht9nXfz5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5t"
737 "WCsmWajsz8+Sq9NMPh4hVaY8MRj///////////////////////9MTEyOp9Lu"
738 "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn82dfz7/zGafDw+f"
739 "Dw+zRSlzlMcMDAyNcji1tbXf5vIcFgvATJOjAAAAY3RSTlP/8///////////"
740 "//////8A//////P/////ov//8//////////////z///T//////////+i////"
741 "//////////8w/////6IA/xAgMP//////////8/////////8w0/////////+z"
742 "ehebAAACkUlEQVR42u2VfVPTQBDG19VqC6LY+lKrRIxFQaFSBPuSvhBPF8SI"
743 "UZK2J5Yav/+HcO8uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6"
744 "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE24pFtBtzG1Q4AoD5"
745 "zvw5pEDcJvIQV/TE3/l+H9GnNJwcdABS5wAbFQLMqI98/UReoAaOTlaJsp0z"
746 "aHx7LwZvY0BUR2xpWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7"
747 "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45OeuPgm4+Xjtv7xm4N"
748 "7PMV4C61+Mrz3H2WImm3ATiWrAiwZRWcUA5Ej4dgIEMxDv6yxHHcNuAutnjv"
749 "2HZ1NeuycoVPh0mwC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW"
750 "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyGqidQSw1eqtJcA8c"
751 "Zq4zXqhPuCBYE1fKJjh/5X6MwRm9c2xf7WVdLf5oSdt64esVIwVAKC1HJ2ol"
752 "i8vj3L0YzC4zjkMagt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD"
753 "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz54uUyet+hBM7BXX"
754 "DS8Xc5+8Gmmbu1xwSoGIokA3oTptQecQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo"
755 "+W9xC8oWpugVCXLuwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua"
756 "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/AV5htgaQ9SeRAAAA"
757 "AElFTkSuQmCC">>;
758 image_base64(<<"powered-by-ejabberd.png">>) ->
759
:-(
<<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaAAAAw1BMVEUAAAAj"
760 "BgYtBAM5AwFCAAAYGAJNAABcAABIDQ5qAAAoJRV7AACFAAAoKSdJHByLAAAw"
761 "Lwk1NQA1MzFJKyo4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb"
762 "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEgAB3dnd4d2+OjACD"
763 "hYKcmACJi4iQkpWspgCYmJm5swCmqazEwACwsbS4ub3X0QLExsPLyszW1Nnc"
764 "3ODm5ugMBwAWAwPHm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ"
765 "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VBkCMAAACcElEQVRI"
766 "x72WjXKiMBSFQalIFbNiy1pdrJZaRVYR5deGwPs/VRNBSBB2OjvQO0oYjPfj"
767 "5J6bCcdx8i2UldxKcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv"
768 "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJlRpsUei0JF6Qi+O"
769 "HyGK7ijXxPklHe/umIllim3iUBMJDIEULxxPP0TVWhhKJoN9fUpdmQLteV8a"
770 "DgEAg9gIcTjL4F4L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF"
771 "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7rovDjMpIHHGYeid"
772 "Xn4TQcaxMYqP3RV3C8oCl2WvrlSPaNpGZadRnmPGCk8ylM2okAJ4i9TEe1Ke"
773 "rsXxSl6jUt5uayiIodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs"
774 "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzvBo1I2tLmuL69kSi"
775 "tAweyHKN9b3leDfQMnu3nIIKWfmXnqGVKedJT6QpICbJvf2f8aOsvn68v+k7"
776 "/cwUQdPoxaMoRTnKFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG"
777 "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCTLav3VrqbPvP3vdd"
778 "uGEhQJzilncbpSA4F3vsihErO+dayv/sY5/yRE0GDEXCu2VoNiMlo5i+P2Kl"
779 "gMEvTNk2eYa5XEyh12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA"
780 "AElFTkSuQmCC">>.
781
782
783 -spec create_image_files(<<_:8, _:_*8>>) -> 'ok'.
784 create_image_files(ImagesDir) ->
785
:-(
Filenames = [<<"powered-by-ejabberd.png">>,
786 <<"powered-by-erlang.png">>,
787 <<"valid-xhtml10.png">>,
788 <<"vcss.png">>
789 ],
790
:-(
lists:foreach(
791 fun(Filename) ->
792
:-(
FilenameFull = filename:join([ImagesDir, Filename]),
793
:-(
{ok, F} = file:open(FilenameFull, [write]),
794
:-(
Image = jlib:decode_base64(image_base64(Filename)),
795
:-(
io:format(F, "~s", [Image]),
796
:-(
file:close(F)
797 end,
798 Filenames),
799
:-(
ok.
800
801
802 -spec fw(file:io_device(), binary()) -> 'ok'.
803 92 fw(F, S) -> fw(F, S, html).
804
805
806 -spec fw(file:io_device(), binary(), file_format()) -> 'ok'.
807 fw(F, S, FileFormat) ->
808 99 S1 = <<S/binary, "~n">>,
809 99 S2 = case FileFormat of
810 html ->
811 99 S1;
812 plaintext ->
813
:-(
re:replace(S1, <<"<[^>]*>">>, <<"">>, [global, {return, binary}])
814 end,
815 99 io:format(F, S2, []).
816
817
818 -spec put_header(file:io_device(), Room :: room(), Date :: binary(),
819 CSSFile :: false | binary(), Lang :: ejabberd:lang(), HourOffset :: integer(),
820 DatePrev :: binary(), DateNext :: binary(), TopLink :: tuple(),
821 file_format(), OccupantsMap :: #{binary() => [jid_nick_role()]}) -> 'ok'.
822 put_header(_, _, _, _, _, _, _, _, _, plaintext, _) ->
823
:-(
ok;
824 put_header(F, Room, Date, CSSFile, Lang, HourOffset, DatePrev, DateNext, TopLink, FileFormat,
825 OccupantsMap) ->
826 2 fw(F, <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
827 " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>),
828 2 fw(F, <<"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"",
829 Lang/binary, "\" lang=\"", Lang/binary, "\">">>),
830 2 fw(F, <<"<head>">>),
831 2 fw(F, <<"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />">>),
832 2 fw(F, <<"<title>", (htmlize(Room#room.title))/binary, " - ", Date/binary, "</title>">>),
833 2 put_header_css(F, CSSFile),
834 2 put_header_script(F),
835 2 fw(F, <<"</head>">>),
836 2 fw(F, <<"<body>">>),
837 2 {TopUrl, TopText} = TopLink,
838 2 fw(F, <<"<div style=\"text-align: right;\"><a style=\"color: #AAAAAA; font-family: monospace;"
839 " text-decoration: none; font-weight: bold;\" href=\"", TopUrl/binary, "\">",
840 TopText/binary, "</a></div>">>),
841
:-(
fw(F, <<"<div class=\"roomtitle\">", (htmlize(Room#room.title))/binary, "</div>">>),
842
:-(
fw(F, <<"<a class=\"roomjid\" href=\"xmpp:", (Room#room.jid)/binary, "?join\">",
843 (Room#room.jid)/binary, "</a>">>),
844
:-(
fw(F, <<"<div class=\"logdate\">", Date/binary, "<span class=\"w3c\"><a class=\"nav\" href=\"",
845 DatePrev/binary, "\">&lt;</a> <a class=\"nav\" href=\".\/\">^</a>"
846 " <a class=\"nav\" href=\"", DateNext/binary, "\">&gt;</a></span></div>">>),
847
:-(
case {htmlize(Room#room.subject_author), htmlize(Room#room.subject)} of
848 {<<"">>, <<"">>} ->
849
:-(
ok;
850
:-(
{SuA, Su} -> fw(F, <<"<div class=\"roomsubject\">", SuA/binary,
851 (?T(<<" has set the subject to: ">>))/binary, Su/binary, "</div>">>)
852 end,
853
:-(
RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
854
:-(
put_room_config(F, RoomConfig, Lang, FileFormat),
855
:-(
Occupants = maps:get(Room#room.jid, OccupantsMap, []),
856
:-(
RoomOccupants = roomoccupants_to_binary(Occupants, FileFormat),
857
:-(
put_room_occupants(F, RoomOccupants, Lang, FileFormat),
858
:-(
TimeOffsetBin = case HourOffset<0 of
859
:-(
true -> list_to_binary(lists:flatten(io_lib:format("~p", [HourOffset])));
860
:-(
false -> list_to_binary(lists:flatten(io_lib:format("+~p", [HourOffset])))
861 end,
862
:-(
fw(F, <<"<br/><a class=\"ts\">GMT", TimeOffsetBin/binary, "</a><br/>">>).
863
864
865 -spec put_header_css(file:io_device(), 'false' | binary()) -> 'ok'.
866 put_header_css(F, false) ->
867 2 fw(F, <<"<style type=\"text/css\">">>),
868 2 fw(F, <<"<!--">>),
869 2 fw(F, <<".ts {color: #AAAAAA; text-decoration: none;}">>),
870 2 fw(F, <<".mrcm {color: #009900; font-style: italic; font-weight: bold;}">>),
871 2 fw(F, <<".msc {color: #009900; font-style: italic; font-weight: bold;}">>),
872 2 fw(F, <<".msm {color: #000099; font-style: italic; font-weight: bold;}">>),
873 2 fw(F, <<".mj {color: #009900; font-style: italic;}">>),
874 2 fw(F, <<".ml {color: #009900; font-style: italic;}">>),
875 2 fw(F, <<".mk {color: #009900; font-style: italic;}">>),
876 2 fw(F, <<".mb {color: #009900; font-style: italic;}">>),
877 2 fw(F, <<".mnc {color: #009900; font-style: italic;}">>),
878 2 fw(F, <<".mn {color: #0000AA;}">>),
879 2 fw(F, <<".mne {color: #AA0099;}">>),
880 2 fw(F, <<"a.nav {color: #AAAAAA; font-family: monospace; letter-spacing: 3px;"
881 " text-decoration: none;}">>),
882 2 fw(F, <<"div.roomtitle {border-bottom: #224466 solid 3pt; margin-left: 20pt;}">>),
883 2 fw(F, <<"div.roomtitle {color: #336699; font-size: 24px; font-weight: bold;"
884 " font-family: sans-serif; letter-spacing: 3px; text-decoration: none;}">>),
885 2 fw(F, <<"a.roomjid {color: #336699; font-size: 24px; font-weight: bold;"
886 " font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt;"
887 " text-decoration: none;}">>),
888 2 fw(F, <<"div.logdate {color: #663399; font-size: 20px; font-weight: bold;"
889 " font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt;"
890 " margin-left:80pt; margin-top:20px;}">>),
891 2 fw(F, <<"div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif;"
892 " margin-left: 80pt; margin-bottom: 10px;}">>),
893 2 fw(F, <<"div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 50%;"
894 " text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699;"
895 " border-right: 4px solid #336699;}">>),
896 2 fw(F, <<"div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px;}">>),
897 2 fw(F, <<"div.rcos {padding-right: 10px;}">>),
898 2 fw(F, <<"div.rcoe {color: green;}">>),
899 2 fw(F, <<"div.rcod {color: red;}">>),
900 2 fw(F, <<"div.rcoe:after {content: \": v\";}">>),
901 2 fw(F, <<"div.rcod:after {content: \": x\";}">>),
902 2 fw(F, <<"div.rcot:after {}">>),
903 2 fw(F, <<".legend {width: 100%; margin-top: 30px; border-top: #224466 solid 1pt;"
904 " padding: 10px 0px 10px 0px; text-align: left;"
905 " font-family: monospace; letter-spacing: 2px;}">>),
906 2 fw(F, <<".w3c {position: absolute; right: 10px; width: 60%; text-align: right;"
907 " font-family: monospace; letter-spacing: 1px;}">>),
908 2 fw(F, <<"//-->">>),
909 2 fw(F, <<"</style>">>);
910 put_header_css(F, CSSFile) ->
911
:-(
fw(F, <<"<link rel=\"stylesheet\" type=\"text/css\" href=\"",
912 CSSFile/binary, "\" media=\"all\">">>).
913
914 put_header_script(F) ->
915 2 fw(F, <<"<script type=\"text/javascript\">">>),
916 2 fw(F, <<"function sh(e) // Show/Hide an element">>),
917 2 fw(F, <<"{if(document.getElementById(e).style.display=='none')">>),
918 2 fw(F, <<"{document.getElementById(e).style.display='block';}">>),
919 2 fw(F, <<"else {document.getElementById(e).style.display='none';}}">>),
920 2 fw(F, <<"</script>">>).
921
922
923 -spec put_room_config(file:io_device(), any(), ejabberd:lang(),
924 file_format()) -> 'ok'.
925 put_room_config(_F, _RoomConfig, _Lang, plaintext) ->
926
:-(
ok;
927 put_room_config(F, RoomConfig, Lang, _FileFormat) ->
928 1 {Now1, Now2, Now3} = erlang:timestamp(),
929 1 NowBin = list_to_binary(lists:flatten(io_lib:format("~p~p~p", [Now1, Now2, Now3]))),
930 1 fw(F, <<"<div class=\"rc\">">>),
931 1 fw(F, <<"<div class=\"rct\" onclick=\"sh('a", NowBin/binary, "');return false;\">",
932 (?T(<<"Room Configuration">>))/binary, "</div>">>),
933 1 fw(F, <<"<div class=\"rcos\" id=\"a", NowBin/binary, "\" style=\"display: none;\" ><br/>",
934 RoomConfig/binary, "</div>">>),
935 1 fw(F, <<"</div>">>).
936
937
938 -spec put_room_occupants(file:io_device(), any(), ejabberd:lang(),
939 file_format()) -> 'ok'.
940 put_room_occupants(_F, _RoomOccupants, _Lang, plaintext) ->
941
:-(
ok;
942 put_room_occupants(F, RoomOccupants, Lang, _FileFormat) ->
943
:-(
{Now1, Now2, Now3} = erlang:timestamp(),
944
:-(
NowBin = list_to_binary(lists:flatten(io_lib:format("~p~p~p", [Now1, Now2, Now3]))),
945
:-(
fw(F, <<"<div class=\"rc\">">>),
946
:-(
fw(F, <<"<div class=\"rct\" onclick=\"sh('o", NowBin/binary, "');return false;\">",
947 (?T(<<"Room Occupants">>))/binary, "</div>">>),
948
:-(
fw(F, <<"<div class=\"rcos\" id=\"o", NowBin/binary, "\" style=\"display: none;\" ><br/>",
949 RoomOccupants/binary, "</div>">>),
950
:-(
fw(F, <<"</div>">>).
951
952
953 %% @doc htmlize
954 %% The default behaviour is to ignore the nofollow spam prevention on links
955 %% (NoFollow=false)
956 htmlize(S1) ->
957 2 htmlize(S1, html).
958
959 htmlize(S1, FileFormat) ->
960 23 htmlize(S1, false, FileFormat).
961
962
963 %% @doc The NoFollow parameter tell if the spam prevention should be applied to
964 %% the link found. true means 'apply nofollow on links'.
965 htmlize(S1, _NoFollow, plaintext) ->
966
:-(
ReplacementRules =
967 [{<<"<">>, <<"[">>},
968 {<<">">>, <<"]">>}],
969
:-(
lists:foldl(fun({RegExp, Replace}, Acc) ->
970
:-(
re:replace(Acc, RegExp, Replace, [global, {return, binary}])
971 end, S1, ReplacementRules);
972 htmlize(S1, NoFollow, _FileFormat) ->
973 25 S2List = binary:split(S1, <<"\n">>, [global]),
974 25 lists:foldl(
975 fun(Si, Res) ->
976 25 Si2 = htmlize2(Si, NoFollow),
977 25 case Res of
978 25 <<"">> -> Si2;
979
:-(
_ -> <<Res/binary, "<br/>", Si2/binary>>
980 end
981 end,
982 <<"">>,
983 S2List).
984
985 htmlize2(S1, NoFollow) ->
986 25 ReplacementRules =
987 [{<<"\\&">>, <<"\\&amp;">>},
988 {<<"<">>, <<"\\&lt;">>},
989 {<<">">>, <<"\\&gt;">>},
990 {<<"((http|https|ftp)://|(mailto|xmpp):)[^] )\'\"}]+">>, link_regexp(NoFollow)},
991 {<<" ">>, <<"\\&nbsp;\\&nbsp;">>},
992 {<<"\\t">>, <<"\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;">>},
993 {<<226, 128, 174>>, <<"[RLO]">>}],
994 25 lists:foldl(fun({RegExp, Replace}, Acc) ->
995 175 re:replace(Acc, RegExp, Replace, [global, {return, binary}])
996 end, S1, ReplacementRules).
997
998 %% @doc Regexp link. Add the nofollow rel attribute when required
999 link_regexp(false) ->
1000 23 <<"<a href=\"&\">&</a>">>;
1001 link_regexp(true) ->
1002 2 <<"<a href=\"&\" rel=\"nofollow\">&</a>">>.
1003
1004
1005 get_room_info(RoomJID, Opts) ->
1006 9 Title =
1007 case lists:keysearch(title, 1, Opts) of
1008 9 {value, {_, T}} -> T;
1009
:-(
false -> <<"">>
1010 end,
1011 9 Subject =
1012 case lists:keysearch(subject, 1, Opts) of
1013 9 {value, {_, S}} -> S;
1014
:-(
false -> <<"">>
1015 end,
1016 9 SubjectAuthor =
1017 case lists:keysearch(subject_author, 1, Opts) of
1018 9 {value, {_, SA}} -> SA;
1019
:-(
false -> <<"">>
1020 end,
1021 9 #room{jid = RoomJID,
1022 title = Title,
1023 subject = Subject,
1024 subject_author = SubjectAuthor,
1025 config = Opts
1026 }.
1027
1028
1029 -spec roomconfig_to_binary(list(), ejabberd:lang(), file_format()) -> binary().
1030 roomconfig_to_binary(Options, Lang, FileFormat) ->
1031 %% Get title, if available
1032 1 Title = case lists:keysearch(title, 1, Options) of
1033 1 {value, Tuple} -> [Tuple];
1034
:-(
false -> []
1035 end,
1036
1037 %% Remove title from list
1038 1 Os1 = lists:keydelete(title, 1, Options),
1039
1040 %% Order list
1041 1 Os2 = lists:sort(Os1),
1042
1043 %% Add title to ordered list
1044 1 Options2 = Title ++ Os2,
1045
1046 1 lists:foldl(
1047 fun({Opt, Val}, R) ->
1048 24 case get_roomconfig_text(Opt) of
1049 undefined ->
1050 5 R;
1051 OptT ->
1052 19 OptText = ?T(OptT),
1053 19 R2 = render_config_value(Opt, Val, OptText, FileFormat),
1054 19 <<R/binary, R2/binary>>
1055 end
1056 end,
1057 <<"">>,
1058 Options2).
1059
1060 -spec render_config_value(Opt :: atom(),
1061 Val :: boolean() | string() | binary(),
1062 OptText :: binary(),
1063 FileFormat :: file_format()) -> binary().
1064 render_config_value(_Opt, false, OptText, _FileFormat) ->
1065 4 <<"<div class=\"rcod\">", OptText/binary, "</div>">>;
1066 render_config_value(_Opt, true, OptText, _FileFormat) ->
1067 11 <<"<div class=\"rcoe\">", OptText/binary, "</div>">>;
1068 render_config_value(_Opt, "", OptText, _FileFormat) ->
1069
:-(
<<"<div class=\"rcod\">", OptText/binary, "</div>">>;
1070 render_config_value(password, _T, OptText, _FileFormat) ->
1071 1 <<"<div class=\"rcoe\">", OptText/binary, "</div>">>;
1072 render_config_value(max_users, T, OptText, FileFormat) ->
1073 1 HtmlizedBin = htmlize(list_to_binary(lists:flatten(io_lib:format("~p", [T]))), FileFormat),
1074 1 <<"<div class=\"rcot\">", OptText/binary, ": \"", HtmlizedBin/binary, "\"</div>">>;
1075 render_config_value(title, T, OptText, FileFormat) ->
1076 1 <<"<div class=\"rcot\">", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"</div>">>;
1077 render_config_value(description, T, OptText, FileFormat) ->
1078 1 <<"<div class=\"rcot\">", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"</div>">>;
1079
:-(
render_config_value(_, T, _OptText, _FileFormat) -> <<"\"", T/binary, "\"">>.
1080
1081 -spec get_roomconfig_text(atom()) -> 'undefined' | binary().
1082 1 get_roomconfig_text(title) -> <<"Room title">>;
1083 1 get_roomconfig_text(persistent) -> <<"Make room persistent">>;
1084 1 get_roomconfig_text(public) -> <<"Make room public searchable">>;
1085 1 get_roomconfig_text(public_list) -> <<"Make participants list public">>;
1086 1 get_roomconfig_text(password_protected) -> <<"Make room password protected">>;
1087 1 get_roomconfig_text(password) -> <<"Password">>;
1088 1 get_roomconfig_text(anonymous) -> <<"This room is not anonymous">>;
1089 1 get_roomconfig_text(members_only) -> <<"Make room members-only">>;
1090 1 get_roomconfig_text(moderated) -> <<"Make room moderated">>;
1091 1 get_roomconfig_text(members_by_default) -> <<"Default users as participants">>;
1092 1 get_roomconfig_text(allow_change_subj) -> <<"Allow users to change the subject">>;
1093 1 get_roomconfig_text(allow_private_messages) -> <<"Allow users to send private messages">>;
1094 1 get_roomconfig_text(allow_query_users) -> <<"Allow users to query other users">>;
1095 1 get_roomconfig_text(allow_user_invites) -> <<"Allow users to send invites">>;
1096 1 get_roomconfig_text(logging) -> <<"Enable logging">>;
1097 1 get_roomconfig_text(allow_visitor_nickchange) -> <<"Allow visitors to change nickname">>;
1098 get_roomconfig_text(allow_visitor_status) ->
1099 1 <<"Allow visitors to send status text in presence updates">>;
1100 1 get_roomconfig_text(description) -> <<"Room description">>;
1101 1 get_roomconfig_text(max_users) -> <<"Maximum Number of Occupants">>;
1102 5 get_roomconfig_text(_) -> undefined.
1103
1104
1105 %% @doc Users = [{JID, Nick, Role}]
1106 -spec roomoccupants_to_binary([jid_nick_role()], file_format()) -> binary().
1107 roomoccupants_to_binary(Users, _FileFormat) ->
1108
:-(
Res = [role_users_to_string(RoleS, Users1)
1109
:-(
|| {RoleS, Users1} <- group_by_role(Users), Users1 /= []],
1110
:-(
list_to_binary(lists:flatten(["<div class=\"rcot\">", Res, "</div>"])).
1111
1112
1113 %% @doc Users = [{JID, Nick, Role}]
1114 -spec group_by_role([{jid_nick_role()}]) -> [{string(), string()}].
1115 group_by_role(Users) ->
1116
:-(
{Ms, Ps, Vs, Ns} =
1117 lists:foldl(
1118 fun({JID, Nick, moderator}, {Mod, Par, Vis, Non}) ->
1119
:-(
{[{JID, Nick}] ++ Mod, Par, Vis, Non};
1120 ({JID, Nick, participant}, {Mod, Par, Vis, Non}) ->
1121
:-(
{Mod, [{JID, Nick}] ++ Par, Vis, Non};
1122 ({JID, Nick, visitor}, {Mod, Par, Vis, Non}) ->
1123
:-(
{Mod, Par, [{JID, Nick}] ++ Vis, Non};
1124 ({JID, Nick, none}, {Mod, Par, Vis, Non}) ->
1125
:-(
{Mod, Par, Vis, [{JID, Nick}] ++ Non}
1126 end,
1127 {[], [], [], []},
1128 Users),
1129
:-(
case Ms of [] -> []; _ -> [{"Moderator", Ms}] end
1130
:-(
++ case Ps of [] -> []; _ -> [{"Participant", Ps}] end
1131
:-(
++ case Vs of [] -> []; _ -> [{"Visitor", Vs}] end
1132
:-(
++ case Ns of [] -> []; _ -> [{"None", Ns}] end.
1133
1134
1135 %% Users = [{JID, Nick}]
1136 -spec role_users_to_string(string(), [jid_nick()]) -> [string(), ...].
1137 role_users_to_string(RoleS, Users) ->
1138
:-(
SortedUsers = lists:keysort(2, Users),
1139
:-(
UsersString = [[Nick, "<br/>"] || {_JID, Nick} <- SortedUsers],
1140
:-(
[RoleS, ": ", UsersString].
1141
1142
1143 2979 get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
1144
1145
1146 -spec calc_hour_offset(calendar:datetime()) -> integer().
1147 calc_hour_offset(TimeHere) ->
1148 2 TimeZero = calendar:now_to_universal_time(erlang:timestamp()),
1149 2 TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600,
1150 2 TimeZeroHour = calendar:datetime_to_gregorian_seconds(TimeZero) div 3600,
1151 2 TimeHereHour - TimeZeroHour.
Line Hits Source