• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

esl / MongooseIM / 4698755488

14 Apr 2023 10:23AM UTC coverage: 81.909% (+0.4%) from 81.533%
4698755488

push

github

GitHub
Merge pull request #4002 from esl/measured-sql-requests

9 of 9 new or added lines in 1 file covered. (100.0%)

27696 of 33813 relevant lines covered (81.91%)

27956.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.83
/src/mod_muc_log.erl
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
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
161✔
102
    gen_server:start_link({local, Proc}, ?MODULE, {Host, Opts}, []).
161✔
103

104
-spec start(mongooseim:host_type(), map()) -> {'error', _}
105
                                  | {'ok', 'undefined' | pid()}
106
                                  | {'ok', 'undefined' | pid(), _}.
107
start(Host, Opts) ->
108
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
161✔
109
    ChildSpec =
161✔
110
        {Proc,
111
         {?MODULE, start_link, [Host, Opts]},
112
         temporary,
113
         1000,
114
         worker,
115
         [?MODULE]},
116
    ejabberd_sup:start_child(ChildSpec).
161✔
117

118
-spec stop(jid:server()) -> 'ok'
119
    | {'error', 'not_found' | 'restarting' | 'running' | 'simple_one_for_one'}.
120
stop(Host) ->
121
    Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
161✔
122
    gen_server:call(Proc, stop),
161✔
123
    ejabberd_sup:stop_child(Proc).
161✔
124

125
-spec supported_features() -> [atom()].
126
supported_features() ->
127
    [dynamic_domains].
68✔
128

129
-spec config_spec() -> mongoose_config_spec:config_section().
130
config_spec() ->
131
    #section{
14,418✔
132
       items = #{<<"outdir">> => #option{type = string,
133
                                         validate = dirname},
134
                 <<"access_log">> => #option{type = atom,
135
                                             validate = access_rule},
136
                 <<"dirtype">> => #option{type = atom,
137
                                          validate = {enum, [subdirs, plain]}},
138
                 <<"dirname">> => #option{type = atom,
139
                                          validate = {enum, [room_jid, room_name]}},
140
                 <<"file_format">> => #option{type = atom,
141
                                              validate = {enum, [html, plaintext]}},
142
                 <<"css_file">> => #option{type = binary,
143
                                           validate = non_empty},
144
                 <<"timezone">> => #option{type = atom,
145
                                           validate = {enum, [local, universal]}},
146
                 <<"top_link">> => top_link_config_spec(),
147
                 <<"spam_prevention">> => #option{type = boolean}
148
                },
149
          defaults = defaults()
150
      }.
151

152
defaults() ->
153
    #{<<"outdir">> => "www/muc",
14,418✔
154
      <<"access_log">> => muc_admin,
155
      <<"dirtype">> => subdirs,
156
      <<"dirname">> => room_jid,
157
      <<"file_format">> => html,
158
      <<"css_file">> => false,
159
      <<"timezone">> => local,
160
      <<"top_link">> => {"/", "Home"},
161
      <<"spam_prevention">> => true}.
162

163
top_link_config_spec() ->
164
    #section{
14,418✔
165
       items = #{<<"target">> => #option{type = string,
166
                                         validate = url},
167
                 <<"text">> => #option{type = string,
168
                                       validate = non_empty}},
169
       required = all,
170
       process = fun ?MODULE:process_top_link/1
171
      }.
172

173
process_top_link(#{target := Target, text := Text}) ->
174
    {Target, Text}.
6✔
175

176
-spec add_to_log(mongooseim:host_type(), Type :: any(), Data :: any(), mod_muc:room(),
177
                 list()) -> 'ok'.
178
add_to_log(HostType, Type, Data, Room, Opts) ->
179
    gen_server:cast(get_proc_name(HostType),
81✔
180
                    {add_to_log, Type, Data, Room, Opts}).
181

182

183
-spec check_access_log(mongooseim:host_type(), jid:lserver(), jid:jid()) -> any().
184
check_access_log(HostType, ServerHost, From) ->
185
    case catch gen_server:call(get_proc_name(HostType),
117✔
186
                               {check_access_log, HostType, ServerHost, From}) of
187
        {'EXIT', _Error} ->
188
            deny;
×
189
        Res ->
190
            Res
117✔
191
    end.
192

193
-spec set_room_occupants(mongooseim:host_type(), RoomPID :: pid(), RoomJID :: jid:jid(),
194
                         Occupants :: [mod_muc_room:user()]) -> ok.
195
set_room_occupants(HostType, RoomPID, RoomJID, Occupants) ->
196
    gen_server:cast(get_proc_name(HostType), {set_room_occupants, RoomPID, RoomJID, Occupants}).
19,066✔
197

198
%%====================================================================
199
%% gen_server callbacks
200
%%====================================================================
201

202
%%--------------------------------------------------------------------
203
%% Function: init(Args) -> {ok, State} |
204
%%                         {ok, State, Timeout} |
205
%%                         ignore               |
206
%%                         {stop, Reason}
207
%% Description: Initiates the server
208
%%--------------------------------------------------------------------
209
-spec init({HostType :: mongooseim:host_type(), map()}) -> {ok, logstate()}.
210
init({HostType, Opts}) ->
211
    #{
161✔
212
        access_log := AccessLog,
213
        css_file := CSSFile,
214
        dirname := DirName,
215
        dirtype := DirType,
216
        file_format := FileFormat,
217
        outdir := OutDir,
218
        spam_prevention := NoFollow,
219
        timezone := Timezone,
220
        top_link := TopLink
221
     } = Opts,
222
    {ok, #logstate{host_type = HostType,
161✔
223
                out_dir = OutDir,
224
                dir_type = DirType,
225
                dir_name = DirName,
226
                file_format = FileFormat,
227
                css_file = CSSFile,
228
                access = AccessLog,
229
                lang = ?MYLANG,
230
                timezone = Timezone,
231
                spam_prevention = NoFollow,
232
                top_link = TopLink}}.
233

234
%%--------------------------------------------------------------------
235
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
236
%%                                      {reply, Reply, State, Timeout} |
237
%%                                      {noreply, State} |
238
%%                                      {noreply, State, Timeout} |
239
%%                                      {stop, Reason, Reply, State} |
240
%%                                      {stop, Reason, State}
241
%% Description: Handling call messages
242
%%--------------------------------------------------------------------
243
-spec handle_call('stop'
244
            | {'check_access_log', mongooseim:host_type(), 'global' | jid:server(), jid:jid()},
245
        From :: any(), logstate()) -> {'reply', 'allow' | 'deny', logstate()}
246
                                    | {'stop', 'normal', 'ok', _}.
247
handle_call({check_access_log, HostType, ServerHost, FromJID}, _From, State) ->
248
    Reply = acl:match_rule(HostType, ServerHost, State#logstate.access, FromJID),
117✔
249
    {reply, Reply, State};
117✔
250
handle_call(stop, _From, State) ->
251
    {stop, normal, ok, State}.
161✔
252

253
%%--------------------------------------------------------------------
254
%% Function: handle_cast(Msg, State) -> {noreply, State} |
255
%%                                      {noreply, State, Timeout} |
256
%%                                      {stop, Reason, State}
257
%% Description: Handling cast messages
258
%%--------------------------------------------------------------------
259
-spec handle_cast
260
    ({add_to_log, any(), any(), mod_muc:room(), list()}, logstate()) -> {'noreply', logstate()};
261
    ({set_room_occupants, pid(), jid:jid(), [mod_muc_room:user()]}, logstate()) ->
262
        {noreply, logstate()}.
263
handle_cast({add_to_log, Type, Data, Room, Opts}, State) ->
264
    try
45✔
265
        add_to_log2(Type, Data, Room, Opts, State)
45✔
266
    catch Class:Reason:Stacktrace ->
267
              ?LOG_ERROR(#{what => muc_add_to_log_failed, room => Room,
15✔
268
                           class => Class, reason => Reason, stacktrace => Stacktrace,
269
                           log_type => Type, log_data => Data})
