Index: src/mod_muc/mod_muc_room.erl =================================================================== --- src/mod_muc/mod_muc_room.erl (revisión: 470) +++ src/mod_muc/mod_muc_room.erl (copia de trabajo) @@ -49,7 +49,7 @@ password_protected = false, password = "", anonymous = true, - logging = false % TODO + logging = false }). -record(user, {jid, @@ -357,7 +357,11 @@ NewState = add_user_presence_un(From, Packet, StateData), send_new_presence(From, NewState), - remove_online_user(From, NewState); + Reason = case xml:get_subtag(Packet, "status") of + false -> ""; + Status_el -> xml:get_tag_cdata(Status_el) + end, + remove_online_user(From, NewState, Reason); _ -> StateData end; @@ -848,10 +852,15 @@ nick = Nick, role = Role}, StateData#state.users), + mod_muc_log:add_to_log(join, {Nick, StateData}, StateData), StateData#state{users = Users}. remove_online_user(JID, StateData) -> + remove_online_user(JID, StateData, ""). + +remove_online_user(JID, StateData, Reason) -> LJID = jlib:jid_tolower(JID), + mod_muc_log:add_to_log(leave, {LJID, Reason, StateData}, StateData), Users = ?DICT:erase(LJID, StateData#state.users), StateData#state{users = Users}. @@ -1278,6 +1287,7 @@ end, StateData#state.users), NewStateData = StateData#state{users = Users}, send_nick_changing(JID, OldNick, NewStateData), + mod_muc_log:add_to_log(nickchange, {OldNick, Nick, StateData}, StateData), NewStateData. send_nick_changing(JID, OldNick, StateData) -> @@ -1377,6 +1387,7 @@ Size = lists:flatlength(xml:element_to_string(SPacket)), Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size}, StateData#state.history), + mod_muc_log:add_to_log(text, {FromNick, Packet, StateData}, StateData), StateData#state{history = Q1}. send_history(JID, Shift, StateData) -> @@ -1883,6 +1894,7 @@ end end, lists:foreach(fun(J) -> + mod_muc_log:add_to_log(kickban, {J, Reason, Code, StateData}, StateData), send_kickban_presence1(J, Reason, Code, StateData) end, LJIDs). @@ -1930,7 +1942,10 @@ {?NS_XDATA, "cancel"} -> {result, [], StateData}; {?NS_XDATA, "submit"} -> - set_config(XEl, StateData); + case check_allowed_log_change(XEl, StateData, From) of + allow -> set_config(XEl, StateData); + deny -> {error, ?ERR_BAD_REQUEST} + end; _ -> {error, ?ERR_BAD_REQUEST} end; @@ -1951,7 +1966,7 @@ {xmlelement, Name, Attrs, Els} = SubEl, case xml:remove_cdata(Els) of [] -> - get_config(Lang, StateData); + get_config(Lang, StateData, From); [Item] -> case xml:get_tag_attr("affiliation", Item) of false -> @@ -1980,6 +1995,11 @@ {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. +check_allowed_log_change(XEl, StateData, From) -> + case lists:keymember("logging", 1, jlib:parse_xdata_submit(XEl)) of + false -> allow; + true -> mod_muc_log:check_access_log(StateData#state.server_host, From) + end. -define(XFIELD(Type, Label, Var, Val), @@ -2002,7 +2022,7 @@ ?XFIELD("text-private", Label, Var, Val)). -get_config(Lang, StateData) -> +get_config(Lang, StateData, From) -> Config = StateData#state.config, Res = [{xmlelement, "title", [], @@ -2052,11 +2072,18 @@ end), ?BOOLXFIELD("Make room anonymous?", "anonymous", - Config#config.anonymous), - ?BOOLXFIELD("Enable logging?", - "logging", - Config#config.logging) - ], + Config#config.anonymous) + ] ++ case mod_muc_log:check_access_log(StateData#state.server_host, From) of + allow -> + [?BOOLXFIELD( + case gen_mod:get_module_opt(StateData#state.server_host, mod_muc, allow_room_log, true) of + true -> "Enable logging"; + false -> "Enable logging" ++ " (disabled by the admin)" + end, + "logging", + Config#config.logging)]; + _ -> [] + end, {result, [{xmlelement, "instructions", [], [{xmlcdata, translate:translate( @@ -2070,6 +2097,7 @@ set_config(XEl, StateData) -> XData = jlib:parse_xdata_submit(XEl), + mod_muc_log:add_to_log(roomconfig_change, {XData, StateData}, StateData), case XData of invalid -> {error, ?ERR_BAD_REQUEST}; Index: src/mod_muc/Makefile.in =================================================================== --- src/mod_muc/Makefile.in (revisión: 470) +++ src/mod_muc/Makefile.in (copia de trabajo) @@ -12,6 +12,7 @@ EFLAGS = -I .. -pz .. OBJS = \ $(OUTDIR)/mod_muc.beam \ + $(OUTDIR)/mod_muc_log.beam \ $(OUTDIR)/mod_muc_room.beam all: $(OBJS) Index: src/mod_muc/mod_muc.erl =================================================================== --- src/mod_muc/mod_muc.erl (revisión: 470) +++ src/mod_muc/mod_muc.erl (copia de trabajo) @@ -45,6 +45,7 @@ Access = gen_mod:get_opt(access, Opts, all), AccessCreate = gen_mod:get_opt(access_create, Opts, all), AccessAdmin = gen_mod:get_opt(access_admin, Opts, none), + mod_muc_log:start(Opts), register(gen_mod:get_module_proc(Host, ?PROCNAME), spawn(?MODULE, init, [MyHost, Host, {Access, AccessCreate, AccessAdmin}])). @@ -258,6 +259,7 @@ ok. stop(Host) -> + mod_muc_log:stop(), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), Proc ! stop, {wait, Proc}. Index: src/mod_muc/mod_muc_log.erl =================================================================== --- src/mod_muc/mod_muc_log.erl (revisión: 0) +++ src/mod_muc/mod_muc_log.erl (revisión: 0) @@ -0,0 +1,481 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_log.erl +%%% Author : Badlop +%%% Purpose : MUC room logging +%%% Created : +%%% Id : +%%%---------------------------------------------------------------------- + +-module(mod_muc_log). +-author(''). +-vsn(''). + +%-behaviour(gen_mod). + +-export([start/1, + stop/0, + init_log/1, + check_access_log/2, + add_to_log/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-define(T(Text), translate:translate("Lang", Text)). + +-define(PROCNAME, ejabberd_mod_muc_log). +-record(room, {jid, title, subject, subject_author, config}). + +%Copied from mod_muc_room.erl +-define(DICT, dict). +-record(config, {title = "", + allow_change_subj = true, + allow_query_users = true, + allow_private_messages = true, + public = true, + public_list = true, + persistent = false, + moderated = false, % TODO + members_by_default = true, + members_only = false, + allow_user_invites = false, + password_protected = false, + password = "", + anonymous = true, + logging = false + }). +-record(user, {jid, + nick, + role, + last_presence}). +-record(state, {room, + host, + server_host, + access, + jid, + config = #config{}, + users = ?DICT:new(), + affiliations = ?DICT:new(), + history = lqueue_new(20), + subject = "", + subject_author = "", + just_created = false}). + + +%%---------------------------------------------------------------------- +%% Module control + +start(Opts) -> + case gen_mod:get_opt(allow_room_log, Opts, true) of + true -> + OutDir = gen_mod:get_opt(outdir, Opts, "www/muc"), + DirType = gen_mod:get_opt(dirtype, Opts, subdirs), + CSSFile = gen_mod:get_opt(cssfile, Opts, false), + CSSFile = gen_mod:get_opt(cssfile, Opts, false), + AccessLog = gen_mod:get_opt(access_log, Opts, muc_admin), + case lists:member(?PROCNAME, registered()) of + false -> + register(?PROCNAME, spawn(?MODULE, init_log, [{OutDir, DirType, CSSFile, AccessLog}])); + true -> + ok + end; + false -> + ok + end. + +init_log(O) -> + loop_log(O). + +loop_log(Options) -> + receive + {log, Data} -> + add_message_to_log2(Data, Options), + loop_log(Options); + {check_access_log, ServerHost, From, Pid} -> + {_, _, _, AccessLog} = Options, + Pid ! acl:match_rule(ServerHost, AccessLog, From), + loop_log(Options); + stop -> + % TODO + ok; + _ -> + loop_log(Options) + end. + +stop() -> + ?PROCNAME ! stop. + +check_access_log(ServerHost, From) -> + case whereis(?PROCNAME) of + undefined -> false; + _ -> + ?PROCNAME ! {check_access_log, ServerHost, From, self()}, + receive A -> A end + end. + + +%%---------------------------------------------------------------------- +%% Frontend + +% Check if this room is configured is logged +add_to_log(A, D, StateData) -> + case (StateData#state.config)#config.logging of + true -> add_to_log(A, D); + false -> ok + end. + +% Check if MUC logging is allowed +add_to_log(A, D) -> + case whereis(?PROCNAME) of + undefined -> ok; + _ -> add_to_log2(A, D) + end. + +add_to_log2(text, {Nick, Packet, StateData}) -> + case {xml:get_subtag(Packet, "subject"), xml:get_subtag(Packet, "body")} of + {false, false} -> + ok; + {false, SubEl} -> + Message = {body, htmlize(xml:get_tag_cdata(SubEl))}, + add_message_to_log(Nick, Message, StateData); + {SubEl, _false} -> + Message = {subject, htmlize(xml:get_tag_cdata(SubEl))}, + add_message_to_log(Nick, Message, StateData) + end; + +add_to_log2(roomconfig_change, {XData, StateData}) -> + add_message_to_log("eeeaaae", {roomconfig_change, XData}, StateData); + +add_to_log2(nickchange, {OldNick, NewNick, StateData}) -> + add_message_to_log(NewNick, {nickchange, OldNick}, StateData); + +add_to_log2(join, {Nick, StateData}) -> + add_message_to_log(Nick, {join}, StateData); + +add_to_log2(leave, {LJID, Reason, StateData}) -> + {ok, #user{nick = Nick}} = + ?DICT:find(LJID, StateData#state.users), + case Reason of + "" -> add_message_to_log(Nick, {leave}, StateData); + _ -> add_message_to_log(Nick, {leave, Reason}, StateData) + end; + +add_to_log2(kickban, {LJID, Reason, Code, StateData}) -> + {ok, #user{nick = Nick}} = + ?DICT:find(LJID, StateData#state.users), + add_message_to_log(Nick, {kickban, Code, Reason}, StateData). + + +%%---------------------------------------------------------------------- +%% Core + +add_message_to_log(Nick, Message, StateData) -> + ?PROCNAME ! {log, {Nick, Message, StateData}}. + +add_message_to_log2({Nick, Message, StateData}, Options) -> + {OutDir, DirType, CSSFile, _AccessLog} = Options, + Room = get_room_info(StateData), + + % Timestamp, Date and Time + TimeStamp = jlib:timestamp_to_iso(calendar:now_to_universal_time(now())), + Year = string:substr(TimeStamp, 1, 4), + Month = string:substr(TimeStamp, 5, 2), + Day = string:substr(TimeStamp, 7, 2), + A1 = string:concat(Year, "-"), + A2 = string:concat(A1, Month), + A3 = string:concat(A2, "-"), + Date = string:concat(A3, Day), + + % Directory and file names + {Dir, Filename} = case DirType of + subdirs -> + {filename:join(Year, Month), Day}; + plain -> + {"", Date} + end, + Fd = filename:join([OutDir, Room#room.jid, Dir]), + Fn = filename:join([Fd, string:concat(Filename, ".html")]), + + % Open file, create if it does not exist, create parent dirs if needed + 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]), + Datestring = get_dateweek(date()), + put_header(F, Room, Datestring, CSSFile) + end, + + % Build message + Text = case Message of + {roomconfig_change, XData} -> + RoomConfig = roomconfigwire_to_string(XData), + put_room_config(F, RoomConfig), + io_lib:format("~s
", + [?T("Chatroom configuration modified")]); + {join} -> + io_lib:format("~s ~s
", + [Nick, ?T("joins the room")]); + {leave} -> + io_lib:format("~s ~s
", + [Nick, ?T("leaves the room")]); + {leave, Reason} -> + io_lib:format("~s ~s: ~s
", + [Nick, ?T("leaves the room"), Reason]); + {kickban, "307", ""} -> + io_lib:format("~s ~s
", + [Nick, ?T("has been kicked")]); + {kickban, "307", Reason} -> + io_lib:format("~s ~s: ~s
", + [Nick, ?T("has been kicked"), Reason]); + {kickban, "301", ""} -> + io_lib:format("~s ~s
", + [Nick, ?T("has been banned")]); + {kickban, "301", Reason} -> + io_lib:format("~s ~s: ~s
", + [Nick, ?T("has been banned"), Reason]); + {nickchange, OldNick} -> + io_lib:format("~s ~s ~s
", + [OldNick, ?T("is now known as"), Nick]); + {subject, T} -> + io_lib:format("~s~s~s
", + [Nick, ?T(" has set the subject to: "), T]); + {body, T} -> + case regexp:first_match(T, "^/me\s") of + {match, _, _} -> + io_lib:format("~s ~s
", + [Nick, string:substr(T, 5)]); + nomatch -> + io_lib:format("<~s> ~s
", + [Nick, T]) + end + end, + Time = string:substr(TimeStamp, 10, 8), + + % Write message + file:write(F, io_lib:format("[~s] ~s~n", + [Time, Time, Time, Text])), + + % Close file + file:close(F), + + ok. + + +%%---------------------------------------------------------------------- +%% Utilities + +get_dateweek(Dt) -> + Weekday = case calendar:day_of_the_week(Dt) of + 1 -> ?T("Monday"); + 2 -> ?T("Tuesday"); + 3 -> ?T("Wednesday"); + 4 -> ?T("Thursday"); + 5 -> ?T("Friday"); + 6 -> ?T("Saturday"); + 7 -> ?T("Sunday") + end, + {Y, M, D} = Dt, + Month = case M of + 1 -> ?T("January"); + 2 -> ?T("February"); + 3 -> ?T("March"); + 4 -> ?T("April"); + 5 -> ?T("May"); + 6 -> ?T("June"); + 7 -> ?T("July"); + 8 -> ?T("August"); + 9 -> ?T("September"); + 10 -> ?T("October"); + 11 -> ?T("November"); + 12 -> ?T("December") + end, + case ?MYLANG of + "en" -> io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y]); + "es" -> io_lib:format("~s ~w de ~s de ~w", [Weekday, D, Month, Y]); + _ -> io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y]) + end. + +make_dir_rec(Dir) -> + case file:read_file_info(Dir) of + {ok, _} -> + ok; + {error, enoent} -> + DirS = filename:split(Dir), + DirR = lists:sublist(DirS, length(DirS)-1), + make_dir_rec(filename:join(DirR)), + file:make_dir(Dir) + end. + +fw(F, S, O) -> file:write(F, io_lib:format(S++"~n", O)). +fw(F, S) -> fw(F, S, []). + +put_header(F, Room, Date, CSSFile) -> + fw(F, ""), + fw(F, ""), + %fw(F, ""), + fw(F, ""), + %fw(F, ""), + fw(F, "~s - ~s", [Room#room.title, Date]), + put_header_css(F, CSSFile), + put_header_script(F), + fw(F, ""), + fw(F, ""), + fw(F, "
ejabberd/mod_muc log
"), + fw(F, "
~s
", [Room#room.title]), + fw(F, "
~s
", [Room#room.jid]), + fw(F, "
~s
", [Date]), + case {Room#room.subject_author, Room#room.subject} of + {"", ""} -> ok; + {SuA, Su} -> fw(F, "
~s~s~s
", [SuA, ?T(" has set the subject to: "), Su]) + end, + RoomConfig = roomconfig_to_string(Room#room.config), + put_room_config(F, RoomConfig), + fw(F, "
"). + +put_header_css(F, false) -> + fw(F, ""); + +put_header_css(F, CSSFile) -> + fw(F, "", [CSSFile]). + +put_header_script(F) -> + fw(F, ""). + +put_room_config(F, RoomConfig) -> + {_, Now2, _} = now(), + fw(F, "
"), + fw(F, "
~s
", [Now2, ?T("Room Configuration")]), + fw(F, "

~s
", [Now2, RoomConfig]), + fw(F, "
"). + +htmlize(S1) -> + S2_list = string:tokens(S1, "\n"), + lists:foldl( + fun(Si, Res) -> + Si2 = htmlize2(Si), + case Res of + "" -> Si2; + _ -> Res ++ "
" ++ Si2 + end + end, + "", + S2_list). + +htmlize2(S1) -> + S2 = element(2, regexp:gsub(S1, "<", "\\<")), + S3 = element(2, regexp:gsub(S2, ">", "\\>")), + S4 = element(2, regexp:gsub(S3, "(http|ftp)://.[^ ]*", "&")), + element(2, regexp:gsub(S4, " ", "\\ ")). + +get_room_info(StateData) -> + Room1 = element(2, StateData), + Room2 = element(3, StateData), + Room3 = string:concat(Room1, "@"), + RoomJID = string:concat(Room3, Room2), + #room{ + jid = RoomJID, + title = (StateData#state.config)#config.title, + subject = StateData#state.subject, + subject_author = StateData#state.subject_author, + config = StateData#state.config + }. + +roomconfig_to_string(RoomConfig) -> + Options = [ + {RoomConfig#config.allow_change_subj, allow_change_subj}, + {RoomConfig#config.allow_query_users, allow_query_users}, + {RoomConfig#config.allow_private_messages, allow_private_messages}, + {RoomConfig#config.public, public}, + {RoomConfig#config.public_list, public_list}, + {RoomConfig#config.persistent, persistent}, + {RoomConfig#config.moderated, moderated}, + {RoomConfig#config.members_by_default, members_by_default}, + {RoomConfig#config.members_only, members_only}, + {RoomConfig#config.allow_user_invites, allow_user_invites}, + {RoomConfig#config.password_protected, password_protected}, + {RoomConfig#config.anonymous, anonymous} + ], + rc_to_string(Options). + +roomconfigwire_to_string(XData) -> + Options = [ + {case Value_str of "0" -> false; "1" -> true; [] -> false; T -> {text, T} end, list_to_atom(Name_str)} + || {Name_str, [Value_str]} <- XData ], + rc_to_string(Options). + +rc_to_string(Options) -> + % Get title, if available + Title = case lists:keysearch(title, 2, Options) of + {value, Tuple} -> [Tuple]; + false -> [] + end, + + % Remove title from list + Os1 = lists:keydelete(title, 2, Options), + + % Order list + Os2 = lists:keysort(2, Os1), + + % Add title to ordered list + Options2 = Title ++ Os2, + + lists:foldl( + fun({RC, O}, R) -> + O2 = ?T(get_roomconfig_text(O)), + R2 = case RC of + false -> "
" ++ O2 ++ "
"; + true -> "
" ++ O2 ++ "
"; + {text, T} -> + case O of + password -> []; % Don't print passwords on log + title -> "
" ++ ?T("Room title") ++ ": \"" ++ T ++ "\"
"; + _ -> "\"" ++ T ++ "\"" + end + end, + R ++ R2 + end, + "", + Options2). + +get_roomconfig_text(title) -> "Room title"; +get_roomconfig_text(persistent) -> "Make room persistent"; +get_roomconfig_text(public) -> "Make room public searchable"; +get_roomconfig_text(public_list) -> "Make participants list public"; +get_roomconfig_text(password_protected) -> "Make room password protected"; +get_roomconfig_text(password) -> "Password"; +get_roomconfig_text(anonymous) -> "Make room semianonymous"; +get_roomconfig_text(members_only) -> "Make room members-only"; +get_roomconfig_text(moderated) -> "Make room moderated"; +get_roomconfig_text(members_by_default) -> "Default users as participants"; +get_roomconfig_text(allow_change_subj) -> "Allow users to change subject"; +get_roomconfig_text(allow_private_messages) -> "Allow users to send private messages"; +get_roomconfig_text(allow_query_users) -> "Allow users to query other users"; +get_roomconfig_text(allow_user_invites) -> "Allow users to send "; +get_roomconfig_text(logging) -> "Enable logging". Index: src/mod_muc/Makefile.win32 =================================================================== --- src/mod_muc/Makefile.win32 (revisión: 470) +++ src/mod_muc/Makefile.win32 (copia de trabajo) @@ -6,6 +6,7 @@ OBJS = \ $(OUTDIR)\mod_muc.beam \ + $(OUTDIR)\mod_muc_log.beam \ $(OUTDIR)\mod_muc_room.beam ALL : $(OBJS) @@ -16,5 +17,8 @@ $(OUTDIR)\mod_muc.beam : mod_muc.erl erlc -W $(EFLAGS) -o $(OUTDIR) mod_muc.erl +$(OUTDIR)\mod_muc_log.beam : mod_muc_log.erl + erlc -W $(EFLAGS) -o $(OUTDIR) mod_muc_log.erl + $(OUTDIR)\mod_muc_room.beam : mod_muc_room.erl erlc -W $(EFLAGS) -o $(OUTDIR) mod_muc_room.erl