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