×
270
    end,
271
    {noreply, State};
45✔
272
handle_cast({set_room_occupants, RoomPID, RoomJID, Users}, State) ->
273
    #logstate{occupants = OldOccupantsMap, room_monitors = OldMonitors} = State,
18,810✔
274
    RoomJIDBin = jid:to_binary(RoomJID),
18,810✔
275
    Monitors =
18,810✔
276
        case maps:is_key(RoomJIDBin, OldOccupantsMap) of
277
            true -> OldMonitors;
16,210✔
278
            false ->
279
                MonitorRef = monitor(process, RoomPID),
2,600✔
280
                maps:put(MonitorRef, RoomJIDBin, OldMonitors)
2,600✔
281
        end,
282
    Occupants = [{U#user.jid, U#user.nick, U#user.role} || U <- Users],
18,810✔
283
    OccupantsMap = maps:put(RoomJIDBin, Occupants, OldOccupantsMap),
18,810✔
284
    {noreply, State#logstate{occupants = OccupantsMap, room_monitors = Monitors}};
18,810✔
285
handle_cast(_Msg, State) ->
286
    {noreply, State}.
×
287

288
%%--------------------------------------------------------------------
289
%% Function: handle_info(Info, State) -> {noreply, State} |
290
%%                                       {noreply, State, Timeout} |
291
%%                                       {stop, Reason, State}
292
%% Description: Handling all non call/cast messages
293
%%--------------------------------------------------------------------
294
handle_info({'DOWN', MonitorRef, process, Pid, Info}, State) ->
295
    #logstate{occupants = OldOccupantsMap, room_monitors = OldMonitors} = State,
2,599✔
296
    case maps:find(MonitorRef, OldMonitors) of
2,599✔
297
        error ->
298
            ?LOG_WARNING(#{what => muc_unknown_monitor,
×
299
                           text => <<"Unknown monitored process is now down">>,
300
                           monitor_ref => MonitorRef, monitor_pid => Pid, reason => Info}),
×
301
            {noreply, State};
×
302
        {ok, RoomJID} ->
303
            Monitors = maps:remove(MonitorRef, OldMonitors),
2,599✔
304
            OccupantsMap = maps:remove(RoomJID, OldOccupantsMap),
2,599✔
305
            {noreply, State#logstate{occupants = OccupantsMap, room_monitors = Monitors}}
2,599✔
306
    end;
307
handle_info(_Info, State) ->
308
    {noreply, State}.
×
309

310
%%--------------------------------------------------------------------
311
%% Function: terminate(Reason, State) -> void()
312
%% Description: This function is called by a gen_server when it is about to
313
%% terminate. It should be the opposite of Module:init/1 and do any necessary
314
%% cleaning up. When it returns, the gen_server terminates with Reason.
315
%% The return value is ignored.
316
%%--------------------------------------------------------------------
317
terminate(_Reason, _State) ->
318
    ok.
161✔
319

320
%%--------------------------------------------------------------------
321
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
322
%% Description: Convert process state when code is changed
323
%%--------------------------------------------------------------------
324
code_change(_OldVsn, State, _Extra) ->
325
    {ok, State}.
×
326

327
%%--------------------------------------------------------------------
328
%%% Internal functions
329
%%--------------------------------------------------------------------
330
-spec add_to_log2(command(), {mod_muc:nick(), mod_muc:packet()}, mod_muc:room(),
331
        list(), logstate()) -> 'ok'.
332
add_to_log2(text, {Nick, Packet}, Room, Opts, State) ->
333
    case {xml:get_subtag(Packet, <<"subject">>), xml:get_subtag(Packet, <<"body">>)} of
5✔
334
        {false, false} ->
335
            ok;
×
336
        {false, SubEl} ->
337
            Message = {body, xml:get_tag_cdata(SubEl)},
5✔
338
            add_message_to_log(Nick, Message, Room, Opts, State);
5✔
339
        {SubEl, _} ->
340
            Message = {subject, xml:get_tag_cdata(SubEl)},
×
341
            add_message_to_log(Nick, Message, Room, Opts, State)
×
342
    end;
343
add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) ->
344
    add_message_to_log(<<"">>, roomconfig_change, Room, Opts, State);
5✔
345
add_to_log2(roomconfig_change_enabledlogging, Occupants, Room, Opts, State) ->
346
    add_message_to_log(<<"">>, {roomconfig_change, Occupants}, Room, Opts, State);
5✔
347
add_to_log2(room_existence, NewStatus, Room, Opts, State) ->
348
    add_message_to_log(<<"">>, {room_existence, NewStatus}, Room, Opts, State);
20✔
349
add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, State) ->
350
    add_message_to_log(NewNick, {nickchange, OldNick}, Room, Opts, State);
×
351
add_to_log2(join, Nick, Room, Opts, State) ->
352
    add_message_to_log(Nick, join, Room, Opts, State);
5✔
353
add_to_log2(leave, {Nick, Reason}, Room, Opts, State) ->
354
    case Reason of
5✔
355
        <<"">> -> add_message_to_log(Nick, leave, Room, Opts, State);
×
356
        _ -> add_message_to_log(Nick, {leave, Reason}, Room, Opts, State)
5✔
357
    end;
358
add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, State) ->
359
    add_message_to_log(Nick, {kickban, Code, Reason}, Room, Opts, State).
×
360

361

362
%%----------------------------------------------------------------------
363
%% Core
364

365
-spec build_filename_string(calendar:datetime(), OutDir :: binary(),
366
        RoomJID :: jid:literal_jid(), dir_type(), dir_name(), file_format())
367
            -> {binary(), binary(), binary()}.
368
build_filename_string(TimeStamp, OutDir, RoomJID, DirType, DirName, FileFormat) ->
369
    {{Year, Month, Day}, _Time} = TimeStamp,
65✔
370

371
    %% Directory and file names
372
    {Dir, Filename, Rel} =
65✔
373
        case DirType of
374
            subdirs ->
375
                    SYear = list_to_binary(lists:flatten(io_lib:format("~4..0w", [Year]))),
65✔
376
                    SMonth = list_to_binary(lists:flatten(io_lib:format("~2..0w", [Month]))),
65✔
377
                    SDay = list_to_binary(lists:flatten(io_lib:format("~2..0w", [Day]))),
65✔
378
                {filename:join(SYear, SMonth), SDay, <<"../..">>};
65✔
379
            plain ->
380
                    Date = list_to_binary(lists:flatten(
×
381
                             io_lib:format("~4..0w-~2..0w-~2..0w",
382
                                           [Year, Month, Day]))),
383
                    {<<"">>, Date, <<".">>}
×
384
        end,
385

386
    RoomString = case DirName of
65✔
387
                     room_jid -> RoomJID;
65✔
388
                     room_name -> get_room_name(RoomJID)
×
389
                 end,
390
    Extension = case FileFormat of
65✔
391
                    html -> <<".html">>;
65✔
392
                    plaintext -> <<".txt">>
×
393
                end,
394
    Fd = filename:join([OutDir, RoomString, Dir]),
65✔
395
    Fn = filename:join([Fd, <<Filename/binary, Extension/binary>>]),
65✔
396
    Fnrel = filename:join([Rel, Dir, <<Filename/binary, Extension/binary>>]),
65✔
397
    {Fd, Fn, Fnrel}.
65✔
398

399

400
-spec get_room_name(jid:literal_jid()) -> mod_muc:room().
401
get_room_name(RoomJID) ->
402
    JID = jid:from_binary(RoomJID),
×
403
    JID#jid.luser.
×
404

405

406
%% @doc calculate day before
407
-spec get_timestamp_daydiff(calendar:datetime(), integer()) -> calendar:datetime().
408
get_timestamp_daydiff(TimeStamp, Daydiff) ->
409
    {Date1, HMS} = TimeStamp,
