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