diff --git a/ChangeLog b/ChangeLog index a285ac143..040b35900 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,21 @@ +2006-03-14 Alexey Shchepin + + * src/xml_stream.erl: Added catching of gen_fsm:send_event errors + + * src/ejabberd_s2s_out.erl: Better support for multiple SRV + records (thanks to Sergei Golovan) + + * src/mod_muc/mod_muc_log.erl: Support for chatroom logging + (thanks to Badlop) + * src/mod_muc/mod_muc_room.erl: Likewise + * src/mod_muc/Makefile.in: Likewise + * src/mod_muc/Makefile.win32: Likewise + +2006-03-11 Alexey Shchepin + + * src/gen_iq_handler.erl: Added support for {queues, N} IQ handler + type + 2006-03-06 Alexey Shchepin * src/mod_muc/mod_muc_room.erl: Bugfix diff --git a/TODO b/TODO index f1efd0d48..a98d63f0a 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,5 @@ Win32 build: Make it possible to compile with +debug_info flag. -mod_muc logging - admin interface users management statistics about each user @@ -10,7 +8,6 @@ admin interface S2S: check "id" attributes in db:verify packets -more correctly work with SRV DNS records (priority, weight, etc...) make roster set to work in one transaction add traffic shapers to c2s connection before authentification more traffic shapers diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index 185e517c9..12beac468 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -31,7 +31,8 @@ handle_sync_event/4, handle_info/3, terminate/3, - code_change/4]). + code_change/4, + test_get_addr_port/1]). -include("ejabberd.hrl"). -include("jlib.hrl"). @@ -141,7 +142,44 @@ init([From, Server, Type]) -> %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- open_socket(init, StateData) -> - {Addr, Port} = get_addr_port(StateData#state.server), + AddrList = get_addr_port(StateData#state.server), + case lists:foldl(fun({Addr, Port}, Acc) -> + case Acc of + {ok, Socket} -> + {ok, Socket}; + _ -> + open_socket1(Addr, Port) + end + end, {error, badarg}, AddrList) of + {ok, Socket} -> + ReceiverPid = ejabberd_receiver:start(Socket, gen_tcp, none), + ok = gen_tcp:controlling_process(Socket, ReceiverPid), + ejabberd_receiver:become_controller(ReceiverPid), + Version = if + StateData#state.use_v10 -> + " version='1.0'"; + true -> + "" + end, + NewStateData = StateData#state{socket = Socket, + sockmod = gen_tcp, + tls_enabled = false, + receiver = ReceiverPid, + streamid = new_id()}, + send_text(NewStateData, io_lib:format(?STREAM_HEADER, + [StateData#state.server, + Version])), + {next_state, wait_for_stream, NewStateData}; + {error, _Reason} -> + Error = ?ERR_REMOTE_SERVER_NOT_FOUND, + bounce_messages(Error), + {stop, normal, StateData} + end; +open_socket(_, StateData) -> + {next_state, open_socket, StateData}. + +%%---------------------------------------------------------------------- +open_socket1(Addr, Port) -> Res = case idna:domain_utf8_to_ascii(Addr) of false -> {error, badarg}; ASCIIAddr -> @@ -164,37 +202,16 @@ open_socket(init, StateData) -> end, case Res of {ok, Socket} -> - ReceiverPid = ejabberd_receiver:start(Socket, gen_tcp, none), - ok = gen_tcp:controlling_process(Socket, ReceiverPid), - ejabberd_receiver:become_controller(ReceiverPid), - Version = if - StateData#state.use_v10 -> - " version='1.0'"; - true -> - "" - end, - NewStateData = StateData#state{socket = Socket, - sockmod = gen_tcp, - tls_enabled = false, - receiver = ReceiverPid, - streamid = new_id()}, - send_text(NewStateData, io_lib:format(?STREAM_HEADER, - [StateData#state.server, - Version])), - {next_state, wait_for_stream, NewStateData}; + {ok, Socket}; {error, Reason} -> ?DEBUG("s2s_out: inet6 connect return ~p~n", [Reason]), - Error = ?ERR_REMOTE_SERVER_NOT_FOUND, - bounce_messages(Error), - {stop, normal, StateData}; + {error, Reason}; {'EXIT', Reason} -> ?DEBUG("s2s_out: inet6 connect crashed ~p~n", [Reason]), - Error = ?ERR_REMOTE_SERVER_NOT_FOUND, - bounce_messages(Error), - {stop, normal, StateData} - end; -open_socket(_, StateData) -> - {next_state, open_socket, StateData}. + {error, Reason} + end. + +%%---------------------------------------------------------------------- wait_for_stream({xmlstreamstart, Name, Attrs}, StateData) -> @@ -789,18 +806,50 @@ get_addr_port(Server) -> case Res of {error, Reason} -> ?DEBUG("srv lookup of '~s' failed: ~p~n", [Server, Reason]), - {Server, ejabberd_config:get_local_option(outgoing_s2s_port)}; + [{Server, ejabberd_config:get_local_option(outgoing_s2s_port)}]; {ok, HEnt} -> ?DEBUG("srv lookup of '~s': ~p~n", [Server, HEnt#hostent.h_addr_list]), case HEnt#hostent.h_addr_list of [] -> - {Server, - ejabberd_config:get_local_option(outgoing_s2s_port)}; - [{_, _, Port, Host} | _] -> - {Host, Port} + [{Server, + ejabberd_config:get_local_option(outgoing_s2s_port)}]; + AddrList -> + % Probabilities are not exactly proportional to weights + % for simplicity (higher weigths are overvalued) + {A1, A2, A3} = now(), + random:seed(A1, A2, A3), + case (catch lists:map( + fun({Priority, Weight, Port, Host}) -> + N = case Weight of + 0 -> 0; + _ -> (Weight + 1) * random:uniform() + end, + {Priority * 65536 - N, Host, Port} + end, AddrList)) of + {'EXIT', _Reasn} -> + [{Server, + ejabberd_config:get_local_option(outgoing_s2s_port)}]; + SortedList -> + List = lists:map( + fun({_, Host, Port}) -> + {Host, Port} + end, lists:keysort(1, SortedList)), + ?DEBUG("srv lookup of '~s': ~p~n", [Server, List]), + List + end end end. - +test_get_addr_port(Server) -> + lists:foldl( + fun(_, Acc) -> + [HostPort | _] = get_addr_port(Server), + case lists:keysearch(HostPort, 1, Acc) of + false -> + [{HostPort, 1} | Acc]; + {value, {_, Num}} -> + lists:keyreplace(HostPort, 1, Acc, {HostPort, Num + 1}) + end + end, [], lists:seq(1, 100000)). diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index f0c77f78c..63f4e357c 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -49,6 +49,17 @@ add_iq_handler(Component, Host, NS, Module, Function, Type) -> [Host, Module, Function]), Component:register_iq_handler(Host, NS, Module, Function, {one_queue, Pid}); + {queues, N} -> + Pids = + lists:map( + fun(_) -> + {ok, Pid} = supervisor:start_child( + ejabberd_iq_sup, + [Host, Module, Function]), + Pid + end, lists:seq(1, N)), + Component:register_iq_handler(Host, NS, Module, Function, + {queues, Pids}); parallel -> Component:register_iq_handler(Host, NS, Module, Function, parallel) end. @@ -60,6 +71,10 @@ stop_iq_handler(_Module, _Function, Opts) -> case Opts of {one_queue, Pid} -> gen_server:call(Pid, stop); + {queues, Pids} -> + lists:foreach(fun(Pid) -> + catch gen_server:call(Pid, stop) + end, Pids); _ -> ok end. @@ -70,6 +85,9 @@ handle(Host, Module, Function, Opts, From, To, IQ) -> process_iq(Host, Module, Function, From, To, IQ); {one_queue, Pid} -> Pid ! {process_iq, From, To, IQ}; + {queues, Pids} -> + Pid = lists:nth(erlang:phash(now(), length(Pids)), Pids), + Pid ! {process_iq, From, To, IQ}; parallel -> spawn(?MODULE, process_iq, [Host, Module, Function, From, To, IQ]); _ -> diff --git a/src/mod_muc/Makefile.in b/src/mod_muc/Makefile.in index 14e9a9763..6ab1d8a72 100644 --- a/src/mod_muc/Makefile.in +++ b/src/mod_muc/Makefile.in @@ -12,6 +12,7 @@ OUTDIR = .. EFLAGS = -I .. -pz .. OBJS = \ $(OUTDIR)/mod_muc.beam \ + $(OUTDIR)/mod_muc_log.beam \ $(OUTDIR)/mod_muc_room.beam all: $(OBJS) diff --git a/src/mod_muc/Makefile.win32 b/src/mod_muc/Makefile.win32 index 67e6c430e..76f6bb7f4 100644 --- a/src/mod_muc/Makefile.win32 +++ b/src/mod_muc/Makefile.win32 @@ -6,6 +6,7 @@ EFLAGS = -I .. -pz .. OBJS = \ $(OUTDIR)\mod_muc.beam \ + $(OUTDIR)\mod_muc_log.beam \ $(OUTDIR)\mod_muc_room.beam ALL : $(OBJS) @@ -16,5 +17,8 @@ CLEAN : $(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 diff --git a/src/mod_muc/mod_muc_log.erl b/src/mod_muc/mod_muc_log.erl new file mode 100644 index 000000000..1888562fc --- /dev/null +++ b/src/mod_muc/mod_muc_log.erl @@ -0,0 +1,580 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc_log.erl +%%% Author : Badlop +%%% Purpose : MUC room logging +%%% Created : 12 Mar 2006 by Alexey Shchepin +%%% Id : $Id$ +%%%---------------------------------------------------------------------- + +-module(mod_muc_log). +-author('badlop'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start_link/2, + start/2, + stop/1, + check_access_log/2, + add_to_log/5]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/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}). + + +-record(state, {host, + out_dir, + dir_type, + css_file, + access, + lang, + timezone, + top_link}). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = + {Proc, + {?MODULE, start_link, [Host, Opts]}, + temporary, + 1000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +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(Host, From) -> + case catch gen_server:call(get_proc_name(Host), + {check_access_log, Host, From}) of + {'EXIT', _Error} -> + false; + Res -> + Res + end. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + 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), + AccessLog = gen_mod:get_opt(access_log, Opts, muc_admin), + Timezone = gen_mod:get_opt(timezone, Opts, local), + Top_link = gen_mod:get_opt(top_link, Opts, {"/", "Home"}), + Lang = case ejabberd_config:get_local_option({language, Host}) of + undefined -> + ""; + L -> + L + end, + {ok, #state{host = Host, + out_dir = OutDir, + dir_type = DirType, + css_file = CSSFile, + access = AccessLog, + lang = Lang, + timezone = Timezone, + top_link = Top_link}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> + Reply = acl:match_rule(ServerHost, State#state.access, FromJID), + {reply, Reply, State}; +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +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) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> + 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, Room, Opts, State); + {SubEl, _} -> + Message = {subject, htmlize(xml:get_tag_cdata(SubEl))}, + add_message_to_log(Nick, Message, Room, Opts, State) + end; + +add_to_log2(roomconfig_change, _, Room, Opts, State) -> + add_message_to_log("", roomconfig_change, 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) -> + {{Year, Month, Day}, _Time} = TimeStamp, + + % Directory and file names + {Dir, Filename, Rel} = + case DirType of + subdirs -> + SYear = lists:flatten(io_lib:format("~4..0w", [Year])), + SMonth = lists:flatten(io_lib:format("~2..0w", [Month])), + SDay = lists:flatten(io_lib:format("~2..0w", [Day])), + {filename:join(SYear, SMonth), SDay, "../.."}; + plain -> + Date = lists:flatten( + io_lib:format("~4..0w-~2..0w-~2..0w", + [Year, Month, Day])), + {"", Date, "."} + end, + Fd = filename:join([OutDir, RoomJID, Dir]), + Fn = filename:join([Fd, Filename ++ ".html"]), + Fnrel = filename:join([Rel, Dir, Filename ++ ".html"]), + {Fd, Fn, Fnrel}. + +% 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) -> + case file:read_file_info(Fn) of + {ok, _} -> + {ok, F} = file:open(Fn, [append]), + fw(F, "
ejabberd/mod_muc log\"Valid \"Valid
"), + file:close(F); + _ -> ok + end. + +add_message_to_log(Nick, Message, RoomJID, Opts, State) -> + #state{out_dir = OutDir, + dir_type = DirType, + css_file = CSSFile, + lang = Lang, + timezone = Timezone, + top_link = TopLink} = State, + Room = get_room_info(RoomJID, Opts), + + 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), + {Date, Time} = TimeStamp, + + % 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, Lang), + + TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1), + {_FdYesterday, FnYesterday, DatePrev} = + build_filename_string( + TimeStampYesterday, OutDir, Room#room.jid, DirType), + + TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), + {_FdTomorrow, _FnTomorrow, DateNext} = + build_filename_string( + TimeStampTomorrow, OutDir, Room#room.jid, DirType), + + HourOffset = calc_hour_offset(TimeStamp), + put_header(F, Room, Datestring, CSSFile, Lang, + HourOffset, DatePrev, DateNext, TopLink), + + close_previous_log(FnYesterday) + end, + + % Build message + Text = case Message of + roomconfig_change -> + RoomConfig = roomconfig_to_string(Room#room.config, Lang), + put_room_config(F, RoomConfig, Lang), + 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, + {Hour, Minute, Second} = Time, + STime = lists:flatten( + io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second])), + + % Write message + file:write(F, io_lib:format("[~s] ~s~n", + [STime, STime, STime, Text])), + + % Close file + file:close(F), + ok. + + +%%---------------------------------------------------------------------- +%% Utilities + +get_dateweek(Date, Lang) -> + Weekday = case calendar:day_of_the_week(Date) 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} = Date, + 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 Lang 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) -> io:format(F, S ++ "~n", O). +fw(F, S) -> fw(F, S, []). + +put_header(F, Room, Date, CSSFile, Lang, Hour_offset, Date_prev, Date_next, Top_link) -> + fw(F, ""), + fw(F, "", [Lang, Lang]), + 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, ""), + {Top_url, Top_text} = Top_link, + fw(F, "
~s
", [Top_url, Top_text]), + fw(F, "", [Room#room.jid, Room#room.title]), + fw(F, "~s", [Room#room.jid, Room#room.jid]), + fw(F, "
~s< ^ >
", [Date, Date_prev, Date_next]), + case {Room#room.subject_author, Room#room.subject} of + {"", ""} -> ok; + {SuA, Su} -> fw(F, "
~s~s~s
", [SuA, ?T(" has set the subject to: "), htmlize(Su)]) + end, + RoomConfig = roomconfig_to_string(Room#room.config, Lang), + put_room_config(F, RoomConfig, 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, "
GMT~s
", [Time_offset_str]). + +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, Lang) -> + {_, 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, " ", "\\ ")). + S4. + +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}} -> S; + false -> "" + end, + SubjectAuthor = + case lists:keysearch(subject_author, 1, Opts) of + {value, {_, SA}} -> SA; + false -> "" + end, + #room{jid = jlib:jid_to_string(RoomJID), + title = Title, + subject = Subject, + subject_author = SubjectAuthor, + config = Opts + }. + +roomconfig_to_string(Options, Lang) -> + % Get title, if available + Title = case lists:keysearch(title, 1, Options) of + {value, Tuple} -> [Tuple]; + false -> [] + end, + + % Remove title from list + Os1 = lists:keydelete(title, 1, Options), + + % Order list + Os2 = lists:sort(Os1), + + % Add title to ordered list + Options2 = Title ++ Os2, + + lists:foldl( + fun({Opt, Val}, R) -> + case get_roomconfig_text(Opt) of + undefined -> + R; + OptT -> + OptText = ?T(OptT), + R2 = case Val of + false -> "
" ++ OptText ++ "
"; + true -> "
" ++ OptText ++ "
"; + "" -> "
" ++ OptText ++ "
"; + T -> + case Opt of + password -> "
" ++ OptText ++ "
"; + title -> "
" ++ ?T("Room title") ++ ": \"" ++ T ++ "\"
"; + _ -> "\"" ++ T ++ "\"" + end + end, + R ++ R2 + end + 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 invites"; +get_roomconfig_text(logging) -> "Enable logging"; +get_roomconfig_text(_) -> undefined. + +get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME). + +calc_hour_offset(TimeHere) -> + TimeZero = calendar:now_to_universal_time(now()), + TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600, + TimeZeroHour = calendar:datetime_to_gregorian_seconds(TimeZero) div 3600, + TimeHereHour - TimeZeroHour. diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index a31443f48..8efe16a14 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -51,7 +51,7 @@ password_protected = false, password = "", anonymous = true, - logging = false % TODO + logging = false }). -record(user, {jid, @@ -370,7 +370,11 @@ normal_state({route, From, Nick, 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; @@ -856,10 +860,17 @@ add_online_user(JID, Nick, Role, StateData) -> nick = Nick, role = Role}, StateData#state.users), + add_to_log(join, Nick, 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), + {ok, #user{nick = Nick}} = + ?DICT:find(LJID, StateData#state.users), + add_to_log(leave, {Nick, Reason}, StateData), Users = ?DICT:erase(LJID, StateData#state.users), StateData#state{users = Users}. @@ -1298,6 +1309,7 @@ change_nick(JID, Nick, StateData) -> end, StateData#state.users), NewStateData = StateData#state{users = Users}, send_nick_changing(JID, OldNick, NewStateData), + add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. send_nick_changing(JID, OldNick, StateData) -> @@ -1397,6 +1409,7 @@ add_message_to_history(FromNick, Packet, StateData) -> Size = lists:flatlength(xml:element_to_string(SPacket)), Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size}, StateData#state.history), + add_to_log(text, {FromNick, Packet}, StateData), StateData#state{history = Q1}. send_history(JID, Shift, StateData) -> @@ -1951,6 +1964,9 @@ send_kickban_presence(JID, Reason, Code, StateData) -> end end, lists:foreach(fun(J) -> + {ok, #user{nick = Nick}} = + ?DICT:find(J, StateData#state.users), + add_to_log(kickban, {Nick, Reason, Code}, StateData), send_kickban_presence1(J, Reason, Code, StateData) end, LJIDs). @@ -1998,7 +2014,10 @@ process_iq_owner(From, set, Lang, SubEl, StateData) -> {?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; @@ -2019,7 +2038,7 @@ process_iq_owner(From, get, Lang, SubEl, StateData) -> {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 -> @@ -2048,6 +2067,14 @@ process_iq_owner(From, get, Lang, SubEl, StateData) -> {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), @@ -2070,7 +2097,7 @@ process_iq_owner(From, get, Lang, SubEl, StateData) -> ?XFIELD("text-private", Label, Var, Val)). -get_config(Lang, StateData) -> +get_config(Lang, StateData, From) -> Config = StateData#state.config, Res = [{xmlelement, "title", [], @@ -2120,11 +2147,17 @@ get_config(Lang, StateData) -> Config#config.allow_query_users), ?BOOLXFIELD("Allow users to send invites", "allow_user_invites", - Config#config.allow_user_invites), - ?BOOLXFIELD("Enable logging", - "logging", - Config#config.logging) - ], + Config#config.allow_user_invites) + ] ++ + case mod_muc_log:check_access_log( + StateData#state.server_host, From) of + allow -> + [?BOOLXFIELD( + "Enable logging", + "logging", + Config#config.logging)]; + _ -> [] + end, {result, [{xmlelement, "instructions", [], [{xmlcdata, translate:translate( @@ -2144,7 +2177,10 @@ set_config(XEl, StateData) -> _ -> case set_xoption(XData, StateData#state.config) of #config{} = Config -> - change_config(Config, StateData); + Res = change_config(Config, StateData), + {result, _, NSD} = Res, + add_to_log(roomconfig_change, [], NSD), + Res; Err -> Err end @@ -2487,3 +2523,16 @@ check_invitation(From, Els, StateData) -> error end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Logging + +add_to_log(Type, Data, StateData) -> + case (StateData#state.config)#config.logging of + true -> + mod_muc_log:add_to_log( + StateData#state.server_host, Type, Data, + StateData#state.jid, make_opts(StateData)); + false -> + ok + end. diff --git a/src/xml_stream.erl b/src/xml_stream.erl index d00fd4ca8..a513532a3 100644 --- a/src/xml_stream.erl +++ b/src/xml_stream.erl @@ -64,8 +64,8 @@ process_data(CallbackPid, Stack, Data) -> {?XML_START, {Name, Attrs}} -> if Stack == [] -> - gen_fsm:send_event(CallbackPid, - {xmlstreamstart, Name, Attrs}); + catch gen_fsm:send_event(CallbackPid, + {xmlstreamstart, Name, Attrs}); true -> ok end, @@ -76,12 +76,12 @@ process_data(CallbackPid, Stack, Data) -> NewEl = {xmlelement, Name, Attrs, lists:reverse(Els)}, case Tail of [] -> - gen_fsm:send_event(CallbackPid, - {xmlstreamend, EndName}), + catch gen_fsm:send_event(CallbackPid, + {xmlstreamend, EndName}), Tail; [_] -> - gen_fsm:send_event(CallbackPid, - {xmlstreamelement, NewEl}), + catch gen_fsm:send_event(CallbackPid, + {xmlstreamelement, NewEl}), Tail; [{xmlelement, Name1, Attrs1, Els1} | Tail1] -> [{xmlelement, Name1, Attrs1, [NewEl | Els1]} | @@ -98,7 +98,7 @@ process_data(CallbackPid, Stack, Data) -> [] -> [] end; {?XML_ERROR, Err} -> - gen_fsm:send_event(CallbackPid, {xmlstreamerror, Err}) + catch gen_fsm:send_event(CallbackPid, {xmlstreamerror, Err}) end.