20✔
410
    Date2 = calendar:gregorian_days_to_date(
20✔
411
              calendar:date_to_gregorian_days(Date1) + Daydiff),
412
    {Date2, HMS}.
20✔
413

414

415
%% @doc Try to close the previous day log, if it exists
416
-spec close_previous_log(binary(), any(), file_format()) -> 'ok' | {'error', atom()}.
417
close_previous_log(Fn, ImagesDir, FileFormat) ->
418
    case file:read_file_info(Fn) of
×
419
        {ok, _} ->
420
            {ok, F} = file:open(Fn, [append]),
×
421
            write_last_lines(F, ImagesDir, FileFormat),
×
422
            file:close(F);
×
423
        _ -> ok
×
424
    end.
425

426

427
-spec write_last_lines(file:io_device(), binary(), file_format()) -> 'ok'.
428
write_last_lines(_, _, plaintext) ->
429
    ok;
×
430
write_last_lines(F, ImagesDir, _FileFormat) ->
431
    fw(F, <<"<div class=\"legend\">">>),
×
432
    fw(F, <<"  <a href=\"http://www.ejabberd.im\"><img style=\"border:0\" src=\"", ImagesDir/binary,
×
433
            "/powered-by-ejabberd.png\" alt=\"Powered by ejabberd\"/></a>">>),
434
    fw(F, <<"  <a href=\"http://www.erlang.org/\"><img style=\"border:0\" src=\"", ImagesDir/binary,
×
435
            "/powered-by-erlang.png\" alt=\"Powered by Erlang\"/></a>">>),
436
    fw(F, <<"<span class=\"w3c\">">>),
×
437
    fw(F, <<"  <a href=\"http://validator.w3.org/check?uri=referer\">"
×
438
            "<img style=\"border:0;width:88px;height:31px\" src=\"", ImagesDir/binary,
439
            "/valid-xhtml10.png\" alt=\"Valid XHTML 1.0 Transitional\" /></a>">>),
440
    fw(F, <<"  <a href=\"http://jigsaw.w3.org/css-validator/\">"
×
441
            "<img style=\"border:0;width:88px;height:31px\" src=\"", ImagesDir/binary,
442
            "/vcss.png\" alt=\"Valid CSS!\"/></a>">>),
443
    fw(F, <<"</span></div></body></html>">>).
×
444

445

446
-spec add_message_to_log(mod_muc:nick(), Message :: atom() | tuple(),
447
    RoomJID :: jid:simple_jid() | jid:jid(), Opts :: list(), State :: logstate()) -> ok.
448
add_message_to_log(Nick1, Message, RoomJID, Opts, State) ->
449
    #logstate{out_dir = OutDir,
45✔
450
           dir_type = DirType,
451
           dir_name = DirName,
452
           file_format = FileFormat,
453
           css_file = CSSFile,
454
           lang = Lang,
455
           timezone = Timezone,
456
           spam_prevention = NoFollow,
457
           top_link = TopLink,
458
           occupants = OccupantsMap} = State,
459
    Room = get_room_info(RoomJID, Opts),
45✔
460
    Nick = htmlize(Nick1, FileFormat),
45✔
461
    Nick2 = htmlize(<<"<", Nick1/binary, ">">>, FileFormat),
45✔
462
    Now = erlang:timestamp(),
45✔
463
    TimeStamp = case Timezone of
45✔
464
                    local -> calendar:now_to_local_time(Now);
45✔
465
                    universal -> calendar:now_to_universal_time(Now)
×
466
                end,
467
    {Fd, Fn, _Dir} = build_filename_string(TimeStamp, OutDir, Room#room.jid,
45✔
468
                                           DirType, DirName, FileFormat),
469
    {Date, Time} = TimeStamp,
45✔
470

471
    %% Open file, create if it does not exist, create parent dirs if needed
472
    case file:read_file_info(Fn) of
45✔
473
        {ok, _} ->
474
            {ok, F} = file:open(Fn, [append]);
35✔
475
        {error, enoent} ->
476
            make_dir_rec(Fd),
10✔
477
            {ok, F} = file:open(Fn, [append]),
10✔
478
            Datestring = get_dateweek(Date, Lang),
10✔
479

480
            TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1),
10✔
481
            {_FdYesterday, FnYesterday, DatePrev} =
10✔
482
                build_filename_string(
483
                  TimeStampYesterday, OutDir, Room#room.jid, DirType, DirName, FileFormat),
484

485
            TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1),
10✔
486
            {_FdTomorrow, _FnTomorrow, DateNext} =
10✔
487
                build_filename_string(
488
                  TimeStampTomorrow, OutDir, Room#room.jid, DirType, DirName, FileFormat),
489

490
            HourOffset = calc_hour_offset(TimeStamp),
10✔
491
            put_header(F, Room, Datestring, CSSFile, Lang,
10✔
492
                       HourOffset, DatePrev, DateNext, TopLink, FileFormat, OccupantsMap),
493

494
            ImagesDir = <<OutDir/binary, "images">>,
×
495
            file:make_dir(ImagesDir),
×
496
            create_image_files(ImagesDir),
×
497
            ImagesUrl = case DirType of
×
498
                             subdirs -> <<"../../../images">>;
×
499
                             plain -> <<"../images">>
×
500
                         end,
501
            close_previous_log(FnYesterday, ImagesUrl, FileFormat)
×
502
    end,
503

504
    %% Build message
505
    Text
35✔
506
    = case Message of
507
          roomconfig_change ->
508
              RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
5✔
509
              put_room_config(F, RoomConfig, Lang, FileFormat),
5✔
510
              <<"<font class=\"mrcm\">", (?T(<<"Chatroom configuration modified">>))/binary,
5✔
511
                "</font><br/>">>;
512
          {roomconfig_change, Occupants} ->
513
              RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
×
514
              put_room_config(F, RoomConfig, Lang, FileFormat),
×
515
              RoomOccupants = roomoccupants_to_binary(Occupants, FileFormat),
×
516
              put_room_occupants(F, RoomOccupants, Lang, FileFormat),
×
517
              <<"<font class=\"mrcm\">", (?T(<<"Chatroom configuration modified">>))/binary,
×
518
                "</font><br/>">>;
519
          join ->
520
              <<"<font class=\"mj\">", Nick/binary, " ", (?T(<<"joins the room">>))/binary,
5✔
521
                "</font><br/>">>;
522
          leave ->
523
              <<"<font class=\"mj\">", Nick/binary, " ", (?T(<<"leaves the room">>))/binary,
×
524
                "</font><br/>">>;
525
          {leave, Reason} ->
526
              <<"<font class=\"ml\">", Nick/binary, " ", (?T(<<"leaves the room">>))/binary, ": ",
5✔
527
                (htmlize(Reason, NoFollow, FileFormat))/binary, ": ~s</font><br/>">>;
528
          {kickban, "301", ""} ->
529
              <<"<font class=\"mb\">", Nick/binary, " ", (?T(<<"has been banned">>))/binary,
×
530
                "</font><br/>">>;
531
          {kickban, "301", Reason} ->
532
              <<"<font class=\"mb\">", Nick/binary, " ", (?T(<<"has been banned">>))/binary, ": ",
×
533
                (htmlize(Reason, FileFormat))/binary, "</font><br/>">>;
534
          {kickban, "307", ""} ->
535
              <<"<font class=\"mk\">", Nick/binary, " ", (?T(<<"has been kicked">>))/binary,
×
536
                "</font><br/>">>;
537
          {kickban, "307", Reason} ->
538
              <<"<font class=\"mk\">", Nick/binary, " ", (?T(<<"has been kicked">>))/binary, ": ",
×
539
                (htmlize(Reason, FileFormat))/binary, "</font><br/>">>;
540
          {kickban, "321", ""} ->
541
              <<"<font class=\"mk\">", Nick/binary, " ",
×
542
                (?T(<<"has been kicked because of an affiliation change">>))/binary,
543
                "</font><br/>">>;
544
          {kickban, "322", ""} ->
545
              <<"<font class=\"mk\">", Nick/binary, " ",
×
546
                (?T(<<"has been kicked because the room has been changed to"
547
                      " members-only">>))/binary, "</font><br/>">>;
548
          {kickban, "332", ""} ->
549
              <<"<font class=\"mk\">", Nick/binary, " ",
×
550
                (?T(<<"has been kicked because of a system shutdown">>))/binary, "</font><br/>">>;
551
          {nickchange, OldNick} ->
552
              <<"<font class=\"mnc\">", (htmlize(OldNick, FileFormat))/binary, " ",
×
553
                (?T(<<"is now known as">>))/binary, " ", Nick/binary, "</font><br/>">>;
554
          {subject, T} ->
555
              <<"<font class=\"msc\">", Nick/binary, (?T(<<" has set the subject to: ">>))/binary,
×
556
                (htmlize(T, NoFollow, FileFormat))/binary, "</font><br/>">>;
557
          {body, T} ->
558
              case {re:run(T, <<"^/me\s">>, [{capture, none}]), Nick} of
5✔
559
                  {_, ""} ->
560
                      <<"<font class=\"msm\">", (htmlize(T, NoFollow, FileFormat))/binary,
×
561
                        "</font><br/>">>;
562
                  {match, _} ->
563
                      %% Delete "/me " from the beginning.
564
                      <<_Pref:32, SubStr/binary>> = htmlize(T, FileFormat),
×
565
                      <<"<font class=\"mne\">", Nick/binary, " ", SubStr/binary, "</font><br/>">>;
×
566
                  {nomatch, _} ->
567
                      <<"<font class=\"mn\">", Nick2/binary, "</font> ",
5✔
568
                        (htmlize(T, NoFollow, FileFormat))/binary, "<br/>">>
569
              end;
570
          {room_existence, RoomNewExistence} ->
571
              <<"<font class=\"mrcm\">", (get_room_existence_string(RoomNewExistence, Lang))/binary,
15✔
572
                "</font><br/>">>
573
      end,
574
    {Hour, Minute, Second} = Time,
35✔
575
    STime = lists:flatten(
35✔
576
              io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second])),
