mirror of
https://github.com/processone/ejabberd.git
synced 2024-08-20 12:35:21 +02:00
1169 lines
40 KiB
Erlang
1169 lines
40 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_muc_log.erl
|
|
%%% Author : Badlop@process-one.net
|
|
%%% Purpose : MUC room logging
|
|
%%% Created : 12 Mar 2006 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2024 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(mod_muc_log).
|
|
|
|
-protocol({xep, 334, '0.2'}).
|
|
|
|
-author('badlop@process-one.net').
|
|
|
|
-behaviour(gen_server).
|
|
|
|
-behaviour(gen_mod).
|
|
|
|
%% API
|
|
-export([start/2, stop/1, reload/3, get_url/2,
|
|
check_access_log/3, add_to_log/5]).
|
|
|
|
-export([init/1, handle_call/3, handle_cast/2,
|
|
handle_info/2, terminate/2, code_change/3,
|
|
mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
|
|
|
|
-include("logger.hrl").
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
|
-include("mod_muc_room.hrl").
|
|
-include("translate.hrl").
|
|
|
|
-record(room, {jid, title, subject, subject_author, config}).
|
|
|
|
-define(PLAINTEXT_CO, <<"ZZCZZ">>).
|
|
-define(PLAINTEXT_IN, <<"ZZIZZ">>).
|
|
-define(PLAINTEXT_OUT, <<"ZZOZZ">>).
|
|
|
|
-record(logstate, {host,
|
|
out_dir,
|
|
dir_type,
|
|
dir_name,
|
|
file_format,
|
|
file_permissions,
|
|
css_file,
|
|
access,
|
|
lang,
|
|
timezone,
|
|
spam_prevention,
|
|
top_link}).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
start(Host, Opts) ->
|
|
gen_mod:start_child(?MODULE, Host, Opts).
|
|
|
|
stop(Host) ->
|
|
gen_mod:stop_child(?MODULE, Host).
|
|
|
|
reload(Host, NewOpts, _OldOpts) ->
|
|
Proc = get_proc_name(Host),
|
|
gen_server:cast(Proc, {reload, NewOpts}).
|
|
|
|
add_to_log(Host, Type, Data, Room, Opts) ->
|
|
gen_server:cast(get_proc_name(Host),
|
|
{add_to_log, Type, Data, Room, Opts}).
|
|
|
|
check_access_log(allow, _Host, _From) ->
|
|
allow;
|
|
check_access_log(_Acc, Host, From) ->
|
|
case catch gen_server:call(get_proc_name(Host),
|
|
{check_access_log, Host, From})
|
|
of
|
|
{'EXIT', _Error} -> deny;
|
|
Res -> Res
|
|
end.
|
|
|
|
-spec get_url(any(), #state{}) -> {ok, binary()} | error.
|
|
get_url({ok, _} = Acc, _State) ->
|
|
Acc;
|
|
get_url(_Acc, #state{room = Room, host = Host, server_host = ServerHost}) ->
|
|
try mod_muc_log_opt:url(ServerHost) of
|
|
undefined -> error;
|
|
URL ->
|
|
case mod_muc_log_opt:dirname(ServerHost) of
|
|
room_jid ->
|
|
{ok, <<URL/binary, $/, Room/binary, $@, Host/binary, $/>>};
|
|
room_name ->
|
|
{ok, <<URL/binary, $/, Room/binary, $/>>}
|
|
end
|
|
catch
|
|
error:{module_not_loaded, _, _} ->
|
|
error
|
|
end.
|
|
|
|
depends(_Host, _Opts) ->
|
|
[{mod_muc, hard}].
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
init([Host|_]) ->
|
|
process_flag(trap_exit, true),
|
|
Opts = gen_mod:get_module_opts(Host, ?MODULE),
|
|
ejabberd_hooks:add(muc_log_add, Host, ?MODULE, add_to_log, 100),
|
|
ejabberd_hooks:add(muc_log_check_access_log, Host, ?MODULE, check_access_log, 100),
|
|
ejabberd_hooks:add(muc_log_get_url, Host, ?MODULE, get_url, 100),
|
|
{ok, init_state(Host, Opts)}.
|
|
|
|
handle_call({check_access_log, ServerHost, FromJID}, _From, State) ->
|
|
Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID),
|
|
{reply, Reply, State};
|
|
handle_call(stop, _From, State) ->
|
|
{stop, normal, ok, State}.
|
|
|
|
handle_cast({reload, Opts}, #logstate{host = Host}) ->
|
|
{noreply, init_state(Host, Opts)};
|
|
handle_cast({add_to_log, Type, Data, Room, Opts}, State) ->
|
|
case catch add_to_log2(Type, Data, Room, Opts, State) of
|
|
{'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]);
|
|
_ -> ok
|
|
end,
|
|
{noreply, State};
|
|
handle_cast(Msg, State) ->
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
|
{noreply, State}.
|
|
|
|
handle_info(_Info, State) -> {noreply, State}.
|
|
|
|
terminate(_Reason, #state{host = Host}) ->
|
|
ejabberd_hooks:delete(muc_log_add, Host, ?MODULE, add_to_log, 100),
|
|
ejabberd_hooks:delete(muc_log_check_access_log, Host, ?MODULE, check_access_log, 100),
|
|
ejabberd_hooks:delete(muc_log_get_url, Host, ?MODULE, get_url, 100),
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
init_state(Host, Opts) ->
|
|
OutDir = mod_muc_log_opt:outdir(Opts),
|
|
DirType = mod_muc_log_opt:dirtype(Opts),
|
|
DirName = mod_muc_log_opt:dirname(Opts),
|
|
FileFormat = mod_muc_log_opt:file_format(Opts),
|
|
FilePermissions = mod_muc_log_opt:file_permissions(Opts),
|
|
CSSFile = mod_muc_log_opt:cssfile(Opts),
|
|
AccessLog = mod_muc_log_opt:access_log(Opts),
|
|
Timezone = mod_muc_log_opt:timezone(Opts),
|
|
Top_link = mod_muc_log_opt:top_link(Opts),
|
|
NoFollow = mod_muc_log_opt:spam_prevention(Opts),
|
|
Lang = ejabberd_option:language(Host),
|
|
#logstate{host = Host, out_dir = OutDir,
|
|
dir_type = DirType, dir_name = DirName,
|
|
file_format = FileFormat, css_file = CSSFile,
|
|
file_permissions = FilePermissions,
|
|
access = AccessLog, lang = Lang, timezone = Timezone,
|
|
spam_prevention = NoFollow, top_link = Top_link}.
|
|
|
|
add_to_log2(text, {Nick, Packet}, Room, Opts, State) ->
|
|
case has_no_permanent_store_hint(Packet) of
|
|
false ->
|
|
case {Packet#message.subject, Packet#message.body} of
|
|
{[], []} -> ok;
|
|
{[], Body} ->
|
|
Message = {body, xmpp:get_text(Body)},
|
|
add_message_to_log(Nick, Message, Room, Opts, State);
|
|
{Subj, _} ->
|
|
Message = {subject, xmpp:get_text(Subj)},
|
|
add_message_to_log(Nick, Message, Room, Opts, State)
|
|
end;
|
|
true -> ok
|
|
end;
|
|
add_to_log2(roomconfig_change, _Occupants, Room, Opts,
|
|
State) ->
|
|
add_message_to_log(<<"">>, roomconfig_change, Room,
|
|
Opts, State);
|
|
add_to_log2(roomconfig_change_enabledlogging, Occupants,
|
|
Room, Opts, State) ->
|
|
add_message_to_log(<<"">>,
|
|
{roomconfig_change, Occupants}, Room, Opts, State);
|
|
add_to_log2(room_existence, NewStatus, Room, Opts,
|
|
State) ->
|
|
add_message_to_log(<<"">>, {room_existence, NewStatus},
|
|
Room, Opts, State);
|
|
add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts,
|
|
State) ->
|
|
add_message_to_log(NewNick, {nickchange, OldNick}, Room,
|
|
Opts, State);
|
|
add_to_log2(join, Nick, Room, Opts, State) ->
|
|
add_message_to_log(Nick, join, Room, Opts, State);
|
|
add_to_log2(leave, {Nick, Reason}, Room, Opts, State) ->
|
|
case Reason of
|
|
<<"">> ->
|
|
add_message_to_log(Nick, leave, Room, Opts, State);
|
|
_ ->
|
|
add_message_to_log(Nick, {leave, Reason}, Room, Opts,
|
|
State)
|
|
end;
|
|
add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts,
|
|
State) ->
|
|
add_message_to_log(Nick, {kickban, Code, Reason}, Room,
|
|
Opts, State).
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Core
|
|
|
|
build_filename_string(TimeStamp, OutDir, RoomJID,
|
|
DirType, DirName, FileFormat) ->
|
|
{{Year, Month, Day}, _Time} = TimeStamp,
|
|
{Dir, Filename, Rel} = case DirType of
|
|
subdirs ->
|
|
SYear =
|
|
(str:format("~4..0w",
|
|
[Year])),
|
|
SMonth =
|
|
(str:format("~2..0w",
|
|
[Month])),
|
|
SDay = (str:format("~2..0w",
|
|
[Day])),
|
|
{fjoin([SYear, SMonth]), SDay,
|
|
<<"../..">>};
|
|
plain ->
|
|
Date =
|
|
(str:format("~4..0w-~2..0w-~2..0w",
|
|
[Year,
|
|
Month,
|
|
Day])),
|
|
{<<"">>, Date, <<".">>}
|
|
end,
|
|
RoomString = case DirName of
|
|
room_jid -> RoomJID;
|
|
room_name -> get_room_name(RoomJID)
|
|
end,
|
|
Extension = case FileFormat of
|
|
html -> <<".html">>;
|
|
plaintext -> <<".txt">>
|
|
end,
|
|
Fd = fjoin([OutDir, RoomString, Dir]),
|
|
Fn = fjoin([Fd, <<Filename/binary, Extension/binary>>]),
|
|
Fnrel = fjoin([Rel, Dir, <<Filename/binary, Extension/binary>>]),
|
|
{Fd, Fn, Fnrel}.
|
|
|
|
get_room_name(RoomJID) ->
|
|
JID = jid:decode(RoomJID), JID#jid.user.
|
|
|
|
%% calculate day before
|
|
get_timestamp_daydiff(TimeStamp, Daydiff) ->
|
|
{Date1, HMS} = TimeStamp,
|
|
Date2 =
|
|
calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date1)
|
|
+ Daydiff),
|
|
{Date2, HMS}.
|
|
|
|
%% Try to close the previous day log, if it exists
|
|
close_previous_log(Fn, Images_dir, FileFormat) ->
|
|
case file:read_file_info(Fn) of
|
|
{ok, _} ->
|
|
{ok, F} = file:open(Fn, [append]),
|
|
write_last_lines(F, Images_dir, FileFormat),
|
|
file:close(F);
|
|
_ -> ok
|
|
end.
|
|
|
|
write_last_lines(_, _, plaintext) -> ok;
|
|
write_last_lines(F, Images_dir, _FileFormat) ->
|
|
fw(F, <<"<div class=\"legend\">">>),
|
|
fw(F,
|
|
<<" <a href=\"http://www.ejabberd.im\"><img "
|
|
"style=\"border:0\" src=\"~ts/powered-by-ejabbe"
|
|
"rd.png\" alt=\"Powered by ejabberd - robust, scalable and extensible XMPP server\"/></a>">>,
|
|
[Images_dir]),
|
|
fw(F,
|
|
<<" <a href=\"http://www.erlang.org/\"><img "
|
|
"style=\"border:0\" src=\"~ts/powered-by-erlang"
|
|
".png\" alt=\"Powered by Erlang\"/></a>">>,
|
|
[Images_dir]),
|
|
fw(F, <<"<span class=\"w3c\">">>),
|
|
fw(F,
|
|
<<" <a href=\"http://validator.w3.org/check?uri"
|
|
"=referer\"><img style=\"border:0;width:88px;h"
|
|
"eight:31px\" src=\"~ts/valid-xhtml10.png\" "
|
|
"alt=\"Valid XHTML 1.0 Transitional\" "
|
|
"/></a>">>,
|
|
[Images_dir]),
|
|
fw(F,
|
|
<<" <a href=\"http://jigsaw.w3.org/css-validato"
|
|
"r/\"><img style=\"border:0;width:88px;height:"
|
|
"31px\" src=\"~ts/vcss.png\" alt=\"Valid "
|
|
"CSS!\"/></a>">>,
|
|
[Images_dir]),
|
|
fw(F, <<"</span></div></body></html>">>).
|
|
|
|
set_filemode(Fn, {FileMode, FileGroup}) ->
|
|
ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)),
|
|
ok = file:change_group(Fn, FileGroup).
|
|
|
|
htmlize_nick(Nick1, html) ->
|
|
htmlize(<<"<", Nick1/binary, ">">>, html);
|
|
htmlize_nick(Nick1, plaintext) ->
|
|
htmlize(<<?PLAINTEXT_IN/binary, Nick1/binary, ?PLAINTEXT_OUT/binary>>, plaintext).
|
|
|
|
add_message_to_log(Nick1, Message, RoomJID, Opts,
|
|
State) ->
|
|
#logstate{out_dir = OutDir, dir_type = DirType,
|
|
dir_name = DirName, file_format = FileFormat,
|
|
file_permissions = FilePermissions,
|
|
css_file = CSSFile, lang = Lang, timezone = Timezone,
|
|
spam_prevention = NoFollow, top_link = TopLink} =
|
|
State,
|
|
Room = get_room_info(RoomJID, Opts),
|
|
Nick = htmlize(Nick1, FileFormat),
|
|
Nick2 = htmlize_nick(Nick1, FileFormat),
|
|
Now = erlang:timestamp(),
|
|
TimeStamp = case Timezone of
|
|
local -> calendar:now_to_local_time(Now);
|
|
universal -> calendar:now_to_universal_time(Now)
|
|
end,
|
|
{Fd, Fn, _Dir} = build_filename_string(TimeStamp,
|
|
OutDir, Room#room.jid, DirType,
|
|
DirName, FileFormat),
|
|
{Date, Time} = TimeStamp,
|
|
case file:read_file_info(Fn) of
|
|
{ok, _} -> {ok, F} = file:open(Fn, [append]);
|
|
{error, enoent} ->
|
|
make_dir_rec(Fd),
|
|
{ok, F} = file:open(Fn, [append]),
|
|
catch set_filemode(Fn, FilePermissions),
|
|
Datestring = get_dateweek(Date, Lang),
|
|
TimeStampYesterday = get_timestamp_daydiff(TimeStamp,
|
|
-1),
|
|
{_FdYesterday, FnYesterday, DatePrev} =
|
|
build_filename_string(TimeStampYesterday, OutDir,
|
|
Room#room.jid, DirType, DirName,
|
|
FileFormat),
|
|
TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1),
|
|
{_FdTomorrow, _FnTomorrow, DateNext} =
|
|
build_filename_string(TimeStampTomorrow, OutDir,
|
|
Room#room.jid, DirType, DirName,
|
|
FileFormat),
|
|
HourOffset = calc_hour_offset(TimeStamp),
|
|
put_header(F, Room, Datestring, CSSFile, Lang,
|
|
HourOffset, DatePrev, DateNext, TopLink, FileFormat),
|
|
Images_dir = fjoin([OutDir, <<"images">>]),
|
|
file:make_dir(Images_dir),
|
|
create_image_files(Images_dir),
|
|
Images_url = case DirType of
|
|
subdirs -> <<"../../../images">>;
|
|
plain -> <<"../images">>
|
|
end,
|
|
close_previous_log(FnYesterday, Images_url, FileFormat)
|
|
end,
|
|
Text = case Message of
|
|
roomconfig_change ->
|
|
RoomConfig = roomconfig_to_string(Room#room.config,
|
|
Lang, FileFormat),
|
|
put_room_config(F, RoomConfig, Lang, FileFormat),
|
|
io_lib:format("<font class=\"mrcm\">~ts</font><br/>",
|
|
[tr(Lang, ?T("Chatroom configuration modified"))]);
|
|
{roomconfig_change, Occupants} ->
|
|
RoomConfig = roomconfig_to_string(Room#room.config,
|
|
Lang, FileFormat),
|
|
put_room_config(F, RoomConfig, Lang, FileFormat),
|
|
RoomOccupants = roomoccupants_to_string(Occupants,
|
|
FileFormat),
|
|
put_room_occupants(F, RoomOccupants, Lang, FileFormat),
|
|
io_lib:format("<font class=\"mrcm\">~ts</font><br/>",
|
|
[tr(Lang, ?T("Chatroom configuration modified"))]);
|
|
join ->
|
|
io_lib:format("<font class=\"mj\">~ts ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("joins the room"))]);
|
|
leave ->
|
|
io_lib:format("<font class=\"ml\">~ts ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("leaves the room"))]);
|
|
{leave, Reason} ->
|
|
io_lib:format("<font class=\"ml\">~ts ~ts: ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("leaves the room")),
|
|
htmlize(Reason, NoFollow, FileFormat)]);
|
|
{kickban, 301, <<"">>} ->
|
|
io_lib:format("<font class=\"mb\">~ts ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("has been banned"))]);
|
|
{kickban, 301, Reason} ->
|
|
io_lib:format("<font class=\"mb\">~ts ~ts: ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("has been banned")),
|
|
htmlize(Reason, FileFormat)]);
|
|
{kickban, 307, <<"">>} ->
|
|
io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("has been kicked"))]);
|
|
{kickban, 307, Reason} ->
|
|
io_lib:format("<font class=\"mk\">~ts ~ts: ~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T("has been kicked")),
|
|
htmlize(Reason, FileFormat)]);
|
|
{kickban, 321, <<"">>} ->
|
|
io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>",
|
|
[Nick,
|
|
tr(Lang, ?T("has been kicked because of an affiliation "
|
|
"change"))]);
|
|
{kickban, 322, <<"">>} ->
|
|
io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>",
|
|
[Nick,
|
|
tr(Lang, ?T("has been kicked because the room has "
|
|
"been changed to members-only"))]);
|
|
{kickban, 332, <<"">>} ->
|
|
io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>",
|
|
[Nick,
|
|
tr(Lang, ?T("has been kicked because of a system "
|
|
"shutdown"))]);
|
|
{nickchange, OldNick} ->
|
|
io_lib:format("<font class=\"mnc\">~ts ~ts ~ts</font><br/>",
|
|
[htmlize(OldNick, FileFormat),
|
|
tr(Lang, ?T("is now known as")), Nick]);
|
|
{subject, T} ->
|
|
io_lib:format("<font class=\"msc\">~ts~ts~ts</font><br/>",
|
|
[Nick, tr(Lang, ?T(" has set the subject to: ")),
|
|
htmlize(T, NoFollow, FileFormat)]);
|
|
{body, T} ->
|
|
case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of
|
|
{_, <<"">>} ->
|
|
io_lib:format("<font class=\"msm\">~ts</font><br/>",
|
|
[htmlize(T, NoFollow, FileFormat)]);
|
|
{match, _} ->
|
|
io_lib:format("<font class=\"mne\">~ts ~ts</font><br/>",
|
|
[Nick,
|
|
str:substr(htmlize(T, FileFormat), 5)]);
|
|
{nomatch, _} ->
|
|
io_lib:format("<font class=\"mn\">~ts</font> ~ts<br/>",
|
|
[Nick2, htmlize(T, NoFollow, FileFormat)])
|
|
end;
|
|
{room_existence, RoomNewExistence} ->
|
|
io_lib:format("<font class=\"mrcm\">~ts</font><br/>",
|
|
[get_room_existence_string(RoomNewExistence,
|
|
Lang)])
|
|
end,
|
|
{Hour, Minute, Second} = Time,
|
|
STime = io_lib:format("~2..0w:~2..0w:~2..0w",
|
|
[Hour, Minute, Second]),
|
|
{_, _, Microsecs} = Now,
|
|
STimeUnique = io_lib:format("~ts.~w",
|
|
[STime, Microsecs]),
|
|
maybe_print_jl(open, F, Message, FileFormat),
|
|
fw(F, io_lib:format("<a id=\"~ts\" name=\"~ts\" href=\"#~ts\" "
|
|
"class=\"ts\">[~ts]</a> ",
|
|
[STimeUnique, STimeUnique, STimeUnique, STime])
|
|
++ Text,
|
|
FileFormat),
|
|
maybe_print_jl(close, F, Message, FileFormat),
|
|
file:close(F),
|
|
ok.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Utilities
|
|
|
|
get_room_existence_string(created, Lang) ->
|
|
tr(Lang, ?T("Chatroom is created"));
|
|
get_room_existence_string(destroyed, Lang) ->
|
|
tr(Lang, ?T("Chatroom is destroyed"));
|
|
get_room_existence_string(started, Lang) ->
|
|
tr(Lang, ?T("Chatroom is started"));
|
|
get_room_existence_string(stopped, Lang) ->
|
|
tr(Lang, ?T("Chatroom is stopped")).
|
|
|
|
get_dateweek(Date, Lang) ->
|
|
Weekday = case calendar:day_of_the_week(Date) of
|
|
1 -> tr(Lang, ?T("Monday"));
|
|
2 -> tr(Lang, ?T("Tuesday"));
|
|
3 -> tr(Lang, ?T("Wednesday"));
|
|
4 -> tr(Lang, ?T("Thursday"));
|
|
5 -> tr(Lang, ?T("Friday"));
|
|
6 -> tr(Lang, ?T("Saturday"));
|
|
7 -> tr(Lang, ?T("Sunday"))
|
|
end,
|
|
{Y, M, D} = Date,
|
|
Month = case M of
|
|
1 -> tr(Lang, ?T("January"));
|
|
2 -> tr(Lang, ?T("February"));
|
|
3 -> tr(Lang, ?T("March"));
|
|
4 -> tr(Lang, ?T("April"));
|
|
5 -> tr(Lang, ?T("May"));
|
|
6 -> tr(Lang, ?T("June"));
|
|
7 -> tr(Lang, ?T("July"));
|
|
8 -> tr(Lang, ?T("August"));
|
|
9 -> tr(Lang, ?T("September"));
|
|
10 -> tr(Lang, ?T("October"));
|
|
11 -> tr(Lang, ?T("November"));
|
|
12 -> tr(Lang, ?T("December"))
|
|
end,
|
|
unicode:characters_to_binary(
|
|
case Lang of
|
|
<<"en">> ->
|
|
io_lib:format("~ts, ~ts ~w, ~w", [Weekday, Month, D, Y]);
|
|
<<"es">> ->
|
|
io_lib:format("~ts ~w de ~ts de ~w",
|
|
[Weekday, D, Month, Y]);
|
|
_ ->
|
|
io_lib:format("~ts, ~w ~ts ~w", [Weekday, D, Month, Y])
|
|
end).
|
|
|
|
make_dir_rec(Dir) ->
|
|
filelib:ensure_dir(<<Dir/binary, $/>>).
|
|
|
|
%% {ok, F1}=file:open("valid-xhtml10.png", [read]).
|
|
%% {ok, F1b}=file:read(F1, 1000000).
|
|
%% c("../../ejabberd/src/jlib.erl").
|
|
%% base64:encode(F1b).
|
|
|
|
create_image_files(Images_dir) ->
|
|
Filenames = [<<"powered-by-ejabberd.png">>,
|
|
<<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>,
|
|
<<"vcss.png">>],
|
|
lists:foreach(
|
|
fun(Filename) ->
|
|
Src = filename:join([misc:img_dir(), Filename]),
|
|
Dst = fjoin([Images_dir, Filename]),
|
|
case file:copy(Src, Dst) of
|
|
{ok, _} -> ok;
|
|
{error, Why} ->
|
|
?ERROR_MSG("Failed to copy ~ts to ~ts: ~ts",
|
|
[Src, Dst, file:format_error(Why)])
|
|
end
|
|
end, Filenames).
|
|
|
|
fw(F, S) -> fw(F, S, [], html).
|
|
|
|
fw(F, S, O) when is_list(O) -> fw(F, S, O, html);
|
|
fw(F, S, FileFormat) when is_atom(FileFormat) ->
|
|
fw(F, S, [], FileFormat).
|
|
|
|
fw(F, S, O, FileFormat) ->
|
|
S1 = <<(str:format(S, O))/binary, "\n">>,
|
|
S2 = case FileFormat of
|
|
html ->
|
|
S1;
|
|
plaintext ->
|
|
S1a = ejabberd_regexp:greplace(S1, <<"<[^<^>]*>">>, <<"">>),
|
|
S1x = ejabberd_regexp:greplace(S1a, ?PLAINTEXT_CO, <<"~~">>),
|
|
S1y = ejabberd_regexp:greplace(S1x, ?PLAINTEXT_IN, <<"<">>),
|
|
ejabberd_regexp:greplace(S1y, ?PLAINTEXT_OUT, <<">">>)
|
|
end,
|
|
file:write(F, S2).
|
|
|
|
put_header(_, _, _, _, _, _, _, _, _, plaintext) -> ok;
|
|
put_header(F, Room, Date, CSSFile, Lang, Hour_offset,
|
|
Date_prev, Date_next, Top_link, FileFormat) ->
|
|
fw(F,
|
|
<<"<!DOCTYPE html PUBLIC \"-//W3C//DTD "
|
|
"XHTML 1.0 Transitional//EN\" \"http://www.w3."
|
|
"org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>),
|
|
fw(F,
|
|
<<"<html xmlns=\"http://www.w3.org/1999/xhtml\" "
|
|
"xml:lang=\"~ts\" lang=\"~ts\">">>,
|
|
[Lang, Lang]),
|
|
fw(F, <<"<head>">>),
|
|
fw(F,
|
|
<<"<meta http-equiv=\"Content-Type\" content=\"t"
|
|
"ext/html; charset=utf-8\" />">>),
|
|
fw(F, <<"<title>~ts - ~ts</title>">>,
|
|
[htmlize(Room#room.title), Date]),
|
|
put_header_css(F, CSSFile),
|
|
put_header_script(F),
|
|
fw(F, <<"</head>">>),
|
|
fw(F, <<"<body>">>),
|
|
{Top_url, Top_text} = Top_link,
|
|
fw(F,
|
|
<<"<div style=\"text-align: right;\"><a "
|
|
"style=\"color: #AAAAAA; font-family: "
|
|
"monospace; text-decoration: none; font-weight"
|
|
": bold;\" href=\"~ts\">~ts</a></div>">>,
|
|
[Top_url, Top_text]),
|
|
fw(F, <<"<div class=\"roomtitle\">~ts</div>">>,
|
|
[htmlize(Room#room.title)]),
|
|
fw(F,
|
|
<<"<a class=\"roomjid\" href=\"xmpp:~ts?join\">~ts"
|
|
"</a>">>,
|
|
[Room#room.jid, Room#room.jid]),
|
|
fw(F,
|
|
<<"<div class=\"logdate\">~ts<span class=\"w3c\">"
|
|
"<a class=\"nav\" href=\"~ts\"><</a> "
|
|
"<a class=\"nav\" href=\"./\">^</a> <a "
|
|
"class=\"nav\" href=\"~ts\">></a></span></di"
|
|
"v>">>,
|
|
[Date, Date_prev, Date_next]),
|
|
case {htmlize(prepare_subject_author(Room#room.subject_author)),
|
|
htmlize(Room#room.subject)}
|
|
of
|
|
{<<"">>, <<"">>} -> ok;
|
|
{SuA, Su} ->
|
|
fw(F, <<"<div class=\"roomsubject\">~ts~ts~ts</div>">>,
|
|
[SuA, tr(Lang, ?T(" has set the subject to: ")), Su])
|
|
end,
|
|
RoomConfig = roomconfig_to_string(Room#room.config,
|
|
Lang, FileFormat),
|
|
put_room_config(F, RoomConfig, Lang, FileFormat),
|
|
Occupants = get_room_occupants(Room#room.jid),
|
|
RoomOccupants = roomoccupants_to_string(Occupants,
|
|
FileFormat),
|
|
put_room_occupants(F, RoomOccupants, Lang, FileFormat),
|
|
put_occupants_join_leave(F, Lang),
|
|
Time_offset_str = case Hour_offset < 0 of
|
|
true -> io_lib:format("~p", [Hour_offset]);
|
|
false -> io_lib:format("+~p", [Hour_offset])
|
|
end,
|
|
fw(F, <<"<br/><a class=\"ts\">GMT~ts</a><br/>">>,
|
|
[Time_offset_str]).
|
|
|
|
put_header_css(F, {file, Path}) ->
|
|
fw(F, <<"<style type=\"text/css\">">>),
|
|
fw(F, <<"<!--">>),
|
|
case file:read_file(Path) of
|
|
{ok, Data} -> fw(F, Data);
|
|
{error, _} -> ok
|
|
end,
|
|
fw(F, <<"//-->">>),
|
|
fw(F, <<"</style>">>);
|
|
put_header_css(F, {url, URL}) ->
|
|
fw(F,
|
|
<<"<link rel=\"stylesheet\" type=\"text/css\" "
|
|
"href=\"~ts\" media=\"all\">">>,
|
|
[URL]).
|
|
|
|
put_header_script(F) ->
|
|
fw(F, <<"<script type=\"text/javascript\">">>),
|
|
case misc:read_js("muc.js") of
|
|
{ok, Data} -> fw(F, Data);
|
|
{error, _} -> ok
|
|
end,
|
|
fw(F, <<"</script>">>).
|
|
|
|
put_room_config(_F, _RoomConfig, _Lang, plaintext) ->
|
|
ok;
|
|
put_room_config(F, RoomConfig, Lang, _FileFormat) ->
|
|
{_, Now2, _} = erlang:timestamp(),
|
|
fw(F, <<"<div class=\"rc\">">>),
|
|
fw(F,
|
|
<<"<div class=\"rct\" onclick=\"sh('a~p');return "
|
|
"false;\">~ts</div>">>,
|
|
[Now2, tr(Lang, ?T("Room Configuration"))]),
|
|
fw(F,
|
|
<<"<div class=\"rcos\" id=\"a~p\" style=\"displa"
|
|
"y: none;\" ><br/>~ts</div>">>,
|
|
[Now2, RoomConfig]),
|
|
fw(F, <<"</div>">>).
|
|
|
|
put_room_occupants(_F, _RoomOccupants, _Lang,
|
|
plaintext) ->
|
|
ok;
|
|
put_room_occupants(F, RoomOccupants, Lang,
|
|
_FileFormat) ->
|
|
{_, Now2, _} = erlang:timestamp(),
|
|
%% htmlize
|
|
%% The default behaviour is to ignore the nofollow spam prevention on links
|
|
%% (NoFollow=false)
|
|
fw(F, <<"<div class=\"rc\">">>),
|
|
fw(F,
|
|
<<"<div class=\"rct\" onclick=\"sh('o~p');return "
|
|
"false;\">~ts</div>">>,
|
|
[Now2, tr(Lang, ?T("Room Occupants"))]),
|
|
fw(F,
|
|
<<"<div class=\"rcos\" id=\"o~p\" style=\"displa"
|
|
"y: none;\" ><br/>~ts</div>">>,
|
|
[Now2, RoomOccupants]),
|
|
fw(F, <<"</div>">>).
|
|
|
|
put_occupants_join_leave(F, Lang) ->
|
|
fw(F, <<"<div class=\"rc\">">>),
|
|
fw(F,
|
|
<<"<div class=\"rct\" onclick=\"jlf();return "
|
|
"false;\">~ts</div>">>,
|
|
[tr(Lang, ?T("Show Occupants Join/Leave"))]),
|
|
fw(F, <<"</div>">>).
|
|
|
|
maybe_print_jl(_Direction, _F, _Message, plaintext) ->
|
|
ok;
|
|
maybe_print_jl(Direction, F, Message, html) ->
|
|
PrintJl = case Message of
|
|
join -> true;
|
|
leave -> true;
|
|
{leave, _} -> true;
|
|
_ -> false
|
|
end,
|
|
case PrintJl of
|
|
true -> print_jl(Direction, F);
|
|
false -> ok
|
|
end.
|
|
|
|
print_jl(Direction, F) ->
|
|
String = case Direction of
|
|
open -> "<div class=\"jl\">";
|
|
close -> "</div>"
|
|
end,
|
|
fw(F, io_lib:format(String, [])).
|
|
|
|
htmlize(S1) -> htmlize(S1, html).
|
|
|
|
htmlize(S1, plaintext) ->
|
|
ejabberd_regexp:greplace(S1, <<"~">>, ?PLAINTEXT_CO);
|
|
htmlize(S1, FileFormat) ->
|
|
htmlize(S1, false, FileFormat).
|
|
|
|
%% The NoFollow parameter tell if the spam prevention should be applied to the link found
|
|
%% true means 'apply nofollow on links'.
|
|
htmlize(S0, _NoFollow, plaintext) ->
|
|
S1 = ejabberd_regexp:greplace(S0, <<"~">>, ?PLAINTEXT_CO),
|
|
S1x = ejabberd_regexp:greplace(S1, <<"<">>, ?PLAINTEXT_IN),
|
|
ejabberd_regexp:greplace(S1x, <<">">>, ?PLAINTEXT_OUT);
|
|
htmlize(S1, NoFollow, _FileFormat) ->
|
|
S2_list = str:tokens(S1, <<"\n">>),
|
|
lists:foldl(fun (Si, Res) ->
|
|
Si2 = htmlize2(Si, NoFollow),
|
|
case Res of
|
|
<<"">> -> Si2;
|
|
_ -> <<Res/binary, "<br/>", Si2/binary>>
|
|
end
|
|
end,
|
|
<<"">>, S2_list).
|
|
|
|
htmlize2(S1, NoFollow) ->
|
|
%% Regexp link
|
|
%% Add the nofollow rel attribute when required
|
|
S2 = ejabberd_regexp:greplace(S1, <<"\\&">>,
|
|
<<"\\&">>),
|
|
S3 = ejabberd_regexp:greplace(S2, <<"<">>,
|
|
<<"\\<">>),
|
|
S4 = ejabberd_regexp:greplace(S3, <<">">>,
|
|
<<"\\>">>),
|
|
S5 = ejabberd_regexp:greplace(S4,
|
|
<<"((http|https|ftp)://|(mailto|xmpp):)[^] "
|
|
")'\"}]+">>,
|
|
link_regexp(NoFollow)),
|
|
S6 = ejabberd_regexp:greplace(S5, <<" ">>,
|
|
<<"\\ \\ ">>),
|
|
S7 = ejabberd_regexp:greplace(S6, <<"\\t">>,
|
|
<<"\\ \\ \\ \\ ">>),
|
|
S8 = ejabberd_regexp:greplace(S7, <<"~">>,
|
|
<<"~~">>),
|
|
ejabberd_regexp:greplace(S8, <<226, 128, 174>>,
|
|
<<"[RLO]">>).
|
|
|
|
link_regexp(false) -> <<"<a href=\"&\">&</a>">>;
|
|
link_regexp(true) ->
|
|
<<"<a href=\"&\" rel=\"nofollow\">&</a>">>.
|
|
|
|
get_room_info(RoomJID, Opts) ->
|
|
Title = case lists:keysearch(title, 1, Opts) of
|
|
{value, {_, T}} -> T;
|
|
false -> <<"">>
|
|
end,
|
|
Subject = case lists:keysearch(subject, 1, Opts) of
|
|
{value, {_, S}} -> xmpp:get_text(S);
|
|
false -> <<"">>
|
|
end,
|
|
SubjectAuthor = case lists:keysearch(subject_author, 1,
|
|
Opts)
|
|
of
|
|
{value, {_, SA}} -> SA;
|
|
false -> <<"">>
|
|
end,
|
|
#room{jid = jid:encode(RoomJID), title = Title,
|
|
subject = Subject, subject_author = SubjectAuthor,
|
|
config = Opts}.
|
|
|
|
roomconfig_to_string(Options, Lang, FileFormat) ->
|
|
Title = case lists:keysearch(title, 1, Options) of
|
|
{value, Tuple} -> [Tuple];
|
|
false -> []
|
|
end,
|
|
Os1 = lists:keydelete(title, 1, Options),
|
|
Os2 = lists:sort(Os1),
|
|
Options2 = Title ++ Os2,
|
|
lists:foldl(fun ({Opt, Val}, R) ->
|
|
case get_roomconfig_text(Opt, Lang) of
|
|
undefined -> R;
|
|
OptText ->
|
|
R2 = case Val of
|
|
false ->
|
|
<<"<div class=\"rcod\">",
|
|
OptText/binary, "</div>">>;
|
|
true ->
|
|
<<"<div class=\"rcoe\">",
|
|
OptText/binary, "</div>">>;
|
|
<<"">> ->
|
|
<<"<div class=\"rcod\">",
|
|
OptText/binary, "</div>">>;
|
|
T ->
|
|
case Opt of
|
|
password ->
|
|
<<"<div class=\"rcoe\">",
|
|
OptText/binary, "</div>">>;
|
|
max_users ->
|
|
<<"<div class=\"rcot\">",
|
|
OptText/binary, ": \"",
|
|
(htmlize(integer_to_binary(T),
|
|
FileFormat))/binary,
|
|
"\"</div>">>;
|
|
title ->
|
|
<<"<div class=\"rcot\">",
|
|
OptText/binary, ": \"",
|
|
(htmlize(T,
|
|
FileFormat))/binary,
|
|
"\"</div>">>;
|
|
description ->
|
|
<<"<div class=\"rcot\">",
|
|
OptText/binary, ": \"",
|
|
(htmlize(T,
|
|
FileFormat))/binary,
|
|
"\"</div>">>;
|
|
allow_private_messages_from_visitors ->
|
|
<<"<div class=\"rcot\">",
|
|
OptText/binary, ": \"",
|
|
(htmlize(tr(Lang, misc:atom_to_binary(T)),
|
|
FileFormat))/binary,
|
|
"\"</div>">>;
|
|
allowpm ->
|
|
<<"<div class=\"rcot\">",
|
|
OptText/binary, ": \"",
|
|
(htmlize(tr(Lang, misc:atom_to_binary(T)),
|
|
FileFormat))/binary,
|
|
"\"</div>">>;
|
|
_ -> <<"\"", T/binary, "\"">>
|
|
end
|
|
end,
|
|
<<R/binary, R2/binary>>
|
|
end
|
|
end,
|
|
<<"">>, Options2).
|
|
|
|
get_roomconfig_text(title, Lang) -> tr(Lang, ?T("Room title"));
|
|
get_roomconfig_text(persistent, Lang) ->
|
|
tr(Lang, ?T("Make room persistent"));
|
|
get_roomconfig_text(public, Lang) ->
|
|
tr(Lang, ?T("Make room public searchable"));
|
|
get_roomconfig_text(public_list, Lang) ->
|
|
tr(Lang, ?T("Make participants list public"));
|
|
get_roomconfig_text(password_protected, Lang) ->
|
|
tr(Lang, ?T("Make room password protected"));
|
|
get_roomconfig_text(password, Lang) -> tr(Lang, ?T("Password"));
|
|
get_roomconfig_text(anonymous, Lang) ->
|
|
tr(Lang, ?T("This room is not anonymous"));
|
|
get_roomconfig_text(members_only, Lang) ->
|
|
tr(Lang, ?T("Make room members-only"));
|
|
get_roomconfig_text(moderated, Lang) ->
|
|
tr(Lang, ?T("Make room moderated"));
|
|
get_roomconfig_text(members_by_default, Lang) ->
|
|
tr(Lang, ?T("Default users as participants"));
|
|
get_roomconfig_text(allow_change_subj, Lang) ->
|
|
tr(Lang, ?T("Allow users to change the subject"));
|
|
get_roomconfig_text(allowpm, Lang) ->
|
|
tr(Lang, ?T("Who can send private messages"));
|
|
get_roomconfig_text(allow_private_messages_from_visitors, Lang) ->
|
|
tr(Lang, ?T("Allow visitors to send private messages to"));
|
|
get_roomconfig_text(allow_query_users, Lang) ->
|
|
tr(Lang, ?T("Allow users to query other users"));
|
|
get_roomconfig_text(allow_user_invites, Lang) ->
|
|
tr(Lang, ?T("Allow users to send invites"));
|
|
get_roomconfig_text(logging, Lang) -> tr(Lang, ?T("Enable logging"));
|
|
get_roomconfig_text(allow_visitor_nickchange, Lang) ->
|
|
tr(Lang, ?T("Allow visitors to change nickname"));
|
|
get_roomconfig_text(allow_visitor_status, Lang) ->
|
|
tr(Lang, ?T("Allow visitors to send status text in presence updates"));
|
|
get_roomconfig_text(captcha_protected, Lang) ->
|
|
tr(Lang, ?T("Make room CAPTCHA protected"));
|
|
get_roomconfig_text(description, Lang) ->
|
|
tr(Lang, ?T("Room description"));
|
|
%% get_roomconfig_text(subject, Lang) -> "Subject";
|
|
%% get_roomconfig_text(subject_author, Lang) -> "Subject author";
|
|
get_roomconfig_text(max_users, Lang) ->
|
|
tr(Lang, ?T("Maximum Number of Occupants"));
|
|
get_roomconfig_text(_, _) -> undefined.
|
|
|
|
%% Users = [{JID, Nick, Role}]
|
|
roomoccupants_to_string(Users, _FileFormat) ->
|
|
Res = [role_users_to_string(RoleS, Users1)
|
|
|| {RoleS, Users1} <- group_by_role(Users),
|
|
Users1 /= []],
|
|
iolist_to_binary([<<"<div class=\"rcot\">">>, Res, <<"</div>">>]).
|
|
|
|
group_by_role(Users) ->
|
|
{Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick,
|
|
moderator},
|
|
{Mod, Par, Vis, Non}) ->
|
|
{[{JID, Nick}] ++ Mod, Par, Vis,
|
|
Non};
|
|
({JID, Nick, participant},
|
|
{Mod, Par, Vis, Non}) ->
|
|
{Mod, [{JID, Nick}] ++ Par, Vis,
|
|
Non};
|
|
({JID, Nick, visitor},
|
|
{Mod, Par, Vis, Non}) ->
|
|
{Mod, Par, [{JID, Nick}] ++ Vis,
|
|
Non};
|
|
({JID, Nick, none},
|
|
{Mod, Par, Vis, Non}) ->
|
|
{Mod, Par, Vis, [{JID, Nick}] ++ Non}
|
|
end,
|
|
{[], [], [], []}, Users),
|
|
case Ms of
|
|
[] -> [];
|
|
_ -> [{<<"Moderator">>, Ms}]
|
|
end
|
|
++
|
|
case Ms of
|
|
[] -> [];
|
|
_ -> [{<<"Participant">>, Ps}]
|
|
end
|
|
++
|
|
case Ms of
|
|
[] -> [];
|
|
_ -> [{<<"Visitor">>, Vs}]
|
|
end
|
|
++
|
|
case Ms of
|
|
[] -> [];
|
|
_ -> [{<<"None">>, Ns}]
|
|
end.
|
|
|
|
role_users_to_string(RoleS, Users) ->
|
|
SortedUsers = lists:keysort(2, Users),
|
|
UsersString = << <<Nick/binary, "<br/>">>
|
|
|| {_JID, Nick} <- SortedUsers >>,
|
|
<<RoleS/binary, ": ", UsersString/binary>>.
|
|
|
|
get_room_occupants(RoomJIDString) ->
|
|
RoomJID = jid:decode(RoomJIDString),
|
|
RoomName = RoomJID#jid.luser,
|
|
MucService = RoomJID#jid.lserver,
|
|
case get_room_state(RoomName, MucService) of
|
|
{ok, StateData} ->
|
|
[{U#user.jid, U#user.nick, U#user.role}
|
|
|| U <- maps:values(StateData#state.users)];
|
|
error ->
|
|
[]
|
|
end.
|
|
|
|
prepare_subject_author({Nick, _}) ->
|
|
Nick;
|
|
prepare_subject_author(SA) ->
|
|
SA.
|
|
|
|
-spec get_room_state(binary(), binary()) -> {ok, mod_muc_room:state()} | error.
|
|
|
|
get_room_state(RoomName, MucService) ->
|
|
case mod_muc:find_online_room(RoomName, MucService) of
|
|
{ok, RoomPid} ->
|
|
get_room_state(RoomPid);
|
|
error ->
|
|
error
|
|
end.
|
|
|
|
-spec get_room_state(pid()) -> {ok, mod_muc_room:state()} | error.
|
|
|
|
get_room_state(RoomPid) ->
|
|
case mod_muc_room:get_state(RoomPid) of
|
|
{ok, State} -> {ok, State};
|
|
{error, _} -> error
|
|
end.
|
|
|
|
get_proc_name(Host) ->
|
|
gen_mod:get_module_proc(Host, ?MODULE).
|
|
|
|
calc_hour_offset(TimeHere) ->
|
|
TimeZero = calendar:universal_time(),
|
|
TimeHereHour =
|
|
calendar:datetime_to_gregorian_seconds(TimeHere) div
|
|
3600,
|
|
TimeZeroHour =
|
|
calendar:datetime_to_gregorian_seconds(TimeZero) div
|
|
3600,
|
|
TimeHereHour - TimeZeroHour.
|
|
|
|
fjoin(FileList) ->
|
|
list_to_binary(filename:join([binary_to_list(File) || File <- FileList])).
|
|
|
|
-spec tr(binary(), binary()) -> binary().
|
|
tr(Lang, Text) ->
|
|
translate:translate(Lang, Text).
|
|
|
|
has_no_permanent_store_hint(Packet) ->
|
|
xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse
|
|
xmpp:has_subtag(Packet, #hint{type = 'no-storage'}) orelse
|
|
xmpp:has_subtag(Packet, #hint{type = 'no-permanent-store'}) orelse
|
|
xmpp:has_subtag(Packet, #hint{type = 'no-permanent-storage'}).
|
|
|
|
mod_opt_type(access_log) ->
|
|
econf:acl();
|
|
mod_opt_type(cssfile) ->
|
|
econf:url_or_file();
|
|
mod_opt_type(dirname) ->
|
|
econf:enum([room_jid, room_name]);
|
|
mod_opt_type(dirtype) ->
|
|
econf:enum([subdirs, plain]);
|
|
mod_opt_type(file_format) ->
|
|
econf:enum([html, plaintext]);
|
|
mod_opt_type(file_permissions) ->
|
|
econf:and_then(
|
|
econf:options(
|
|
#{mode => econf:non_neg_int(),
|
|
group => econf:non_neg_int()}),
|
|
fun(Opts) ->
|
|
{proplists:get_value(mode, Opts, 644),
|
|
proplists:get_value(group, Opts, 33)}
|
|
end);
|
|
mod_opt_type(outdir) ->
|
|
econf:directory(write);
|
|
mod_opt_type(spam_prevention) ->
|
|
econf:bool();
|
|
mod_opt_type(timezone) ->
|
|
econf:enum([local, universal]);
|
|
mod_opt_type(url) ->
|
|
econf:url();
|
|
mod_opt_type(top_link) ->
|
|
econf:and_then(
|
|
econf:non_empty(
|
|
econf:map(econf:binary(), econf:binary())),
|
|
fun hd/1).
|
|
|
|
-spec mod_options(binary()) -> [{top_link, {binary(), binary()}} |
|
|
{file_permissions,
|
|
{non_neg_integer(), non_neg_integer()}} |
|
|
{atom(), any()}].
|
|
mod_options(_) ->
|
|
[{access_log, muc_admin},
|
|
{cssfile, {file, filename:join(misc:css_dir(), <<"muc.css">>)}},
|
|
{dirname, room_jid},
|
|
{dirtype, subdirs},
|
|
{file_format, html},
|
|
{file_permissions, {644, 33}},
|
|
{outdir, <<"www/muc">>},
|
|
{spam_prevention, true},
|
|
{timezone, local},
|
|
{url, undefined},
|
|
{top_link, {<<"/">>, <<"Home">>}}].
|
|
|
|
mod_doc() ->
|
|
#{desc =>
|
|
[?T("This module enables optional logging "
|
|
"of Multi-User Chat (MUC) public "
|
|
"conversations to HTML. Once you enable "
|
|
"this module, users can join a room using a "
|
|
"MUC capable XMPP client, and if they have "
|
|
"enough privileges, they can request the "
|
|
"configuration form in which they can set "
|
|
"the option to enable room logging."), "",
|
|
?T("Features:"), "",
|
|
?T("- Room details are added on top of each page: "
|
|
"room title, JID, author, subject and configuration."), "",
|
|
?T("- The room JID in the generated HTML is a link "
|
|
"to join the room (using XMPP URI)."), "",
|
|
?T("- Subject and room configuration changes are tracked "
|
|
"and displayed."), "",
|
|
?T("- Joins, leaves, nick changes, kicks, bans and '/me' "
|
|
"are tracked and displayed, including the reason if available."), "",
|
|
?T("- Generated HTML files are XHTML 1.0 Transitional and "
|
|
"CSS compliant."), "",
|
|
?T("- Timestamps are self-referencing links."), "",
|
|
?T("- Links on top for quicker navigation: "
|
|
"Previous day, Next day, Up."), "",
|
|
?T("- CSS is used for style definition, and a custom "
|
|
"CSS file can be used."), "",
|
|
?T("- URLs on messages and subjects are converted to hyperlinks."), "",
|
|
?T("- Timezone used on timestamps is shown on the log files."), "",
|
|
?T("- A custom link can be added on top of each page."), "",
|
|
?T("The module depends on _`mod_muc`_.")],
|
|
opts =>
|
|
[{access_log,
|
|
#{value => ?T("AccessName"),
|
|
desc =>
|
|
?T("This option restricts which occupants are "
|
|
"allowed to enable or disable room logging. "
|
|
"The default value is 'muc_admin'. NOTE: "
|
|
"for this default setting you need to have an "
|
|
"access rule for 'muc_admin' in order to take effect.")}},
|
|
{cssfile,
|
|
#{value => ?T("Path | URL"),
|
|
desc =>
|
|
?T("With this option you can set whether the HTML "
|
|
"files should have a custom CSS file or if they "
|
|
"need to use the embedded CSS. Allowed values "
|
|
"are either 'Path' to local file or an 'URL' to "
|
|
"a remote file. By default a predefined CSS will "
|
|
"be embedded into the HTML page.")}},
|
|
{dirname,
|
|
#{value => "room_jid | room_name",
|
|
desc =>
|
|
?T("Configure the name of the room directory. "
|
|
"If set to 'room_jid', the room directory name will "
|
|
"be the full room JID. Otherwise, the room directory "
|
|
"name will be only the room name, not including the "
|
|
"MUC service name. The default value is 'room_jid'.")}},
|
|
{dirtype,
|
|
#{value => "subdirs | plain",
|
|
desc =>
|
|
?T("The type of the created directories can be specified "
|
|
"with this option. If set to 'subdirs', subdirectories "
|
|
"are created for each year and month. Otherwise, the "
|
|
"names of the log files contain the full date, and "
|
|
"there are no subdirectories. The default value is 'subdirs'.")}},
|
|
{file_format,
|
|
#{value => "html | plaintext",
|
|
desc =>
|
|
?T("Define the format of the log files: 'html' stores "
|
|
"in HTML format, 'plaintext' stores in plain text. "
|
|
"The default value is 'html'.")}},
|
|
{file_permissions,
|
|
#{value => "{mode: Mode, group: Group}",
|
|
desc =>
|
|
?T("Define the permissions that must be used when "
|
|
"creating the log files: the number of the mode, "
|
|
"and the numeric id of the group that will own the "
|
|
"files. The default value is shown in the example below:"),
|
|
example =>
|
|
["file_permissions:",
|
|
" mode: 644",
|
|
" group: 33"]}},
|
|
{outdir,
|
|
#{value => ?T("Path"),
|
|
desc =>
|
|
?T("This option sets the full path to the directory "
|
|
"in which the HTML files should be stored. "
|
|
"Make sure the ejabberd daemon user has write "
|
|
"access on that directory. The default value is 'www/muc'.")}},
|
|
{spam_prevention,
|
|
#{value => "true | false",
|
|
desc =>
|
|
?T("If set to 'true', a special attribute is added to links "
|
|
"that prevent their indexation by search engines. "
|
|
"The default value is 'true', which mean that 'nofollow' "
|
|
"attributes will be added to user submitted links.")}},
|
|
{timezone,
|
|
#{value => "local | universal",
|
|
desc =>
|
|
?T("The time zone for the logs is configurable with "
|
|
"this option. If set to 'local', the local time, as "
|
|
"reported to Erlang emulator by the operating system, "
|
|
"will be used. Otherwise, UTC time will be used. "
|
|
"The default value is 'local'.")}},
|
|
{url,
|
|
#{value => ?T("URL"),
|
|
desc =>
|
|
?T("A top level 'URL' where a client can access "
|
|
"logs of a particular conference. The conference name "
|
|
"is appended to the URL if 'dirname' option is set to "
|
|
"'room_name' or a conference JID is appended to the 'URL' "
|
|
"otherwise. There is no default value.")}},
|
|
{top_link,
|
|
#{value => "{URL: Text}",
|
|
desc =>
|
|
?T("With this option you can customize the link on "
|
|
"the top right corner of each log file. "
|
|
"The default value is shown in the example below:"),
|
|
example =>
|
|
["top_link:",
|
|
" /: Home"]}}]}.
|