577
    {_, _, Microsecs} = Now,
35✔
578
    STimeUnique = list_to_binary(lists:flatten(io_lib:format("~s.~w", [STime, Microsecs]))),
35✔
579

580
    %% Write message
581
    fw(F, <<"<a id=\"", STimeUnique/binary, "\" name=\"", STimeUnique/binary,
35✔
582
        "\" href=\"#", STimeUnique/binary, "\" class=\"ts\">[",
583
        (list_to_binary(STime))/binary, "]</a> ", Text/binary>>, FileFormat),
584

585
    %% Close file
586
    file:close(F),
30✔
587
    ok.
30✔
588

589

590
%%----------------------------------------------------------------------
591
%% Utilities
592

593
-spec get_room_existence_string('created' | 'destroyed' | 'started' | 'stopped',
594
        binary()) -> binary().
595
get_room_existence_string(created, Lang) -> ?T(<<"Chatroom is created">>);
×
596
get_room_existence_string(destroyed, Lang) -> ?T(<<"Chatroom is destroyed">>);
5✔
597
get_room_existence_string(started, Lang) -> ?T(<<"Chatroom is started">>);
5✔
598
get_room_existence_string(stopped, Lang) -> ?T(<<"Chatroom is stopped">>).
5✔
599

600

601
-spec get_dateweek(calendar:date(), binary()) -> binary().
602
get_dateweek(Date, Lang) ->
603
    Weekday = case calendar:day_of_the_week(Date) of
10✔
604
                  1 -> ?T(<<"Monday">>);
×
605
                  2 -> ?T(<<"Tuesday">>);
×
606
                  3 -> ?T(<<"Wednesday">>);
×
607
                  4 -> ?T(<<"Thursday">>);
×
608
                  5 -> ?T(<<"Friday">>);
10✔
609
                  6 -> ?T(<<"Saturday">>);
×
610
                  7 -> ?T(<<"Sunday">>)
×
611
              end,
612
    {Y, M, D} = Date,
10✔
613
    Month = case M of
10✔
614
                1 -> ?T(<<"January">>);
×
615
                2 -> ?T(<<"February">>);
×
616
                3 -> ?T(<<"March">>);
×
617
                4 -> ?T(<<"April">>);
10✔
618
                5 -> ?T(<<"May">>);
×
619
                6 -> ?T(<<"June">>);
×
620
                7 -> ?T(<<"July">>);
×
621
                8 -> ?T(<<"August">>);
×
622
                9 -> ?T(<<"September">>);
×
623
                10 -> ?T(<<"October">>);
×
624
                11 -> ?T(<<"November">>);
×
625
                12 -> ?T(<<"December">>)
×
626
            end,
627
    case Lang of
10✔
628
        <<"en">> -> list_to_binary(
10✔
629
            lists:flatten(io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y])));
630
        <<"es">> -> list_to_binary(
×
631
            lists:flatten(io_lib:format("~s ~w de ~s de ~w", [Weekday, D, Month, Y])));
632
        _    -> list_to_binary(
×
633
            lists:flatten(io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y])))
634
    end.
635

636

637
-spec make_dir_rec(binary()) -> 'ok' | {'error', atom()}.
638
make_dir_rec(Dir) ->
639
    case file:read_file_info(Dir) of
45✔
640
        {ok, _} ->
641
            ok;
10✔
642
        {error, enoent} ->
643
            DirS = filename:split(Dir),
35✔
644
            DirR = lists:sublist(DirS, length(DirS)-1),
35✔
645
            make_dir_rec(filename:join(DirR)),
35✔
646
            file:make_dir(Dir)
35✔
647
    end.
648

649

650
%% {ok, F1}=file:open("valid-xhtml10.png", [read]).
651
%% {ok, F1b}=file:read(F1, 1000000).
652
%% c("../../ejabberd/src/jlib.erl").
653
%% jlib:encode_base64(F1b).
654

655
image_base64(<<"powered-by-erlang.png">>) ->
656
    <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoAAADN0lEQVRo3u1a"
×
657
        "P0waURz+rjGRRQ+nUyRCYmJyDPTapDARaSIbTUjt1gVSh8ZW69aBAR0cWLSx"
658
        "CXWp59LR1jbdqKnGxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu"
659
        "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOHo8HkiQxDBXEOjg9"
660
        "PcHc3BxuUSqsI8jR0REAUFGsCCoKFYWCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJ"
661
        "ut9hMQlvdNbUhKWshLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm"
662
        "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1MdG8rSKKXlJ7lHr"
663
        "fjouCut2IrC82BDPbe/gc+xlXez7KxEz63H4lmIN473Rh8Si1BKhRY6aEJI8"
664
        "pLmbjSPN0xOnBBILmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG"
665
        "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFOTYznzpPO+IQi+IK"
666
        "+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN3Olvk4ge/IU98spne+FfGtlslm16nn"
667
        "a8fduntfDscoVjGJqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5"
668
        "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizgwwu5SLK8jllu1t9"
669
        "ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y1m+hSj13WjqOiJKK0qpXKhSFxJAYBv"
670
        "KYaZ9TjWRu4SiWi2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g"
671
        "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJvHsdL+utNYrm2uf"
672
        "ZDVZSkzPKaQkW8kthpyS297BvRdRzR6DdTurJbPy9Ov1K6xr3HBPQuIMowR3"
673
        "asegUyDuU9SuUG+dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q"
674
        "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafIYrizbfESoB+dd9/"
675
        "KF/d/wX3cJvREzl1vAAAAABJRU5ErkJggg==">>;
676
image_base64(<<"valid-xhtml10.png">>) ->
677
    <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEAAACiFBMVEUAAADe"
×
678
        "5+fOezmtra3ejEKlhELvvWO9WlrehELOe3vepaWclHvetVLGc3PerVKcCAj3"
679
        "vVqUjHOUe1JjlL0xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY"
680
        "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoICACEazEhGAgIAACE"
681
        "YzFra2utjELWcznGnEr/7+9jY2POazHOYzGta2NShLVrlL05OUqctdacCADG"
682
        "a2ucAADGpVqUtc61ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe"
683
        "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7/fGxsa9pVqEOSkp"
684
        "Y5xznL29tZxahLXOpVr/99ZrY1L/79ZjUiljSikAOYTvxmMAMYScezmchFqU"
685
        "czGtlFp7c2utjFqUlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC"
686
        "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1mtCMRhzY1L/zmvn"
687
        "vVpSQiHOpVJrUinntVr3zmOEc1L3xmNaWlq1nFo5QkrGWim1lFoISpRSUlK1"
688
        "zt4hWpwASoz///////8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3"
689
        "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUlHtKORh7a1IxIRCU"
690
        "jHtaSiHWrVIpIQhzWinvvVpaQiH/1mPWpVKMe1L/zmP/xmNrUiGErc4YGBj/"
691
        "73PG1ucQWpT/53O9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS"
692
        "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////iP9sSf//dP//////"
693
        "//////////////////////////////////////////8M////////////ef//"
694
        "////////////////////////////////////////////////////////////"
695
        "//////////////////////9d////////////////////////////////////"
696
        "AP//////////////CP//RP//////////////////////////////////////"
697
        "//////////////////////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba"
698
        "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpIvGQGzokvTTA85VH"
699
        "KTpbRoeJnPno/p1+M7t3txj20e/Nzu7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSA"
700
        "NeBDxVmxZZSwEUYkGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb"
701
        "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6VbdtBblesOs+MjmEmz"
702
        "JKNIJWFEfEQTCWNPFKvcKEymjLO1b8bwYQd1hCiiDCl5KsrDCIlhj4fSuvcp"
703
        "fSpgJmyv6dzeZv+nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk"
704
        "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDNE9uzA1YD/5MhnzW"
705
        "1KRqF7mYG8jFYXLcfLpjOe2LA0fuGqQrQHl10sdK0sFcFSOSlzF0BgXQH9h3"
706
        "QZDBI0ccNEhftjXuippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7"
707
        "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woWSRJ0m2ewhvAOUiY"
708
        "YtZtaZL0CZZmtmVOQttLfr/dbveLZodrfrL7W75wG/JjqkQxoNTtNsTKELQp"
709
        "QL6/D5loaSmyTT8TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+"
710
        "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qqmj5bHqVZlJKZIRk"
711
        "1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p1V76DeIs0MX+s3GxRlA1vtw83KhgNp"
712
        "hc1nyErLO5zcvbOsrq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N"
713
        "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4Fsvjmuv662jsWxPZ"
714
        "x2xg890bYMYnTgya7bjmCiEY0qgJ0vMF3c+NoFdPyzxz6V3Uxs3AOWCDchRv"
715
        "OsQtBrbFsrT2fhHEc7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1"
716
        "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3e4HFmYPDl/thYcR"
717
        "Vi8qPEoXVUWBl6FTBFTtnqmKKg5wnlF4wZ1yeLv7TiwXKektE+iDBNicWEyL"
718
        "pnFhfDkpJc3q2khSPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx"
719
        "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdiMOlEOJHw1jOQ4sW"
720
        "t5rQopRkXZhMEi7pmeDCVWBlfUKwhMZ7rsF6elKsvbwiKxgxIdewa3ErsaYo"
721
        "mCVZFYJb0GUu3JqGUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+"
722
        "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx17XhBuDCaOerbt5"
723
        "38erta7TFktLvdHghZcCbcPQO33zIJG9kxF5hoVXnzTzRz0r5js8oTj6uyPk"
724
        "GRf346HOLcasgFexueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl"
725
        "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1juvBSWNFmNWGZy8Lv"
726
        "zUl5PN4JCwyNDzbQ0aAj4Zrjz0FatGJJYhvq4j7mGSpvytGFlZtHf2C4o/28"
727
        "Zu8z7wo7eYPfXysnF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO"
728
        "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+faCSJPJ1bmy6Thyvp"
729
        "ivBmYWgjxPDPAp7JTemY/yGKFEiRt/jG/2P79s8KCwoLCgoLC/khUBA5F0Sf"
730
        "QZ+RYfpNE/4Xosmq7jsZAJsAAAAASUVORK5CYII=">>;
731
image_base64(<<"vcss.png">>) ->
732
    <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSAAABKVBMVEUAAAAj"
×
733
        "Ix8MR51ZVUqAdlmdnZ3ejEWLDAuNjY1kiMG0n2d9fX19Ghfrp1FtbW3y39+3"
734
        "Ph6lIRNdXV2qJBFcVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd"
735
        "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T09RRDwsJBG+vr73"
736
        "wV6fkG6eCQRFcLSurq6/X1+ht9nXfz5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5t"
737
        "WCsmWajsz8+Sq9NMPh4hVaY8MRj///////////////////////9MTEyOp9Lu"
738
        "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn82dfz7/zGafDw+f"
739
        "Dw+zRSlzlMcMDAyNcji1tbXf5vIcFgvATJOjAAAAY3RSTlP/8///////////"
740
        "//////8A//////P/////ov//8//////////////z///T//////////+i////"
741
        "//////////8w/////6IA/xAgMP//////////8/////////8w0/////////+z"
742
        "ehebAAACkUlEQVR42u2VfVPTQBDG19VqC6LY+lKrRIxFQaFSBPuSvhBPF8SI"
743
        "UZK2J5Yav/+HcO8uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6"
744
        "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE24pFtBtzG1Q4AoD5"
745
        "zvw5pEDcJvIQV/TE3/l+H9GnNJwcdABS5wAbFQLMqI98/UReoAaOTlaJsp0z"
746
        "aHx7LwZvY0BUR2xpWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7"
747
        "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45OeuPgm4+Xjtv7xm4N"
748
        "7PMV4C61+Mrz3H2WImm3ATiWrAiwZRWcUA5Ej4dgIEMxDv6yxHHcNuAutnjv"
749
        "2HZ1NeuycoVPh0mwC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW"
750
        "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyGqidQSw1eqtJcA8c"
751
        "Zq4zXqhPuCBYE1fKJjh/5X6MwRm9c2xf7WVdLf5oSdt64esVIwVAKC1HJ2ol"
752
        "i8vj3L0YzC4zjkMagt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD"
753
        "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz54uUyet+hBM7BXX"
754
        "DS8Xc5+8Gmmbu1xwSoGIokA3oTptQecQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo"
755
        "+W9xC8oWpugVCXLuwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua"
756
        "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/AV5htgaQ9SeRAAAA"
757
        "AElFTkSuQmCC">>;
758
image_base64(<<"powered-by-ejabberd.png">>) ->
759
    <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaAAAAw1BMVEUAAAAj"
×
760
        "BgYtBAM5AwFCAAAYGAJNAABcAABIDQ5qAAAoJRV7AACFAAAoKSdJHByLAAAw"
761
        "Lwk1NQA1MzFJKyo4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb"
762
        "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEgAB3dnd4d2+OjACD"
763
        "hYKcmACJi4iQkpWspgCYmJm5swCmqazEwACwsbS4ub3X0QLExsPLyszW1Nnc"
764
        "3ODm5ugMBwAWAwPHm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ"
765
        "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VBkCMAAACcElEQVRI"
766
        "x72WjXKiMBSFQalIFbNiy1pdrJZaRVYR5deGwPs/VRNBSBB2OjvQO0oYjPfj"
767
        "5J6bCcdx8i2UldxKcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv"
768
        "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJlRpsUei0JF6Qi+O"
769
        "HyGK7ijXxPklHe/umIllim3iUBMJDIEULxxPP0TVWhhKJoN9fUpdmQLteV8a"
770
        "DgEAg9gIcTjL4F4L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF"
771
        "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7rovDjMpIHHGYeid"
772
        "Xn4TQcaxMYqP3RV3C8oCl2WvrlSPaNpGZadRnmPGCk8ylM2okAJ4i9TEe1Ke"
773
        "rsXxSl6jUt5uayiIodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs"
774
        "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzvBo1I2tLmuL69kSi"
775
        "tAweyHKN9b3leDfQMnu3nIIKWfmXnqGVKedJT6QpICbJvf2f8aOsvn68v+k7"
776
        "/cwUQdPoxaMoRTnKFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG"
777
        "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCTLav3VrqbPvP3vdd"
778
        "uGEhQJzilncbpSA4F3vsihErO+dayv/sY5/yRE0GDEXCu2VoNiMlo5i+P2Kl"
779
        "gMEvTNk2eYa5XEyh12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA"
780
        "AElFTkSuQmCC">>.
781

782

783
-spec create_image_files(<<_:8, _:_*8>>) -> 'ok'.
784
create_image_files(ImagesDir) ->
785
    Filenames = [<<"powered-by-ejabberd.png">>,
×
786
                 <<"powered-by-erlang.png">>,
787
                 <<"valid-xhtml10.png">>,
788
                 <<"vcss.png">>
789
                ],
790
    lists:foreach(
×
791
      fun(Filename) ->
792
              FilenameFull = filename:join([ImagesDir, Filename]),
×
793
              {ok, F} = file:open(FilenameFull, [write]),
×
794
              Image = jlib:decode_base64(image_base64(Filename)),
×
795
              io:format(F, "~s", [Image]),
×
796
              file:close(F)
×
797
      end,
798
      Filenames),
799
    ok.
×
800

801

802
-spec fw(file:io_device(), binary()) -> 'ok'.
803
fw(F, S) -> fw(F, S, html).
460✔
804

805

806
-spec fw(file:io_device(), binary(), file_format()) -> 'ok'.
807
fw(F, S, FileFormat) ->
808
    S1 = <<S/binary, "~n">>,
495✔
809
    S2 = case FileFormat of
495✔
810
             html ->
811
                    S1;
495✔
812
             plaintext ->
813
            re:replace(S1, <<"<[^>]*>">>, <<"">>, [global, {return, binary}])
×
814
         end,
815
    io:format(F, S2, []).
495✔
816

817

818
-spec put_header(file:io_device(), Room :: room(), Date :: binary(),
819
        CSSFile :: false | binary(), Lang :: ejabberd:lang(), HourOffset :: integer(),
820
        DatePrev :: binary(), DateNext :: binary(), TopLink :: tuple(),
821
        file_format(), OccupantsMap :: #{binary() => [jid_nick_role()]}) -> 'ok'.
822
put_header(_, _, _, _, _, _, _, _, _, plaintext, _) ->
823
    ok;
×
824
put_header(F, Room, Date, CSSFile, Lang, HourOffset, DatePrev, DateNext, TopLink, FileFormat,
825
           OccupantsMap) ->
826
    fw(F, <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
10✔
827
            " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>),
828
    fw(F, <<"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"",
10✔
829
            Lang/binary, "\" lang=\"", Lang/binary, "\">">>),
830
    fw(F, <<"<head>">>),
10✔
831
    fw(F, <<"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />">>),
10✔
832
    fw(F, <<"<title>", (htmlize(Room#room.title))/binary, " - ", Date/binary, "</title>">>),
10✔
833
    put_header_css(F, CSSFile),
10✔
834
    put_header_script(F),
10✔
835
    fw(F, <<"</head>">>),
10✔
836
    fw(F, <<"<body>">>),
10✔
837
    {TopUrl, TopText} = TopLink,
10✔
838
    fw(F, <<"<div style=\"text-align: right;\"><a style=\"color: #AAAAAA; font-family: monospace;"
10✔
839
            " text-decoration: none; font-weight: bold;\" href=\"", TopUrl/binary, "\">",
840
            TopText/binary, "</a></div>">>),
841
    fw(F, <<"<div class=\"roomtitle\">", (htmlize(Room#room.title))/binary, "</div>">>),
×
842
    fw(F, <<"<a class=\"roomjid\" href=\"xmpp:", (Room#room.jid)/binary, "?join\">",
×
843
            (Room#room.jid)/binary, "</a>">>),
844
    fw(F, <<"<div class=\"logdate\">", Date/binary, "<span class=\"w3c\"><a class=\"nav\" href=\"",
×
845
            DatePrev/binary, "\">&lt;</a> <a class=\"nav\" href=\".\/\">^</a>"
846
            " <a class=\"nav\" href=\"", DateNext/binary, "\">&gt;</a></span></div>">>),
847
    case {htmlize(Room#room.subject_author), htmlize(Room#room.subject)} of
×
848
        {<<"">>, <<"">>} ->
849
            ok;
×
850
        {SuA, Su} -> fw(F, <<"<div class=\"roomsubject\">", SuA/binary,
×
851
                             (?T(<<" has set the subject to: ">>))/binary, Su/binary, "</div>">>)
852
    end,
853
    RoomConfig = roomconfig_to_binary(Room#room.config, Lang, FileFormat),
×
854
    put_room_config(F, RoomConfig, Lang, FileFormat),
×
855
    Occupants = maps:get(Room#room.jid, OccupantsMap, []),
×
856
    RoomOccupants = roomoccupants_to_binary(Occupants, FileFormat),
×
857
    put_room_occupants(F, RoomOccupants, Lang, FileFormat),
×
858
    TimeOffsetBin = case HourOffset<0 of
×
859
                          true -> list_to_binary(lists:flatten(io_lib:format("~p", [HourOffset])));
×
860
                          false -> list_to_binary(lists:flatten(io_lib:format("+~p", [HourOffset])))
×
861
        end,
862
    fw(F, <<"<br/><a class=\"ts\">GMT", TimeOffsetBin/binary, "</a><br/>">>).
×
863

864

865
-spec put_header_css(file:io_device(), 'false' | binary()) -> 'ok'.
866
put_header_css(F, false) ->
867
    fw(F, <<"<style type=\"text/css\">">>),
10✔
868
    fw(F, <<"<!--">>),
10✔
869
    fw(F, <<".ts {color: #AAAAAA; text-decoration: none;}">>),
10✔
870
    fw(F, <<".mrcm {color: #009900; font-style: italic; font-weight: bold;}">>),
10✔
871
    fw(F, <<".msc {color: #009900; font-style: italic; font-weight: bold;}">>),
10✔
872
    fw(F, <<".msm {color: #000099; font-style: italic; font-weight: bold;}">>),
10✔
873
    fw(F, <<".mj {color: #009900; font-style: italic;}">>),
10✔
874
    fw(F, <<".ml {color: #009900; font-style: italic;}">>),
10✔
875
    fw(F, <<".mk {color: #009900; font-style: italic;}">>),
10✔
876
    fw(F, <<".mb {color: #009900; font-style: italic;}">>),
10✔
877
    fw(F, <<".mnc {color: #009900; font-style: italic;}">>),
10✔
878
    fw(F, <<".mn {color: #0000AA;}">>),
10✔
879
    fw(F, <<".mne {color: #AA0099;}">>),
10✔
880
    fw(F, <<"a.nav {color: #AAAAAA; font-family: monospace; letter-spacing: 3px;"
10✔
881
            " text-decoration: none;}">>),
882
    fw(F, <<"div.roomtitle {border-bottom: #224466 solid 3pt; margin-left: 20pt;}">>),
10✔
883
    fw(F, <<"div.roomtitle {color: #336699; font-size: 24px; font-weight: bold;"
10✔
884
            " font-family: sans-serif; letter-spacing: 3px; text-decoration: none;}">>),
885
    fw(F, <<"a.roomjid {color: #336699; font-size: 24px; font-weight: bold;"
10✔
886
            " font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt;"
887
            " text-decoration: none;}">>),
888
    fw(F, <<"div.logdate {color: #663399; font-size: 20px; font-weight: bold;"
10✔
889
            " font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt;"
890
            " margin-left:80pt; margin-top:20px;}">>),
891
    fw(F, <<"div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif;"
10✔
892
            " margin-left: 80pt; margin-bottom: 10px;}">>),
893
    fw(F, <<"div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 50%;"
10✔
894
            " text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699;"
895
            " border-right: 4px solid #336699;}">>),
896
    fw(F, <<"div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px;}">>),
10✔
897
    fw(F, <<"div.rcos {padding-right: 10px;}">>),
10✔
898
    fw(F, <<"div.rcoe {color: green;}">>),
10✔
899
    fw(F, <<"div.rcod {color: red;}">>),
10✔
900
    fw(F, <<"div.rcoe:after {content: \": v\";}">>),
10✔
901
    fw(F, <<"div.rcod:after {content: \": x\";}">>),
10✔
902
    fw(F, <<"div.rcot:after {}">>),
10✔
903
    fw(F, <<".legend {width: 100%; margin-top: 30px; border-top: #224466 solid 1pt;"
10✔
904
            " padding: 10px 0px 10px 0px; text-align: left;"
905
            " font-family: monospace; letter-spacing: 2px;}">>),
906
    fw(F, <<".w3c {position: absolute; right: 10px; width: 60%; text-align: right;"
10✔
907
            " font-family: monospace; letter-spacing: 1px;}">>),
908
    fw(F, <<"//-->">>),
10✔
909
    fw(F, <<"</style>">>);
10✔
910
put_header_css(F, CSSFile) ->
911
    fw(F, <<"<link rel=\"stylesheet\" type=\"text/css\" href=\"",
×
912
            CSSFile/binary, "\" media=\"all\">">>).
913

914
put_header_script(F) ->
915
    fw(F, <<"<script type=\"text/javascript\">">>),
10✔
916
    fw(F, <<"function sh(e) // Show/Hide an element">>),
10✔
917
    fw(F, <<"{if(document.getElementById(e).style.display=='none')">>),
10✔
918
    fw(F, <<"{document.getElementById(e).style.display='block';}">>),
10✔
919
    fw(F, <<"else {document.getElementById(e).style.display='none';}}">>),
10✔
920
    fw(F, <<"</script>">>).
10✔
921

922

923
-spec put_room_config(file:io_device(), any(), ejabberd:lang(),
924
                      file_format()) -> 'ok'.
925
put_room_config(_F, _RoomConfig, _Lang, plaintext) ->
926
    ok;
×
927
put_room_config(F, RoomConfig, Lang, _FileFormat) ->
928
    {Now1, Now2, Now3} = erlang:timestamp(),
5✔
929
    NowBin = list_to_binary(lists:flatten(io_lib:format("~p~p~p", [Now1, Now2, Now3]))),
5✔
930
    fw(F, <<"<div class=\"rc\">">>),
5✔
931
    fw(F, <<"<div class=\"rct\" onclick=\"sh('a", NowBin/binary, "');return false;\">",
5✔
932
            (?T(<<"Room Configuration">>))/binary, "</div>">>),
933
    fw(F, <<"<div class=\"rcos\" id=\"a", NowBin/binary, "\" style=\"display: none;\" ><br/>",
5✔
934
            RoomConfig/binary, "</div>">>),
935
    fw(F, <<"</div>">>).
5✔
936

937

938
-spec put_room_occupants(file:io_device(), any(), ejabberd:lang(),
939
                         file_format()) -> 'ok'.
940
put_room_occupants(_F, _RoomOccupants, _Lang, plaintext) ->
941
    ok;
×
942
put_room_occupants(F, RoomOccupants, Lang, _FileFormat) ->
943
    {Now1, Now2, Now3} = erlang:timestamp(),
×
944
    NowBin = list_to_binary(lists:flatten(io_lib:format("~p~p~p", [Now1, Now2, Now3]))),
×
945
    fw(F, <<"<div class=\"rc\">">>),
×
946
    fw(F, <<"<div class=\"rct\" onclick=\"sh('o", NowBin/binary, "');return false;\">",
×
947
            (?T(<<"Room Occupants">>))/binary, "</div>">>),
948
    fw(F, <<"<div class=\"rcos\" id=\"o", NowBin/binary, "\" style=\"display: none;\" ><br/>",
×
949
            RoomOccupants/binary, "</div>">>),
950
    fw(F, <<"</div>">>).
×
951

952

953
%% @doc htmlize
954
%% The default behaviour is to ignore the nofollow spam prevention on links
955
%% (NoFollow=false)
956
htmlize(S1) ->
957
    htmlize(S1, html).
10✔
958

959
htmlize(S1, FileFormat) ->
960
    htmlize(S1, false, FileFormat).
115✔
961

962

963
%% @doc The NoFollow parameter tell if the spam prevention should be applied to
964
%% the link found. true means 'apply nofollow on links'.
965
htmlize(S1, _NoFollow, plaintext) ->
966
    ReplacementRules =
×
967
        [{<<"<">>, <<"[">>},
968
         {<<">">>, <<"]">>}],
969
    lists:foldl(fun({RegExp, Replace}, Acc) ->
×
970
                        re:replace(Acc, RegExp, Replace, [global, {return, binary}])
×
971
                end, S1, ReplacementRules);
972
htmlize(S1, NoFollow, _FileFormat) ->
973
    S2List = binary:split(S1, <<"\n">>, [global]),
125✔
974
    lists:foldl(
125✔
975
      fun(Si, Res) ->
976
              Si2 = htmlize2(Si, NoFollow),
125✔
977
              case Res of
125✔
978
                      <<"">> -> Si2;
125✔
979
                      _ -> <<Res/binary, "<br/>", Si2/binary>>
×
980
              end
981
      end,
982
      <<"">>,
983
      S2List).
984

985
htmlize2(S1, NoFollow) ->
986
    ReplacementRules =
125✔
987
        [{<<"\\&">>, <<"\\&amp;">>},
988
         {<<"<">>, <<"\\&lt;">>},
989
         {<<">">>, <<"\\&gt;">>},
990
         {<<"((http|https|ftp)://|(mailto|xmpp):)[^] )\'\"}]+">>, link_regexp(NoFollow)},
991
         {<<"  ">>, <<"\\&nbsp;\\&nbsp;">>},
992
         {<<"\\t">>, <<"\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;">>},
993
         {<<226, 128, 174>>, <<"[RLO]">>}],
994
    lists:foldl(fun({RegExp, Replace}, Acc) ->
125✔
995
                        re:replace(Acc, RegExp, Replace, [global, {return, binary}])
875✔
996
                end, S1, ReplacementRules).
997

998
%% @doc Regexp link. Add the nofollow rel attribute when required
999
link_regexp(false) ->
1000
    <<"<a href=\"&\">&</a>">>;
115✔
1001
link_regexp(true) ->
1002
    <<"<a href=\"&\" rel=\"nofollow\">&</a>">>.
10✔
1003

1004

1005
get_room_info(RoomJID, Opts) ->
1006
    Title =
45✔
1007
        case lists:keysearch(title, 1, Opts) of
1008
            {value, {_, T}} -> T;
45✔
1009
            false -> <<"">>
×
1010
        end,
1011
    Subject =
45✔
1012
        case lists:keysearch(subject, 1, Opts) of
1013
            {value, {_, S}} -> S;
45✔
1014
            false -> <<"">>
×
1015
        end,
1016
    SubjectAuthor =
45✔
1017
        case lists:keysearch(subject_author, 1, Opts) of
1018
            {value, {_, SA}} -> SA;
45✔
1019
            false -> <<"">>
×
1020
        end,
1021
    #room{jid = RoomJID,
45✔
1022
          title = Title,
1023
          subject = Subject,
1024
          subject_author = SubjectAuthor,
1025
          config = Opts
1026
         }.
1027

1028

1029
-spec roomconfig_to_binary(list(), ejabberd:lang(), file_format()) -> binary().
1030
roomconfig_to_binary(Options, Lang, FileFormat) ->
1031
    %% Get title, if available
1032
    Title = case lists:keysearch(title, 1, Options) of
5✔
1033
                {value, Tuple} -> [Tuple];
5✔
1034
                false -> []
×
1035
            end,
1036

1037
    %% Remove title from list
1038
    Os1 = lists:keydelete(title, 1, Options),
5✔
1039

1040
    %% Order list
1041
    Os2 = lists:sort(Os1),
5✔
1042

1043
    %% Add title to ordered list
1044
    Options2 = Title ++ Os2,
5✔
1045

1046
    lists:foldl(
5✔
1047
      fun({Opt, Val}, R) ->
1048
              case get_roomconfig_text(Opt) of
120✔
1049
                  undefined ->
1050
                      R;
25✔
1051
                  OptT ->
1052
                      OptText = ?T(OptT),
95✔
1053
                      R2 = render_config_value(Opt, Val, OptText, FileFormat),
95✔
1054
                      <<R/binary, R2/binary>>
95✔
1055
              end
1056
      end,
1057
      <<"">>,
1058
      Options2).
1059

1060
-spec render_config_value(Opt :: atom(),
1061
                          Val :: boolean() | string() | binary(),
1062
                          OptText :: binary(),
1063
                          FileFormat :: file_format()) -> binary().
1064
render_config_value(_Opt, false, OptText, _FileFormat) ->
1065
    <<"<div class=\"rcod\">", OptText/binary, "</div>">>;
20✔
1066
render_config_value(_Opt, true, OptText, _FileFormat) ->
1067
    <<"<div class=\"rcoe\">", OptText/binary, "</div>">>;
55✔
1068
render_config_value(_Opt, "", OptText, _FileFormat) ->
1069
    <<"<div class=\"rcod\">", OptText/binary, "</div>">>;
×
1070
render_config_value(password, _T, OptText, _FileFormat) ->
1071
    <<"<div class=\"rcoe\">", OptText/binary, "</div>">>;
5✔
1072
render_config_value(max_users, T, OptText, FileFormat) ->
1073
    HtmlizedBin = htmlize(list_to_binary(lists:flatten(io_lib:format("~p", [T]))), FileFormat),
5✔
1074
    <<"<div class=\"rcot\">", OptText/binary, ": \"", HtmlizedBin/binary, "\"</div>">>;
5✔
1075
render_config_value(title, T, OptText, FileFormat) ->
1076
    <<"<div class=\"rcot\">", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"</div>">>;
5✔
1077
render_config_value(description, T, OptText, FileFormat) ->
1078
    <<"<div class=\"rcot\">", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"</div>">>;
5✔
1079
render_config_value(_, T, _OptText, _FileFormat) -> <<"\"", T/binary, "\"">>.
×
1080

1081
-spec get_roomconfig_text(atom()) -> 'undefined' | binary().
1082
get_roomconfig_text(title) -> <<"Room title">>;
5✔
1083
get_roomconfig_text(persistent) -> <<"Make room persistent">>;
5✔
1084
get_roomconfig_text(public) -> <<"Make room public searchable">>;
5✔
1085
get_roomconfig_text(public_list) -> <<"Make participants list public">>;
5✔
1086
get_roomconfig_text(password_protected) -> <<"Make room password protected">>;
5✔
1087
get_roomconfig_text(password) -> <<"Password">>;
5✔
1088
get_roomconfig_text(anonymous) -> <<"This room is not anonymous">>;
5✔
1089
get_roomconfig_text(members_only) -> <<"Make room members-only">>;
5✔
1090
get_roomconfig_text(moderated) -> <<"Make room moderated">>;
5✔
1091
get_roomconfig_text(members_by_default) -> <<"Default users as participants">>;
5✔
1092
get_roomconfig_text(allow_change_subj) -> <<"Allow users to change the subject">>;
5✔
1093
get_roomconfig_text(allow_private_messages) -> <<"Allow users to send private messages">>;
5✔
1094
get_roomconfig_text(allow_query_users) -> <<"Allow users to query other users">>;
5✔
1095
get_roomconfig_text(allow_user_invites) -> <<"Allow users to send invites">>;
5✔
1096
get_roomconfig_text(logging) ->  <<"Enable logging">>;
5✔
1097
get_roomconfig_text(allow_visitor_nickchange) ->  <<"Allow visitors to change nickname">>;
5✔
1098
get_roomconfig_text(allow_visitor_status) ->
1099
    <<"Allow visitors to send status text in presence updates">>;
5✔
1100
get_roomconfig_text(description) ->  <<"Room description">>;
5✔
1101
get_roomconfig_text(max_users) -> <<"Maximum Number of Occupants">>;
5✔
1102
get_roomconfig_text(_) -> undefined.
25✔
1103

1104

1105
%% @doc Users = [{JID, Nick, Role}]
1106
-spec roomoccupants_to_binary([jid_nick_role()], file_format()) -> binary().
1107
roomoccupants_to_binary(Users, _FileFormat) ->
1108
    Res = [role_users_to_string(RoleS, Users1)
×
1109
           || {RoleS, Users1} <- group_by_role(Users), Users1 /= []],
×
1110
    list_to_binary(lists:flatten(["<div class=\"rcot\">", Res, "</div>"])).
×
1111

1112

1113
%% @doc Users = [{JID, Nick, Role}]
1114
-spec group_by_role([{jid_nick_role()}]) -> [{string(), string()}].
1115
group_by_role(Users) ->
1116
    {Ms, Ps, Vs, Ns} =
×
1117
        lists:foldl(
1118
          fun({JID, Nick, moderator}, {Mod, Par, Vis, Non}) ->
1119
                  {[{JID, Nick}] ++ Mod, Par, Vis, Non};
×
1120
             ({JID, Nick, participant}, {Mod, Par, Vis, Non}) ->
1121
                  {Mod, [{JID, Nick}] ++ Par, Vis, Non};
×
1122
             ({JID, Nick, visitor}, {Mod, Par, Vis, Non}) ->
1123
                  {Mod, Par, [{JID, Nick}] ++ Vis, Non};
×
1124
             ({JID, Nick, none}, {Mod, Par, Vis, Non}) ->
1125
                  {Mod, Par, Vis, [{JID, Nick}] ++ Non}
×
1126
          end,
1127
          {[], [], [], []},
1128
          Users),
1129
    case Ms of [] -> []; _ -> [{"Moderator", Ms}] end
×
1130
        ++ case Ps of [] -> []; _ -> [{"Participant", Ps}] end
×
1131
        ++ case Vs of [] -> []; _ -> [{"Visitor", Vs}] end
×
1132
        ++ case Ns of [] -> []; _ -> [{"None", Ns}] end.
×
1133

1134

1135
%% Users = [{JID, Nick}]
1136
-spec role_users_to_string(string(), [jid_nick()]) -> [string(), ...].
1137
role_users_to_string(RoleS, Users) ->
1138
    SortedUsers = lists:keysort(2, Users),
×
1139
    UsersString = [[Nick, "<br/>"] || {_JID, Nick} <- SortedUsers],
×
1140
    [RoleS, ": ", UsersString].
×
1141

1142

1143
get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME).
19,264✔
1144

1145

1146
-spec calc_hour_offset(calendar:datetime()) -> integer().
1147
calc_hour_offset(TimeHere) ->
1148
    TimeZero = calendar:now_to_universal_time(erlang:timestamp()),
10✔
1149
    TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600,
10✔
1150
    TimeZeroHour = calendar:datetime_to_gregorian_seconds(TimeZero) div 3600,
10✔
1151
    TimeHereHour - TimeZeroHour.
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc