diff options
author | Badlop <badlop@process-one.net> | 2009-10-20 15:28:48 +0000 |
---|---|---|
committer | Badlop <badlop@process-one.net> | 2009-10-20 15:28:48 +0000 |
commit | 04545f2668a09c5a235007d663a2de0897dd9d18 (patch) | |
tree | be39dbe996ef704c8a121263e107093f710faa90 /src/mod_muc | |
parent | add release_notes_2.0.1 (diff) | |
parent | does not use slash as default separator in nodename (EJAB-667) (diff) |
Create branch for ejabberd 2.1.x release line.
SVN Revision: 2688
Diffstat (limited to 'src/mod_muc')
-rw-r--r-- | src/mod_muc/Makefile.in | 4 | ||||
-rw-r--r-- | src/mod_muc/Makefile.win32 | 3 | ||||
-rw-r--r-- | src/mod_muc/mod_muc.erl | 165 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_log.erl | 361 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.erl | 797 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.hrl | 82 |
6 files changed, 1035 insertions, 377 deletions
diff --git a/src/mod_muc/Makefile.in b/src/mod_muc/Makefile.in index 8aa3bf5e1..5ede5e521 100644 --- a/src/mod_muc/Makefile.in +++ b/src/mod_muc/Makefile.in @@ -9,7 +9,9 @@ LIBS = @LIBS@ ERLANG_CFLAGS = @ERLANG_CFLAGS@ ERLANG_LIBS = @ERLANG_LIBS@ -EFLAGS = -I .. -pz .. +EFLAGS += -I .. +EFLAGS += -pz .. + # make debug=true to compile Erlang module with debug informations. ifdef debug EFLAGS+=+debug_info diff --git a/src/mod_muc/Makefile.win32 b/src/mod_muc/Makefile.win32 index e53f9b7f5..5107b1069 100644 --- a/src/mod_muc/Makefile.win32 +++ b/src/mod_muc/Makefile.win32 @@ -4,8 +4,7 @@ include ..\Makefile.inc EFLAGS = -I .. -pz .. OUTDIR = .. -SOURCES = $(wildcard *.erl) -BEAMS = $(addprefix $(OUTDIR)/,$(SOURCES:.erl=.beam)) +BEAMS = ..\mod_muc.beam ..\mod_muc_log.beam ..\mod_muc_room.beam ALL : $(BEAMS) diff --git a/src/mod_muc/mod_muc.erl b/src/mod_muc/mod_muc.erl index 90a74b70f..c0e2168ae 100644 --- a/src/mod_muc/mod_muc.erl +++ b/src/mod_muc/mod_muc.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2008 Process-one +%%% ejabberd, Copyright (C) 2002-2009 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -16,7 +16,7 @@ %%% 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., 59 Temple Place, Suite 330, Boston, MA @@ -38,6 +38,7 @@ store_room/3, restore_room/2, forget_room/2, + create_room/5, process_iq_disco_items/4, can_use_nick/3]). @@ -102,6 +103,13 @@ room_destroyed(Host, Room, Pid, ServerHost) -> {room_destroyed, {Room, Host}, Pid}, ok. +%% @doc Create a room. +%% If Opts = default, the default room options are used. +%% Else use the passed options as defined in mod_muc_room. +create_room(Host, Name, From, Nick, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, {create, Name, From, Nick, Opts}). + store_room(Host, Name, Opts) -> F = fun() -> mnesia:write(#muc_room{name_host = {Name, Host}, @@ -124,10 +132,11 @@ forget_room(Host, Name) -> mnesia:transaction(F). process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> + Rsm = jlib:rsm_decode(IQ), Res = IQ#iq{type = result, sub_el = [{xmlelement, "query", [{"xmlns", ?NS_DISCO_ITEMS}], - iq_disco_items(Host, From, Lang)}]}, + iq_disco_items(Host, From, Lang, Rsm)}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)). @@ -209,7 +218,28 @@ init([Host, Opts]) -> %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call(stop, _From, State) -> - {stop, normal, ok, State}. + {stop, normal, ok, State}; + +handle_call({create, Room, From, Nick, Opts}, + _From, + #state{host = Host, + server_host = ServerHost, + access = Access, + default_room_opts = DefOpts, + history_size = HistorySize, + room_shaper = RoomShaper} = State) -> + ?DEBUG("MUC: create new room '~s'~n", [Room]), + NewOpts = case Opts of + default -> DefOpts; + _ -> Opts + end, + {ok, Pid} = mod_muc_room:start( + Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, From, + Nick, NewOpts), + register_room(Host, Room, Pid), + {reply, ok, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | @@ -323,10 +353,14 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, case jlib:iq_query_info(Packet) of #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, sub_el = _SubEl, lang = Lang} = IQ -> + Info = ejabberd_hooks:run_fold( + disco_info, ServerHost, [], + [ServerHost, ?MODULE, "", ""]), Res = IQ#iq{type = result, sub_el = [{xmlelement, "query", [{"xmlns", XMLNS}], - iq_disco_info(Lang)}]}, + iq_disco_info(Lang) + ++Info}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); @@ -430,10 +464,11 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, Type = xml:get_attr_s("type", Attrs), case {Name, Type} of {"presence", ""} -> - case acl:match_rule(ServerHost, AccessCreate, From) of - allow -> - ?DEBUG("MUC: open new room '~s'~n", [Room]), - {ok, Pid} = mod_muc_room:start( + case check_user_can_create_room(ServerHost, + AccessCreate, From, + Room) of + true -> + {ok, Pid} = start_new_room( Host, ServerHost, Access, Room, HistorySize, RoomShaper, From, @@ -441,7 +476,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, register_room(Host, Room, Pid), mod_muc_room:route(Pid, From, Nick, Packet), ok; - _ -> + false -> Lang = xml:get_attr_s("xml:lang", Attrs), ErrText = "Room creation is denied by service policy", Err = jlib:make_error_reply( @@ -463,7 +498,14 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, end end. - +check_user_can_create_room(ServerHost, AccessCreate, From, RoomID) -> + case acl:match_rule(ServerHost, AccessCreate, From) of + allow -> + (length(RoomID) =< gen_mod:get_module_opt(ServerHost, mod_muc, + max_room_id, infinite)); + _ -> + false + end. load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> @@ -495,6 +537,23 @@ load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> end, Rs) end. +start_new_room(Host, ServerHost, Access, Room, + HistorySize, RoomShaper, From, + Nick, DefRoomOpts) -> + case mnesia:dirty_read(muc_room, {Room, Host}) of + [] -> + ?DEBUG("MUC: open new room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, From, + Nick, DefRoomOpts); + [#muc_room{opts = Opts}|_] -> + ?DEBUG("MUC: restore room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, Opts) + end. + register_room(Host, Room, Pid) -> F = fun() -> mnesia:write(#muc_online_room{name_host = {Room, Host}, @@ -508,12 +567,15 @@ iq_disco_info(Lang) -> [{"category", "conference"}, {"type", "text"}, {"name", translate:translate(Lang, "Chatrooms")}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}, + {xmlelement, "feature", [{"var", ?NS_DISCO_ITEMS}], []}, {xmlelement, "feature", [{"var", ?NS_MUC}], []}, {xmlelement, "feature", [{"var", ?NS_REGISTER}], []}, + {xmlelement, "feature", [{"var", ?NS_RSM}], []}, {xmlelement, "feature", [{"var", ?NS_VCARD}], []}]. -iq_disco_items(Host, From, Lang) -> +iq_disco_items(Host, From, Lang, none) -> lists:zf(fun(#muc_online_room{name_host = {Name, _Host}, pid = Pid}) -> case catch gen_fsm:sync_send_all_state_event( Pid, {get_disco_item, From, Lang}, 100) of @@ -526,7 +588,72 @@ iq_disco_items(Host, From, Lang) -> _ -> false end - end, get_vh_rooms(Host)). + end, get_vh_rooms(Host)); + +iq_disco_items(Host, From, Lang, Rsm) -> + {Rooms, RsmO} = get_vh_rooms(Host, Rsm), + RsmOut = jlib:rsm_encode(RsmO), + lists:zf(fun(#muc_online_room{name_host = {Name, _Host}, pid = Pid}) -> + case catch gen_fsm:sync_send_all_state_event( + Pid, {get_disco_item, From, Lang}, 100) of + {item, Desc} -> + flush(), + {true, + {xmlelement, "item", + [{"jid", jlib:jid_to_string({Name, Host, ""})}, + {"name", Desc}], []}}; + _ -> + false + end + end, Rooms) ++ RsmOut. + +get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> + AllRooms = lists:sort(get_vh_rooms(Host)), + Count = erlang:length(AllRooms), + Guard = case Direction of + _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}]; + aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}]; + before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}]; + _ -> [{'==', {element, 2, '$1'}, Host}] + end, + L = lists:sort( + mnesia:dirty_select(muc_online_room, + [{#muc_online_room{name_host = '$1', _ = '_'}, + Guard, + ['$_']}])), + L2 = if + Index == undefined andalso Direction == before -> + lists:reverse(lists:sublist(lists:reverse(L), 1, M)); + Index == undefined -> + lists:sublist(L, 1, M); + Index > Count orelse Index < 0 -> + []; + true -> + lists:sublist(L, Index+1, M) + end, + if + L2 == [] -> + {L2, #rsm_out{count=Count}}; + true -> + H = hd(L2), + NewIndex = get_room_pos(H, AllRooms), + T=lists:last(L2), + {F, _}=H#muc_online_room.name_host, + {Last, _}=T#muc_online_room.name_host, + {L2, #rsm_out{first=F, last=Last, count=Count, index=NewIndex}} + end. + +%% @doc Return the position of desired room in the list of rooms. +%% The room must exist in the list. The count starts in 0. +%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer() +get_room_pos(Desired, Rooms) -> + get_room_pos(Desired, Rooms, 0). +get_room_pos(Desired, [HeadRoom | _], HeadPosition) + when (Desired#muc_online_room.name_host == + HeadRoom#muc_online_room.name_host) -> + HeadPosition; +get_room_pos(Desired, [_ | Rooms], HeadPosition) -> + get_room_pos(Desired, Rooms, HeadPosition + 1). flush() -> receive @@ -608,7 +735,7 @@ iq_set_register_info(Host, From, Nick, Lang) -> {atomic, ok} -> {result, []}; {atomic, false} -> - ErrText = "Specified nickname is already registered", + ErrText = "That nickname is registered by another person", {error, ?ERRT_CONFLICT(Lang, ErrText)}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} @@ -631,11 +758,11 @@ process_iq_register_set(Host, From, SubEl, Lang) -> {error, ?ERR_BAD_REQUEST}; _ -> case lists:keysearch("nick", 1, XData) of - false -> + {value, {_, [Nick]}} when Nick /= "" -> + iq_set_register_info(Host, From, Nick, Lang); + _ -> ErrText = "You must fill in field \"Nickname\" in the form", - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - {value, {_, [Nick]}} -> - iq_set_register_info(Host, From, Nick, Lang) + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)} end end; _ -> @@ -655,7 +782,7 @@ iq_get_vcard(Lang) -> [{xmlcdata, ?EJABBERD_URI}]}, {xmlelement, "DESC", [], [{xmlcdata, translate:translate(Lang, "ejabberd MUC module") ++ - "\nCopyright (c) 2003-2008 Alexey Shchepin"}]}]. + "\nCopyright (c) 2003-2009 Alexey Shchepin"}]}]. broadcast_service_message(Host, Msg) -> diff --git a/src/mod_muc/mod_muc_log.erl b/src/mod_muc/mod_muc_log.erl index fce9c15b2..d1942f4bb 100644 --- a/src/mod_muc/mod_muc_log.erl +++ b/src/mod_muc/mod_muc_log.erl @@ -5,7 +5,7 @@ %%% Created : 12 Mar 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2008 Process-one +%%% ejabberd, Copyright (C) 2002-2009 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -16,7 +16,7 @@ %%% 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., 59 Temple Place, Suite 330, Boston, MA @@ -43,15 +43,21 @@ -include("ejabberd.hrl"). -include("jlib.hrl"). +-include("mod_muc_room.hrl"). + +%% Copied from mod_muc/mod_muc.erl +-record(muc_online_room, {name_host, pid}). -define(T(Text), translate:translate(Lang, Text)). -define(PROCNAME, ejabberd_mod_muc_log). -record(room, {jid, title, subject, subject_author, config}). --record(state, {host, +-record(logstate, {host, out_dir, dir_type, + dir_name, + file_format, css_file, access, lang, @@ -113,6 +119,8 @@ check_access_log(Host, From) -> init([Host, Opts]) -> OutDir = gen_mod:get_opt(outdir, Opts, "www/muc"), DirType = gen_mod:get_opt(dirtype, Opts, subdirs), + DirName = gen_mod:get_opt(dirname, Opts, room_jid), + FileFormat = gen_mod:get_opt(file_format, Opts, html), % Allowed values: html|plaintext 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), @@ -120,13 +128,17 @@ init([Host, Opts]) -> NoFollow = gen_mod:get_opt(spam_prevention, Opts, true), Lang = case ejabberd_config:get_local_option({language, Host}) of undefined -> - ""; - L -> - L + case ejabberd_config:get_global_option(language) of + undefined -> "en"; + L -> L + end; + L -> L end, - {ok, #state{host = Host, + {ok, #logstate{host = Host, out_dir = OutDir, dir_type = DirType, + dir_name = DirName, + file_format = FileFormat, css_file = CSSFile, access = AccessLog, lang = Lang, @@ -144,7 +156,7 @@ init([Host, Opts]) -> %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> - Reply = acl:match_rule(ServerHost, State#state.access, FromJID), + Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. @@ -207,9 +219,12 @@ add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> add_message_to_log(Nick, Message, Room, Opts, State) end; -add_to_log2(roomconfig_change, _, Room, Opts, State) -> +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(nickchange, {OldNick, NewNick}, Room, Opts, State) -> add_message_to_log(NewNick, {nickchange, OldNick}, Room, Opts, State); @@ -229,10 +244,10 @@ add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, State) -> %%---------------------------------------------------------------------- %% Core -build_filename_string(TimeStamp, OutDir, RoomJID, DirType) -> +build_filename_string(TimeStamp, OutDir, RoomJID, DirType, DirName, FileFormat) -> {{Year, Month, Day}, _Time} = TimeStamp, - % Directory and file names + %% Directory and file names {Dir, Filename, Rel} = case DirType of subdirs -> @@ -246,54 +261,75 @@ build_filename_string(TimeStamp, OutDir, RoomJID, DirType) -> [Year, Month, Day])), {"", Date, "."} end, - Fd = filename:join([OutDir, RoomJID, Dir]), - Fn = filename:join([Fd, Filename ++ ".html"]), - Fnrel = filename:join([Rel, Dir, Filename ++ ".html"]), + + RoomString = case DirName of + room_jid -> RoomJID; + room_name -> get_room_name(RoomJID) + end, + Extension = case FileFormat of + html -> ".html"; + plaintext -> ".txt" + end, + Fd = filename:join([OutDir, RoomString, Dir]), + Fn = filename:join([Fd, Filename ++ Extension]), + Fnrel = filename:join([Rel, Dir, Filename ++ Extension]), {Fd, Fn, Fnrel}. -% calculate day before +get_room_name(RoomJID) -> + JID = jlib:string_to_jid(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) -> +%% 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]), - %fw(F, "<div class=\"legend\">ejabberd/mod_muc log<span class=\"w3c\">"), - fw(F, "<div class=\"legend\">"), - fw(F, " <a href=\"http://www.ejabberd.im\"><img style=\"border:0\" src=\"~s/powered-by-ejabberd.png\" alt=\"Powered by ejabberd\"/></a>", [Images_dir]), - fw(F, " <a href=\"http://www.erlang.org/\"><img style=\"border:0\" src=\"~s/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;height:31px\" src=\"~s/valid-xhtml10.png\" alt=\"Valid XHTML 1.0 Transitional\" /></a>", [Images_dir]), - fw(F, " <a href=\"http://jigsaw.w3.org/css-validator/\"><img style=\"border:0;width:88px;height:31px\" src=\"~s/vcss.png\" alt=\"Valid CSS!\"/></a>", [Images_dir]), - fw(F, "</span></div></body></html>"), + 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\">ejabberd/mod_muc log<span class=\"w3c\">"), + fw(F, "<div class=\"legend\">"), + fw(F, " <a href=\"http://www.ejabberd.im\"><img style=\"border:0\" src=\"~s/powered-by-ejabberd.png\" alt=\"Powered by ejabberd\"/></a>", [Images_dir]), + fw(F, " <a href=\"http://www.erlang.org/\"><img style=\"border:0\" src=\"~s/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;height:31px\" src=\"~s/valid-xhtml10.png\" alt=\"Valid XHTML 1.0 Transitional\" /></a>", [Images_dir]), + fw(F, " <a href=\"http://jigsaw.w3.org/css-validator/\"><img style=\"border:0;width:88px;height:31px\" src=\"~s/vcss.png\" alt=\"Valid CSS!\"/></a>", [Images_dir]), + fw(F, "</span></div></body></html>"). + add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> - Nick = htmlize(Nick1), - #state{out_dir = OutDir, + #logstate{out_dir = OutDir, dir_type = DirType, + dir_name = DirName, + file_format = FileFormat, 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("<"++Nick1++">", FileFormat), + Now = now(), TimeStamp = case Timezone of - local -> calendar:now_to_local_time(now()); - universal -> calendar:now_to_universal_time(now()) + 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), + {Fd, Fn, _Dir} = build_filename_string(TimeStamp, OutDir, Room#room.jid, DirType, DirName, FileFormat), {Date, Time} = TimeStamp, - % Open file, create if it does not exist, create parent dirs if needed + %% 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]); @@ -305,32 +341,39 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1), {_FdYesterday, FnYesterday, DatePrev} = build_filename_string( - TimeStampYesterday, OutDir, Room#room.jid, DirType), + 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), + TimeStampTomorrow, OutDir, Room#room.jid, DirType, DirName, FileFormat), HourOffset = calc_hour_offset(TimeStamp), put_header(F, Room, Datestring, CSSFile, Lang, - HourOffset, DatePrev, DateNext, TopLink), + HourOffset, DatePrev, DateNext, TopLink, FileFormat), - Images_dir = filename:join([OutDir, "images"]), + Images_dir = filename:join([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) + create_image_files(Images_dir), + Images_url = case DirType of + subdirs -> "../../../images"; + plain -> "../images" + end, + close_previous_log(FnYesterday, Images_url, FileFormat) end, - % Build message + %% Build message Text = case Message of roomconfig_change -> - RoomConfig = roomconfig_to_string(Room#room.config, Lang), - put_room_config(F, RoomConfig, Lang), + RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), + put_room_config(F, RoomConfig, Lang, FileFormat), + io_lib:format("<font class=\"mrcm\">~s</font><br/>", + [?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\">~s</font><br/>", [?T("Chatroom configuration modified")]); join -> @@ -341,19 +384,19 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> [Nick, ?T("leaves the room")]); {leave, Reason} -> io_lib:format("<font class=\"ml\">~s ~s: ~s</font><br/>", - [Nick, ?T("leaves the room"), htmlize(Reason,NoFollow)]); + [Nick, ?T("leaves the room"), htmlize(Reason,NoFollow,FileFormat)]); {kickban, "301", ""} -> io_lib:format("<font class=\"mb\">~s ~s</font><br/>", [Nick, ?T("has been banned")]); {kickban, "301", Reason} -> io_lib:format("<font class=\"mb\">~s ~s: ~s</font><br/>", - [Nick, ?T("has been banned"), htmlize(Reason)]); + [Nick, ?T("has been banned"), htmlize(Reason,FileFormat)]); {kickban, "307", ""} -> io_lib:format("<font class=\"mk\">~s ~s</font><br/>", [Nick, ?T("has been kicked")]); {kickban, "307", Reason} -> io_lib:format("<font class=\"mk\">~s ~s: ~s</font><br/>", - [Nick, ?T("has been kicked"), htmlize(Reason)]); + [Nick, ?T("has been kicked"), htmlize(Reason,FileFormat)]); {kickban, "321", ""} -> io_lib:format("<font class=\"mk\">~s ~s</font><br/>", [Nick, ?T("has been kicked because of an affiliation change")]); @@ -365,29 +408,34 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> [Nick, ?T("has been kicked because of a system shutdown")]); {nickchange, OldNick} -> io_lib:format("<font class=\"mnc\">~s ~s ~s</font><br/>", - [htmlize(OldNick), ?T("is now known as"), Nick]); + [htmlize(OldNick,FileFormat), ?T("is now known as"), Nick]); {subject, T} -> io_lib:format("<font class=\"msc\">~s~s~s</font><br/>", - [Nick, ?T(" has set the subject to: "), htmlize(T,NoFollow)]); + [Nick, ?T(" has set the subject to: "), htmlize(T,NoFollow,FileFormat)]); {body, T} -> - case regexp:first_match(T, "^/me\s") of - {match, _, _} -> + case {regexp:first_match(T, "^/me\s"), Nick} of + {_, ""} -> + io_lib:format("<font class=\"msm\">~s</font><br/>", + [htmlize(T,NoFollow,FileFormat)]); + {{match, _, _}, _} -> io_lib:format("<font class=\"mne\">~s ~s</font><br/>", - [Nick, string:substr(htmlize(T), 5)]); - nomatch -> - io_lib:format("<font class=\"mn\"><~s></font> ~s<br/>", - [Nick, htmlize(T,NoFollow)]) + [Nick, string:substr(htmlize(T,FileFormat), 5)]); + {nomatch, _} -> + io_lib:format("<font class=\"mn\">~s</font> ~s<br/>", + [Nick2, htmlize(T,NoFollow,FileFormat)]) end end, {Hour, Minute, Second} = Time, STime = lists:flatten( io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second])), + {_, _, Microsecs} = Now, + STimeUnique = io_lib:format("~s.~w", [STime, Microsecs]), - % Write message - file:write(F, io_lib:format("<a name=\"~s\" href=\"#~s\" class=\"ts\">[~s]</a> ~s~n", - [STime, STime, STime, Text])), + %% Write message + fw(F, io_lib:format("<a id=\"~s\" name=\"~s\" href=\"#~s\" class=\"ts\">[~s]</a> ", + [STimeUnique, STimeUnique, STimeUnique, STime]) ++ Text, FileFormat), - % Close file + %% Close file file:close(F), ok. @@ -438,13 +486,13 @@ make_dir_rec(Dir) -> end. -% {ok, F1}=file:open("valid-xhtml10.png", [read]). -% {ok, F1b}=file:read(F1, 1000000). -% c("../../ejabberd/src/jlib.erl"). -% jlib:encode_base64(F1b). +%% {ok, F1}=file:open("valid-xhtml10.png", [read]). +%% {ok, F1b}=file:read(F1, 1000000). +%% c("../../ejabberd/src/jlib.erl"). +%% jlib:encode_base64(F1b). image_base64("powered-by-erlang.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoAAADN0lEQVRo3u1a" + "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoAAADN0lEQVRo3u1a" "P0waURz+rjGRRQ+nUyRCYmJyDPTapDARaSIbTUjt1gVSh8ZW69aBAR0cWLSx" "CXWp59LR1jbdqKnGxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu" "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOHo8HkiQxDBXEOjg9" @@ -466,7 +514,7 @@ image_base64("powered-by-erlang.png") -> "KF/d/wX3cJvREzl1vAAAAABJRU5ErkJggg=="; image_base64("valid-xhtml10.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEAAACiFBMVEUAAADe" + "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEAAACiFBMVEUAAADe" "5+fOezmtra3ejEKlhELvvWO9WlrehELOe3vepaWclHvetVLGc3PerVKcCAj3" "vVqUjHOUe1JjlL0xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY" "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoICACEazEhGAgIAACE" @@ -522,7 +570,7 @@ image_base64("valid-xhtml10.png") -> "QZ+RYfpNE/4Xosmq7jsZAJsAAAAASUVORK5CYII="; image_base64("vcss.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSAAABKVBMVEUAAAAj" + "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSAAABKVBMVEUAAAAj" "Ix8MR51ZVUqAdlmdnZ3ejEWLDAuNjY1kiMG0n2d9fX19Ghfrp1FtbW3y39+3" "Ph6lIRNdXV2qJBFcVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd" "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T09RRDwsJBG+vr73" @@ -550,7 +598,7 @@ image_base64("vcss.png") -> "AElFTkSuQmCC"; image_base64("powered-by-ejabberd.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaAAAAw1BMVEUAAAAj" + "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaAAAAw1BMVEUAAAAj" "BgYtBAM5AwFCAAAYGAJNAABcAABIDQ5qAAAoJRV7AACFAAAoKSdJHByLAAAw" "Lwk1NQA1MzFJKyo4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb" "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEgAB3dnd4d2+OjACD" @@ -574,27 +622,43 @@ image_base64("powered-by-ejabberd.png") -> "AElFTkSuQmCC". create_image_files(Images_dir) -> - Filenames = [ - "powered-by-ejabberd.png", - "powered-by-erlang.png", - "valid-xhtml10.png", - "vcss.png" - ], - lists:foreach( - fun(Filename) -> - Filename_full = filename:join([Images_dir, Filename]), - {ok, F} = file:open(Filename_full, [write]), - Image = jlib:decode_base64(image_base64(Filename)), - io:format(F, "~s", [Image]), - file:close(F) - end, - Filenames), - ok. - -fw(F, S, O) -> io:format(F, S ++ "~n", O). -fw(F, S) -> fw(F, S, []). + Filenames = ["powered-by-ejabberd.png", + "powered-by-erlang.png", + "valid-xhtml10.png", + "vcss.png" + ], + lists:foreach( + fun(Filename) -> + Filename_full = filename:join([Images_dir, Filename]), + {ok, F} = file:open(Filename_full, [write]), + Image = jlib:decode_base64(image_base64(Filename)), + io:format(F, "~s", [Image]), + file:close(F) + end, + Filenames), + ok. -put_header(F, Room, Date, CSSFile, Lang, Hour_offset, Date_prev, Date_next, Top_link) -> +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 = io_lib:format(S ++ "~n", O), + S2 = case FileFormat of + html -> + S1; + plaintext -> + {ok, Res, _} = regexp:gsub(S1, "<[^>]*>", ""), + Res + end, + io:format(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=\"~s\" lang=\"~s\">", [Lang, Lang]), fw(F, "<head>"), @@ -613,8 +677,11 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, Date_prev, Date_next, Top_ {"", ""} -> ok; {SuA, Su} -> fw(F, "<div class=\"roomsubject\">~s~s~s</div>", [SuA, ?T(" has set the subject to: "), Su]) end, - RoomConfig = roomconfig_to_string(Room#room.config, Lang), - put_room_config(F, RoomConfig, Lang), + 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), Time_offset_str = case Hour_offset<0 of true -> io_lib:format("~p", [Hour_offset]); false -> io_lib:format("+~p", [Hour_offset]) @@ -627,6 +694,7 @@ put_header_css(F, false) -> fw(F, ".ts {color: #AAAAAA; text-decoration: none;}"), fw(F, ".mrcm {color: #009900; font-style: italic; font-weight: bold;}"), fw(F, ".msc {color: #009900; font-style: italic; font-weight: bold;}"), + fw(F, ".msm {color: #000099; font-style: italic; font-weight: bold;}"), fw(F, ".mj {color: #009900; font-style: italic;}"), fw(F, ".ml {color: #009900; font-style: italic;}"), fw(F, ".mk {color: #009900; font-style: italic;}"), @@ -664,22 +732,40 @@ put_header_script(F) -> fw(F, "else {document.getElementById(e).style.display='none';}}"), fw(F, "</script>"). -put_room_config(F, RoomConfig, Lang) -> +put_room_config(_F, _RoomConfig, _Lang, plaintext) -> + ok; +put_room_config(F, RoomConfig, Lang, _FileFormat) -> {_, Now2, _} = now(), fw(F, "<div class=\"rc\">"), fw(F, "<div class=\"rct\" onclick=\"sh('a~p');return false;\">~s</div>", [Now2, ?T("Room Configuration")]), fw(F, "<div class=\"rcos\" id=\"a~p\" style=\"display: none;\" ><br/>~s</div>", [Now2, RoomConfig]), fw(F, "</div>"). +put_room_occupants(_F, _RoomOccupants, _Lang, plaintext) -> + ok; +put_room_occupants(F, RoomOccupants, Lang, _FileFormat) -> + {_, Now2, _} = now(), + fw(F, "<div class=\"rc\">"), + fw(F, "<div class=\"rct\" onclick=\"sh('o~p');return false;\">~s</div>", [Now2, ?T("Room Occupants")]), + fw(F, "<div class=\"rcos\" id=\"o~p\" style=\"display: none;\" ><br/>~s</div>", [Now2, RoomOccupants]), + fw(F, "</div>"). + %% htmlize %% The default behaviour is to ignore the nofollow spam prevention on links %% (NoFollow=false) htmlize(S1) -> - htmlize(S1, false). + htmlize(S1, html). + +htmlize(S1, plaintext) -> + S1; +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(S1, NoFollow) -> +htmlize(S1, _NoFollow, plaintext) -> + S1; +htmlize(S1, NoFollow, _FileFormat) -> S2_list = string:tokens(S1, "\n"), lists:foldl( fun(Si, Res) -> @@ -696,9 +782,12 @@ htmlize2(S1, NoFollow) -> S2 = element(2, regexp:gsub(S1, "\\&", "\\&")), S3 = element(2, regexp:gsub(S2, "<", "\\<")), S4 = element(2, regexp:gsub(S3, ">", "\\>")), - S5 = element(2, regexp:gsub(S4, "[-+.a-zA-Z0-9]+://[^] )\'\"}]+", link_regexp(NoFollow))), + S5 = element(2, regexp:gsub(S4, "((http|https|ftp)://|(mailto|xmpp):)[^] )\'\"}]+", + link_regexp(NoFollow))), %% Remove 'right-to-left override' unicode character 0x202e - element(2, regexp:gsub(S5, [226,128,174], "[RLO]")). + S6 = element(2, regexp:gsub(S5, " ", "\\ \\ ")), + S7 = element(2, regexp:gsub(S6, "\\t", "\\ \\ \\ \\ ")), + element(2, regexp:gsub(S7, [226,128,174], "[RLO]")). %% Regexp link %% Add the nofollow rel attribute when required @@ -730,20 +819,20 @@ get_room_info(RoomJID, Opts) -> config = Opts }. -roomconfig_to_string(Options, Lang) -> - % Get title, if available +roomconfig_to_string(Options, Lang, FileFormat) -> + %% Get title, if available Title = case lists:keysearch(title, 1, Options) of {value, Tuple} -> [Tuple]; false -> [] end, - - % Remove title from list + + %% Remove title from list Os1 = lists:keydelete(title, 1, Options), - - % Order list + + %% Order list Os2 = lists:sort(Os1), - - % Add title to ordered list + + %% Add title to ordered list Options2 = Title ++ Os2, lists:foldl( @@ -760,7 +849,7 @@ roomconfig_to_string(Options, Lang) -> T -> case Opt of password -> "<div class=\"rcoe\">" ++ OptText ++ "</div>"; - title -> "<div class=\"rcot\">" ++ ?T("Room title") ++ ": \"" ++ htmlize(T) ++ "\"</div>"; + title -> "<div class=\"rcot\">" ++ ?T("Room title") ++ ": \"" ++ htmlize(T, FileFormat) ++ "\"</div>"; _ -> "\"" ++ T ++ "\"" end end, @@ -780,17 +869,71 @@ 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_change_subj) -> "Allow users to change the 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. +%% Users = [{JID, Nick, Role}] +roomoccupants_to_string(Users, _FileFormat) -> + Res = [role_users_to_string(RoleS, Users1) + || {RoleS, Users1} <- group_by_role(Users), Users1 /= []], + lists:flatten(["<div class=\"rcot\">", Res, "</div>"]). + +%% Users = [{JID, Nick, Role}] +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 = atom() +%% Users = [{JID, Nick}] +role_users_to_string(RoleS, Users) -> + SortedUsers = lists:keysort(2, Users), + UsersString = [[Nick, "<br/>"] || {_JID, Nick} <- SortedUsers], + [RoleS, ": ", UsersString]. + +get_room_occupants(RoomJIDString) -> + RoomJID = jlib:string_to_jid(RoomJIDString), + RoomName = RoomJID#jid.luser, + MucService = RoomJID#jid.lserver, + StateData = get_room_state(RoomName, MucService), + [{U#user.jid, U#user.nick, U#user.role} + || {_, U} <- ?DICT:to_list(StateData#state.users)]. + +get_room_state(RoomName, MucService) -> + case mnesia:dirty_read(muc_online_room, {RoomName, MucService}) of + [R] -> + RoomPid = R#muc_online_room.pid, + get_room_state(RoomPid); + [] -> + room_not_found + end. + +get_room_state(RoomPid) -> + {ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, get_state), + R. + 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. + 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 10b7d8a75..47b40f6dd 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2008 Process-one +%%% ejabberd, Copyright (C) 2002-2009 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -16,7 +16,7 @@ %%% 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., 59 Temple Place, Suite 330, Boston, MA @@ -48,67 +48,11 @@ -include("ejabberd.hrl"). -include("jlib.hrl"). +-include("mod_muc_room.hrl"). --define(MAX_USERS_DEFAULT, 200). -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). --define(SETS, gb_sets). --define(DICT, dict). - --record(lqueue, {queue, len, max}). - --record(config, {title = "", - allow_change_subj = true, - allow_query_users = true, - allow_private_messages = true, - public = true, - public_list = true, - persistent = false, - moderated = true, - members_by_default = true, - members_only = false, - allow_user_invites = false, - password_protected = false, - password = "", - anonymous = true, - max_users = ?MAX_USERS_DEFAULT, - logging = false - }). - --record(user, {jid, - nick, - role, - last_presence}). - --record(activity, {message_time = 0, - presence_time = 0, - message_shaper, - presence_shaper, - message, - 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, - activity = ?DICT:new(), - room_shaper, - room_queue = queue:new()}). - --record(muc_online_users, {us, - room, - host}). - - %-define(DBGFSM, true). -ifdef(DBGFSM). @@ -204,7 +148,8 @@ normal_state({route, From, "", {xmlelement, "message", Attrs, Els} = Packet}, StateData) -> Lang = xml:get_attr_s("xml:lang", Attrs), - case is_user_online(From, StateData) of + case is_user_online(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of true -> case xml:get_attr_s("type", Attrs) of "groupchat" -> @@ -239,13 +184,12 @@ normal_state({route, From, "", message_time = Now, message_shaper = MessageShaper}, StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), + store_user_activity( + From, NewActivity, StateData), + StateData2 = + StateData1#state{ room_shaper = RoomShaper}, - process_groupchat_message(From, Packet, StateData1); + process_groupchat_message(From, Packet, StateData2); true -> StateData1 = if @@ -266,13 +210,12 @@ normal_state({route, From, "", {message, From}, StateData#state.room_queue), StateData2 = - StateData1#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), + store_user_activity( + From, NewActivity, StateData1), + StateData3 = + StateData2#state{ room_queue = RoomQueue}, - {next_state, normal_state, StateData2} + {next_state, normal_state, StateData3} end; true -> MessageInterval = @@ -286,11 +229,8 @@ normal_state({route, From, "", message = Packet, message_shaper = MessageShaper}, StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, + store_user_activity( + From, NewActivity, StateData), {next_state, normal_state, StateData1} end; "error" -> @@ -376,7 +316,8 @@ normal_state({route, From, "", (XMLNS == ?NS_MUC_ADMIN) or (XMLNS == ?NS_MUC_OWNER) or (XMLNS == ?NS_DISCO_INFO) or - (XMLNS == ?NS_DISCO_ITEMS) -> + (XMLNS == ?NS_DISCO_ITEMS) or + (XMLNS == ?NS_CAPTCHA) -> Res1 = case XMLNS of ?NS_MUC_ADMIN -> process_iq_admin(From, Type, Lang, SubEl, StateData); @@ -385,7 +326,9 @@ normal_state({route, From, "", ?NS_DISCO_INFO -> process_iq_disco_info(From, Type, Lang, StateData); ?NS_DISCO_ITEMS -> - process_iq_disco_items(From, Type, Lang, StateData) + process_iq_disco_items(From, Type, Lang, StateData); + ?NS_CAPTCHA -> + process_iq_captcha(From, Type, Lang, SubEl, StateData) end, {IQRes, NewStateData} = case Res1 of @@ -432,12 +375,7 @@ normal_state({route, From, Nick, (Now >= Activity#activity.presence_time + MinPresenceInterval) and (Activity#activity.presence == undefined) -> NewActivity = Activity#activity{presence_time = Now}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, + StateData1 = store_user_activity(From, NewActivity, StateData), process_presence(From, Nick, Packet, StateData1); true -> if @@ -450,12 +388,7 @@ normal_state({route, From, Nick, ok end, NewActivity = Activity#activity{presence = {Nick, Packet}}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, + StateData1 = store_user_activity(From, NewActivity, StateData), {next_state, normal_state, StateData1} end; @@ -475,9 +408,9 @@ normal_state({route, From, ToNick, forget_message -> {next_state, normal_state, StateData}; continue_delivery -> - case (StateData#state.config)#config.allow_private_messages - andalso is_user_online(From, StateData) of - true -> + case {(StateData#state.config)#config.allow_private_messages, + is_user_online(From, StateData)} of + {true, true} -> case Type of "groupchat" -> ErrText = "It is not allowed to send private " @@ -511,7 +444,7 @@ normal_state({route, From, ToNick, ToJID, Packet) end end; - _ -> + {true, false} -> ErrText = "Only occupants are allowed to send messages to the conference", Err = jlib:make_error_reply( Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), @@ -519,6 +452,15 @@ normal_state({route, From, ToNick, jlib:jid_replace_resource( StateData#state.jid, ToNick), + From, Err); + {false, _} -> + ErrText = "It is not allowed to send private messages", + Err = jlib:make_error_reply( + Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + ToNick), From, Err) end, {next_state, normal_state, StateData} @@ -604,6 +546,7 @@ handle_event({service_message, Msg}, _StateName, StateData) -> end, ?DICT:to_list(StateData#state.users)), NSD = add_message_to_history("", + StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; @@ -680,6 +623,8 @@ handle_sync_event(get_state, _From, StateName, StateData) -> handle_sync_event({change_config, Config}, _From, StateName, StateData) -> {result, [], NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; +handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> + {reply, {ok, NewStateData}, StateName, NewStateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. @@ -694,31 +639,27 @@ code_change(_OldVsn, StateName, StateData, _Extra) -> %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - {Nick, Packet} = Activity#activity.presence, - NewActivity = Activity#activity{presence_time = Now, - presence = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - process_presence(From, Nick, Packet, StateData1); + RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), + RoomQueue = queue:in({presence, From}, StateData#state.room_queue), + StateData1 = StateData#state{room_queue = RoomQueue}, + if + RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> + {next_state, normal_state, StateData1} + end; handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - Packet = Activity#activity.message, - NewActivity = Activity#activity{message_time = Now, - message = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - process_groupchat_message(From, Packet, StateData1); + RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), + RoomQueue = queue:in({message, From}, StateData#state.room_queue), + StateData1 = StateData#state{room_queue = RoomQueue}, + if + RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> + {next_state, normal_state, StateData1} + end; handle_info(process_room_queue, normal_state = StateName, StateData) -> case queue:out(StateData#state.room_queue) of {{value, {message, From}}, RoomQueue} -> @@ -726,30 +667,52 @@ handle_info(process_room_queue, normal_state = StateName, StateData) -> Packet = Activity#activity.message, NewActivity = Activity#activity{message = undefined}, StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), + store_user_activity( + From, NewActivity, StateData), + StateData2 = + StateData1#state{ room_queue = RoomQueue}, - StateData2 = prepare_room_queue(StateData1), - process_groupchat_message(From, Packet, StateData2); + StateData3 = prepare_room_queue(StateData2), + process_groupchat_message(From, Packet, StateData3); {{value, {presence, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), {Nick, Packet} = Activity#activity.presence, NewActivity = Activity#activity{presence = undefined}, StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), + store_user_activity( + From, NewActivity, StateData), + StateData2 = + StateData1#state{ room_queue = RoomQueue}, - StateData2 = prepare_room_queue(StateData1), - process_presence(From, Nick, Packet, StateData2); + StateData3 = prepare_room_queue(StateData2), + process_presence(From, Nick, Packet, StateData3); {empty, _} -> {next_state, StateName, StateData} end; +handle_info({captcha_succeed, From}, normal_state, StateData) -> + NewState = case ?DICT:find(From, StateData#state.robots) of + {ok, {Nick, Packet}} -> + Robots = ?DICT:store(From, passed, StateData#state.robots), + add_new_user(From, Nick, Packet, StateData#state{robots=Robots}); + _ -> + StateData + end, + {next_state, normal_state, NewState}; +handle_info({captcha_failed, From}, normal_state, StateData) -> + NewState = case ?DICT:find(From, StateData#state.robots) of + {ok, {Nick, Packet}} -> + Robots = ?DICT:erase(From, StateData#state.robots), + Err = jlib:make_error_reply( + Packet, ?ERR_NOT_AUTHORIZED), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData#state{robots=Robots}; + _ -> + StateData + end, + {next_state, normal_state, NewState}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. @@ -777,11 +740,10 @@ route(Pid, From, ToNick, Packet) -> process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, StateData) -> Lang = xml:get_attr_s("xml:lang", Attrs), - case is_user_online(From, StateData) of + case is_user_online(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) of true -> - {ok, #user{nick = FromNick, role = Role}} = - ?DICT:find(jlib:jid_tolower(From), - StateData#state.users), + {FromNick, Role} = get_participant_data(From, StateData), if (Role == moderator) or (Role == participant) or ((StateData#state.config)#config.moderated == false) -> @@ -826,6 +788,7 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, ?DICT:to_list(StateData#state.users)), NewStateData2 = add_message_to_history(FromNick, + From, Packet, NewStateData1), {next_state, normal_state, NewStateData2}; @@ -836,12 +799,12 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, ?ERRT_FORBIDDEN( Lang, "Only moderators and participants " - "are allowed to change subject in this room"); + "are allowed to change the subject in this room"); _ -> ?ERRT_FORBIDDEN( Lang, "Only moderators " - "are allowed to change subject in this room") + "are allowed to change the subject in this room") end, ejabberd_router:route( StateData#state.jid, @@ -866,6 +829,35 @@ process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, {next_state, normal_state, StateData} end. + +%% @doc Check if this non participant can send message to room. +%% +%% XEP-0045 v1.23: +%% 7.9 Sending a Message to All Occupants +%% an implementation MAY allow users with certain privileges +%% (e.g., a room owner, room admin, or service-level admin) +%% to send messages to the room even if those users are not occupants. +%% +%% Check the mod_muc option access_message_nonparticipant and wether this JID +%% is allowed or denied +is_user_allowed_message_nonparticipant(JID, StateData) -> + case get_service_affiliation(JID, StateData) of + owner -> + true; + _ -> false + end. + +%% @doc Get information of this participant, or default values. +%% If the JID is not a participant, return values for a service message. +get_participant_data(From, StateData) -> + case ?DICT:find(jlib:jid_tolower(From), StateData#state.users) of + {ok, #user{nick = FromNick, role = Role}} -> + {FromNick, Role}; + error -> + {"", moderator} + end. + + process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, StateData) -> Type = xml:get_attr_s("type", Attrs), @@ -903,10 +895,24 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, true -> case {is_nick_exists(Nick, StateData), mod_muc:can_use_nick( - StateData#state.host, From, Nick)} of - {true, _} -> + StateData#state.host, From, Nick), + {(StateData#state.config)#config.allow_visitor_nickchange, + is_visitor(From, StateData)}} of + {_, _, {false, true}} -> + ErrText = "Visitors are not allowed to change their nicknames in this room", + Err = jlib:make_error_reply( + Packet, + ?ERRT_NOT_ALLOWED(Lang, ErrText)), + ejabberd_router:route( + % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, + Nick), + From, Err), + StateData; + {true, _, _} -> Lang = xml:get_attr_s("xml:lang", Attrs), - ErrText = "Nickname is already in use by another occupant", + ErrText = "That nickname is already in use by another occupant", Err = jlib:make_error_reply( Packet, ?ERRT_CONFLICT(Lang, ErrText)), @@ -916,8 +922,8 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, Nick), % TODO: s/Nick/""/ From, Err), StateData; - {_, false} -> - ErrText = "Nickname is registered by another person", + {_, false, _} -> + ErrText = "That nickname is registered by another person", Err = jlib:make_error_reply( Packet, ?ERRT_CONFLICT(Lang, ErrText)), @@ -931,11 +937,17 @@ process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, _ -> change_nick(From, Nick, StateData) end; - _ -> - NewState = - add_user_presence(From, Packet, StateData), - send_new_presence(From, NewState), - NewState + _NotNickChange -> + Stanza = case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _Allowed -> + Packet + end, + NewState = add_user_presence(From, Stanza, StateData), + send_new_presence(From, NewState), + NewState end; _ -> add_new_user(From, Nick, Packet, StateData) @@ -1205,6 +1217,9 @@ get_default_role(Affiliation, StateData) -> end end. +is_visitor(Jid, StateData) -> + get_role(Jid, StateData) =:= visitor. + get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), @@ -1222,9 +1237,9 @@ get_max_users_admin_threshold(StateData) -> mod_muc, max_users_admin_threshold, 5). get_user_activity(JID, StateData) -> - case ?DICT:find(jlib:jid_tolower(JID), - StateData#state.activity) of - {ok, A} -> A; + case treap:lookup(jlib:jid_tolower(JID), + StateData#state.activity) of + {ok, _P, A} -> A; error -> MessageShaper = shaper:new(gen_mod:get_module_opt( @@ -1238,6 +1253,82 @@ get_user_activity(JID, StateData) -> presence_shaper = PresenceShaper} end. +store_user_activity(JID, UserActivity, StateData) -> + MinMessageInterval = + gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, min_message_interval, 0), + MinPresenceInterval = + gen_mod:get_module_opt( + StateData#state.server_host, + mod_muc, min_presence_interval, 0), + Key = jlib:jid_tolower(JID), + Now = now_to_usec(now()), + Activity1 = clean_treap(StateData#state.activity, {1, -Now}), + Activity = + case treap:lookup(Key, Activity1) of + {ok, _P, _A} -> + treap:delete(Key, Activity1); + error -> + Activity1 + end, + StateData1 = + case (MinMessageInterval == 0) andalso + (MinPresenceInterval == 0) andalso + (UserActivity#activity.message_shaper == none) andalso + (UserActivity#activity.presence_shaper == none) andalso + (UserActivity#activity.message == undefined) andalso + (UserActivity#activity.presence == undefined) of + true -> + StateData#state{activity = Activity}; + false -> + case (UserActivity#activity.message == undefined) andalso + (UserActivity#activity.presence == undefined) of + true -> + {_, MessageShaperInterval} = + shaper:update(UserActivity#activity.message_shaper, + 100000), + {_, PresenceShaperInterval} = + shaper:update(UserActivity#activity.presence_shaper, + 100000), + Delay = lists:max([MessageShaperInterval, + PresenceShaperInterval, + MinMessageInterval * 1000, + MinPresenceInterval * 1000]) * 1000, + Priority = {1, -(Now + Delay)}, + StateData#state{ + activity = treap:insert( + Key, + Priority, + UserActivity, + Activity)}; + false -> + Priority = {0, 0}, + StateData#state{ + activity = treap:insert( + Key, + Priority, + UserActivity, + Activity)} + end + end, + StateData1. + +clean_treap(Treap, CleanPriority) -> + case treap:is_empty(Treap) of + true -> + Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if + Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> + Treap + end + end. + + prepare_room_queue(StateData) -> case queue:out(StateData#state.room_queue) of {{value, {message, From}}, _RoomQueue} -> @@ -1309,6 +1400,13 @@ filter_presence({xmlelement, "presence", Attrs, Els}) -> end, Els), {xmlelement, "presence", Attrs, FEls}. +strip_status({xmlelement, "presence", Attrs, Els}) -> + FEls = lists:filter( + fun({xmlelement, "status", _Attrs1, _Els1}) -> + false; + (_) -> true + end, Els), + {xmlelement, "presence", Attrs, FEls}. add_user_presence(JID, Presence, StateData) -> LJID = jlib:jid_tolower(JID), @@ -1396,7 +1494,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> ErrText = "You have been banned from this room", ?ERRT_FORBIDDEN(Lang, ErrText); _ -> - ErrText = "Membership required to enter this room", + ErrText = "Membership is required to enter this room", ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) end), ejabberd_router:route( % TODO: s/Nick/""/ @@ -1404,7 +1502,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> From, Err), StateData; {_, true, _, _} -> - ErrText = "Nickname is already in use by another occupant", + ErrText = "That nickname is already in use by another occupant", Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), ejabberd_router:route( % TODO: s/Nick/""/ @@ -1412,7 +1510,7 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> From, Err), StateData; {_, _, false, _} -> - ErrText = "Nickname is registered by another person", + ErrText = "That nickname is registered by another person", Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), ejabberd_router:route( % TODO: s/Nick/""/ @@ -1420,7 +1518,8 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> From, Err), StateData; {_, _, _, Role} -> - case check_password(Affiliation, Els, StateData) of + case check_password(ServiceAffiliation, Affiliation, + Els, From, StateData) of true -> NewState = add_user_presence( @@ -1453,10 +1552,11 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> true -> NewState#state{just_created = false}; false -> - NewState + Robots = ?DICT:erase(From, StateData#state.robots), + NewState#state{robots = Robots} end; nopass -> - ErrText = "Password required to enter this room", + ErrText = "A password is required to enter this room", Err = jlib:make_error_reply( Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), ejabberd_router:route( % TODO: s/Nick/""/ @@ -1464,6 +1564,29 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> StateData#state.jid, Nick), From, Err), StateData; + captcha_required -> + ID = randoms:get_string(), + SID = xml:get_attr_s("id", Attrs), + RoomJID = StateData#state.jid, + To = jlib:jid_replace_resource(RoomJID, Nick), + case ejabberd_captcha:create_captcha( + ID, SID, RoomJID, To, Lang, From) of + {ok, CaptchaEls} -> + MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls}, + Robots = ?DICT:store(From, + {Nick, Packet}, StateData#state.robots), + ejabberd_router:route(RoomJID, From, MsgPkt), + StateData#state{robots = Robots}; + error -> + ErrText = "Unable to generate a captcha", + Err = jlib:make_error_reply( + Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)), + ejabberd_router:route( % TODO: s/Nick/""/ + jlib:jid_replace_resource( + StateData#state.jid, Nick), + From, Err), + StateData + end; _ -> ErrText = "Incorrect password", Err = jlib:make_error_reply( @@ -1476,12 +1599,13 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> end end. -check_password(owner, _Els, _StateData) -> +check_password(owner, _Affiliation, _Els, _From, _StateData) -> + %% Don't check pass if user is owner in MUC service (access_admin option) true; -check_password(_Affiliation, Els, StateData) -> +check_password(_ServiceAffiliation, Affiliation, Els, From, StateData) -> case (StateData#state.config)#config.password_protected of false -> - true; + check_captcha(Affiliation, From, StateData); true -> Pass = extract_password(Els), case Pass of @@ -1492,11 +1616,25 @@ check_password(_Affiliation, Els, StateData) -> Pass -> true; _ -> - false + false end end end. +check_captcha(Affiliation, From, StateData) -> + case (StateData#state.config)#config.captcha_protected + andalso ejabberd_captcha:is_feature_available() of + true when Affiliation == none -> + case ?DICT:find(From, StateData#state.robots) of + {ok, passed} -> + true; + _ -> + captcha_required + end; + _ -> + true + end. + extract_password([]) -> false; extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) -> @@ -1624,6 +1762,9 @@ extract_history([_ | Els], Type) -> send_update_presence(JID, StateData) -> + send_update_presence(JID, "", StateData). + +send_update_presence(JID, Reason, StateData) -> LJID = jlib:jid_tolower(JID), LJIDs = case LJID of {U, S, ""} -> @@ -1645,10 +1786,13 @@ send_update_presence(JID, StateData) -> end end, lists:foreach(fun(J) -> - send_new_presence(J, StateData) + send_new_presence(J, Reason, StateData) end, LJIDs). send_new_presence(NJID, StateData) -> + send_new_presence(NJID, "", StateData). + +send_new_presence(NJID, Reason, StateData) -> {ok, #user{jid = RealJID, nick = Nick, role = Role, @@ -1670,16 +1814,23 @@ send_new_presence(NJID, StateData) -> [{"affiliation", SAffiliation}, {"role", SRole}] end, + ItemEls = case Reason of + "" -> + []; + _ -> + [{xmlelement, "reason", [], + [{xmlcdata, Reason}]}] + end, Status = case StateData#state.just_created of true -> [{xmlelement, "status", [{"code", "201"}], []}]; false -> [] end, - Packet = append_subtags( + Packet = xml:append_subtags( Presence, [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs, []} | Status]}]), + [{xmlelement, "item", ItemAttrs, ItemEls} | Status]}]), ejabberd_router:route( jlib:jid_replace_resource(StateData#state.jid, Nick), Info#user.jid, @@ -1717,7 +1868,7 @@ send_existing_presences(ToJID, StateData) -> affiliation_to_list(FromAffiliation)}, {"role", role_to_list(FromRole)}] end, - Packet = append_subtags( + Packet = xml:append_subtags( Presence, [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], [{xmlelement, "item", ItemAttrs, []}]}]), @@ -1730,10 +1881,6 @@ send_existing_presences(ToJID, StateData) -> end, ?DICT:to_list(StateData#state.users)). -append_subtags({xmlelement, Name, Attrs, SubTags1}, SubTags2) -> - {xmlelement, Name, Attrs, SubTags1 ++ SubTags2}. - - now_to_usec({MSec, Sec, USec}) -> (MSec*1000000 + Sec)*1000000 + USec. @@ -1793,7 +1940,7 @@ send_nick_changing(JID, OldNick, StateData) -> [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], [{xmlelement, "item", ItemAttrs1, []}, {xmlelement, "status", [{"code", "303"}], []}]}]}, - Packet2 = append_subtags( + Packet2 = xml:append_subtags( Presence, [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], [{xmlelement, "item", ItemAttrs2, []}]}]), @@ -1837,7 +1984,7 @@ lqueue_to_list(#lqueue{queue = Q1}) -> queue:to_list(Q1). -add_message_to_history(FromNick, Packet, StateData) -> +add_message_to_history(FromNick, FromJID, Packet, StateData) -> HaveSubject = case xml:get_subtag(Packet, "subject") of false -> false; @@ -1845,8 +1992,18 @@ add_message_to_history(FromNick, Packet, StateData) -> true end, TimeStamp = calendar:now_to_universal_time(now()), - TSPacket = append_subtags(Packet, - [jlib:timestamp_to_xml(TimeStamp)]), + %% Chatroom history is stored as XMPP packets, so + %% the decision to include the original sender's JID or not is based on the + %% chatroom configuration when the message was originally sent. + %% Also, if the chatroom is anonymous, even moderators will not get the real JID + SenderJid = case ((StateData#state.config)#config.anonymous) of + true -> StateData#state.jid; + false -> FromJID + end, + TSPacket = xml:append_subtags(Packet, + [jlib:timestamp_to_xml(TimeStamp, utc, SenderJid, ""), + %% TODO: Delete the next line once XEP-0091 is Obsolete + jlib:timestamp_to_xml(TimeStamp)]), SPacket = jlib:replace_from_to( jlib:jid_replace_resource(StateData#state.jid, FromNick), StateData#state.jid, @@ -2047,21 +2204,21 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> set_affiliation_and_reason( JID, outcast, Reason, set_role(JID, none, SD)); - {JID, affiliation, A, _Reason} when + {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> - SD1 = set_affiliation(JID, A, SD), + SD1 = set_affiliation_and_reason(JID, A, Reason, SD), SD2 = set_role(JID, moderator, SD1), - send_update_presence(JID, SD2), + send_update_presence(JID, Reason, SD2), SD2; - {JID, affiliation, member, _Reason} -> - SD1 = set_affiliation( - JID, member, SD), + {JID, affiliation, member, Reason} -> + SD1 = set_affiliation_and_reason( + JID, member, Reason, SD), SD2 = set_role(JID, participant, SD1), - send_update_presence(JID, SD2), + send_update_presence(JID, Reason, SD2), SD2; - {JID, role, R, _Reason} -> - SD1 = set_role(JID, R, SD), - catch send_new_presence(JID, SD1), + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, SD), + catch send_new_presence(JID, Reason, SD1), SD1; {JID, affiliation, A, _Reason} -> SD1 = set_affiliation(JID, A, SD), @@ -2105,7 +2262,7 @@ find_changed_items(UJID, UAffiliation, URole, ErrText = io_lib:format( translate:translate( Lang, - "JID ~s is invalid"), [S]), + "Jabber ID ~s is invalid"), [S]), {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; J -> {value, J} @@ -2149,11 +2306,13 @@ find_changed_items(UJID, UAffiliation, URole, [StrAffiliation]), {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; SAffiliation -> + ServiceAf = get_service_affiliation(JID, StateData), CanChangeRA = case can_change_ra( UAffiliation, URole, TAffiliation, TRole, - affiliation, SAffiliation) of + affiliation, SAffiliation, + ServiceAf) of nothing -> nothing; true -> @@ -2204,11 +2363,13 @@ find_changed_items(UJID, UAffiliation, URole, [StrRole]), {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; SRole -> + ServiceAf = get_service_affiliation(JID, StateData), CanChangeRA = case can_change_ra( UAffiliation, URole, TAffiliation, TRole, - role, SRole) of + role, SRole, + ServiceAf) of nothing -> nothing; true -> @@ -2255,142 +2416,148 @@ find_changed_items(_UJID, _UAffiliation, _URole, _Items, can_change_ra(_FAffiliation, _FRole, + owner, _TRole, + affiliation, owner, owner) -> + %% A room owner tries to add as persistent owner a + %% participant that is already owner because he is MUC admin + true; +can_change_ra(_FAffiliation, _FRole, TAffiliation, _TRole, - affiliation, Value) + affiliation, Value, _ServiceAf) when (TAffiliation == Value) -> nothing; can_change_ra(_FAffiliation, _FRole, _TAffiliation, TRole, - role, Value) + role, Value, _ServiceAf) when (TRole == Value) -> nothing; can_change_ra(FAffiliation, _FRole, outcast, _TRole, - affiliation, none) + affiliation, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, outcast, _TRole, - affiliation, member) + affiliation, member, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, outcast, _TRole, - affiliation, admin) -> + affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, outcast, _TRole, - affiliation, owner) -> + affiliation, owner, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, none, _TRole, - affiliation, outcast) + affiliation, outcast, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, none, _TRole, - affiliation, member) + affiliation, member, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, none, _TRole, - affiliation, admin) -> + affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, none, _TRole, - affiliation, owner) -> + affiliation, owner, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, member, _TRole, - affiliation, outcast) + affiliation, outcast, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, member, _TRole, - affiliation, none) + affiliation, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, member, _TRole, - affiliation, admin) -> + affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, member, _TRole, - affiliation, owner) -> + affiliation, owner, _ServiceAf) -> true; can_change_ra(owner, _FRole, admin, _TRole, - affiliation, _Affiliation) -> + affiliation, _Affiliation, _ServiceAf) -> true; can_change_ra(owner, _FRole, owner, _TRole, - affiliation, _Affiliation) -> + affiliation, _Affiliation, _ServiceAf) -> check_owner; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, - affiliation, _Value) -> + affiliation, _Value, _ServiceAf) -> false; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, - role, none) -> + role, none, _ServiceAf) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, - role, participant) -> + role, participant, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, _TAffiliation, visitor, - role, moderator) + role, moderator, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, - role, none) -> + role, none, _ServiceAf) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, - role, visitor) -> + role, visitor, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, _TAffiliation, participant, - role, moderator) + role, moderator, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, _FRole, owner, moderator, - role, visitor) -> + role, visitor, _ServiceAf) -> false; can_change_ra(owner, _FRole, _TAffiliation, moderator, - role, visitor) -> + role, visitor, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, admin, moderator, - role, visitor) -> + role, visitor, _ServiceAf) -> false; can_change_ra(admin, _FRole, _TAffiliation, moderator, - role, visitor) -> + role, visitor, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, owner, moderator, - role, participant) -> + role, participant, _ServiceAf) -> false; can_change_ra(owner, _FRole, _TAffiliation, moderator, - role, participant) -> + role, participant, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, admin, moderator, - role, participant) -> + role, participant, _ServiceAf) -> false; can_change_ra(admin, _FRole, _TAffiliation, moderator, - role, participant) -> + role, participant, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, - role, _Value) -> + role, _Value, _ServiceAf) -> false. @@ -2467,11 +2634,18 @@ process_iq_owner(From, set, Lang, SubEl, StateData) -> {?NS_XDATA, "cancel"} -> {result, [], StateData}; {?NS_XDATA, "submit"} -> - case {check_allowed_log_change(XEl, StateData, From), - check_allowed_persistent_change(XEl, StateData, From)} of - {allow, allow} -> set_config(XEl, StateData); - _ -> {error, ?ERR_BAD_REQUEST} - end; + case is_allowed_log_change(XEl, StateData, From) + andalso + is_allowed_persistent_change(XEl, StateData, + From) + andalso + is_allowed_room_name_desc_limits(XEl, + StateData) + andalso + is_password_settings_correct(XEl, StateData) of + true -> set_config(XEl, StateData); + false -> {error, ?ERR_NOT_ACCEPTABLE} + end; _ -> {error, ?ERR_BAD_REQUEST} end; @@ -2523,26 +2697,89 @@ process_iq_owner(From, get, Lang, SubEl, StateData) -> {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. -check_allowed_log_change(XEl, StateData, From) -> +is_allowed_log_change(XEl, StateData, From) -> case lists:keymember("muc#roomconfig_enablelogging", 1, jlib:parse_xdata_submit(XEl)) of false -> - allow; + true; true -> - mod_muc_log:check_access_log( - StateData#state.server_host, From) + (allow == mod_muc_log:check_access_log( + StateData#state.server_host, From)) end. -check_allowed_persistent_change(XEl, StateData, From) -> +is_allowed_persistent_change(XEl, StateData, From) -> case lists:keymember("muc#roomconfig_persistentroom", 1, jlib:parse_xdata_submit(XEl)) of false -> - allow; + true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, - acl:match_rule(StateData#state.server_host, AccessPersistent, From) + (allow == acl:match_rule(StateData#state.server_host, AccessPersistent, From)) + end. + +%% Check if the Room Name and Room Description defined in the Data Form +%% are conformant to the configured limits +is_allowed_room_name_desc_limits(XEl, StateData) -> + IsNameAccepted = + case lists:keysearch("muc#roomconfig_roomname", 1, + jlib:parse_xdata_submit(XEl)) of + {value, {_, [N]}} -> + length(N) =< gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_room_name, + infinite); + _ -> + true + end, + IsDescAccepted = + case lists:keysearch("muc#roomconfig_roomdesc", 1, + jlib:parse_xdata_submit(XEl)) of + {value, {_, [D]}} -> + length(D) =< gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_room_desc, + infinite); + _ -> + true + end, + IsNameAccepted and IsDescAccepted. + +%% Return false if: +%% "the password for a password-protected room is blank" +is_password_settings_correct(XEl, StateData) -> + Config = StateData#state.config, + OldProtected = Config#config.password_protected, + OldPassword = Config#config.password, + NewProtected = + case lists:keysearch("muc#roomconfig_passwordprotectedroom", 1, + jlib:parse_xdata_submit(XEl)) of + {value, {_, ["1"]}} -> + true; + {value, {_, ["0"]}} -> + false; + _ -> + undefined + end, + NewPassword = + case lists:keysearch("muc#roomconfig_roomsecret", 1, + jlib:parse_xdata_submit(XEl)) of + {value, {_, [P]}} -> + P; + _ -> + undefined + end, + case {OldProtected, NewProtected, OldPassword, NewPassword} of + {true, undefined, "", undefined} -> + false; + {true, undefined, _, ""} -> + false; + {_, true , "", undefined} -> + false; + {_, true, _, ""} -> + false; + _ -> + true end. + -define(XFIELD(Type, Label, Var, Val), {xmlelement, "field", [{"type", Type}, {"label", translate:translate(Lang, Label)}, @@ -2562,22 +2799,35 @@ check_allowed_persistent_change(XEl, StateData, From) -> -define(PRIVATEXFIELD(Label, Var, Val), ?XFIELD("text-private", Label, Var, Val)). +get_default_room_maxusers(RoomState) -> + DefRoomOpts = gen_mod:get_module_opt(RoomState#state.server_host, mod_muc, default_room_options, []), + RoomState2 = set_opts(DefRoomOpts, RoomState), + (RoomState2#state.config)#config.max_users. get_config(Lang, StateData, From) -> {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), + DefaultRoomMaxUsers = get_default_room_maxusers(StateData), Config = StateData#state.config, + {MaxUsersRoomInteger, MaxUsersRoomString} = + case get_max_users(StateData) of + N when is_integer(N) -> + {N, erlang:integer_to_list(N)}; + _ -> {0, "none"} + end, Res = [{xmlelement, "title", [], - [{xmlcdata, translate:translate(Lang, "Configuration for ") ++ - jlib:jid_to_string(StateData#state.jid)}]}, + [{xmlcdata, io_lib:format(translate:translate(Lang, "Configuration of room ~s"), [jlib:jid_to_string(StateData#state.jid)])}]}, {xmlelement, "field", [{"type", "hidden"}, {"var", "FORM_TYPE"}], [{xmlelement, "value", [], [{xmlcdata, "http://jabber.org/protocol/muc#roomconfig"}]}]}, ?STRINGXFIELD("Room title", "muc#roomconfig_roomname", - Config#config.title) + Config#config.title), + ?STRINGXFIELD("Room description", + "muc#roomconfig_roomdesc", + Config#config.description) ] ++ case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of allow -> @@ -2606,13 +2856,7 @@ get_config(Lang, StateData, From) -> [{"type", "list-single"}, {"label", translate:translate(Lang, "Maximum Number of Occupants")}, {"var", "muc#roomconfig_maxusers"}], - [{xmlelement, "value", [], [{xmlcdata, - case get_max_users(StateData) of - N when is_integer(N) -> - erlang:integer_to_list(N); - _ -> "none" - end - }]}] ++ + [{xmlelement, "value", [], [{xmlcdata, MaxUsersRoomString}]}] ++ if is_integer(ServiceMaxUsers) -> []; true -> @@ -2623,11 +2867,12 @@ get_config(Lang, StateData, From) -> [{xmlelement, "option", [{"label", erlang:integer_to_list(N)}], [{xmlelement, "value", [], [{xmlcdata, erlang:integer_to_list(N)}]}]} || - N <- ?MAX_USERS_DEFAULT_LIST, N =< ServiceMaxUsers] + N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger | + ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers] }, {xmlelement, "field", [{"type", "list-single"}, - {"label", translate:translate(Lang, "Present real JIDs to")}, + {"label", translate:translate(Lang, "Present real Jabber IDs to")}, {"var", "muc#roomconfig_whois"}], [{xmlelement, "value", [], [{xmlcdata, if Config#config.anonymous -> @@ -2648,7 +2893,7 @@ get_config(Lang, StateData, From) -> ?BOOLXFIELD("Default users as participants", "members_by_default", Config#config.members_by_default), - ?BOOLXFIELD("Allow users to change subject", + ?BOOLXFIELD("Allow users to change the subject", "muc#roomconfig_changesubject", Config#config.allow_change_subj), ?BOOLXFIELD("Allow users to send private messages", @@ -2659,8 +2904,21 @@ get_config(Lang, StateData, From) -> Config#config.allow_query_users), ?BOOLXFIELD("Allow users to send invites", "muc#roomconfig_allowinvites", - Config#config.allow_user_invites) + Config#config.allow_user_invites), + ?BOOLXFIELD("Allow visitors to send status text in presence updates", + "muc#roomconfig_allowvisitorstatus", + Config#config.allow_visitor_status), + ?BOOLXFIELD("Allow visitors to change nickname", + "muc#roomconfig_allowvisitornickchange", + Config#config.allow_visitor_nickchange) ] ++ + case ejabberd_captcha:is_feature_available() of + true -> + [?BOOLXFIELD("Make room captcha protected", + "captcha_protected", + Config#config.captcha_protected)]; + false -> [] + end ++ case mod_muc_log:check_access_log( StateData#state.server_host, From) of allow -> @@ -2691,7 +2949,18 @@ set_config(XEl, StateData) -> #config{} = Config -> Res = change_config(Config, StateData), {result, _, NSD} = Res, - add_to_log(roomconfig_change, [], NSD), + Type = case {(StateData#state.config)#config.logging, + Config#config.logging} of + {true, false} -> + roomconfig_change_disabledlogging; + {false, true} -> + roomconfig_change_enabledlogging; + {_, _} -> + roomconfig_change + end, + Users = [{U#user.jid, U#user.nick, U#user.role} || + {_, U} <- ?DICT:to_list(StateData#state.users)], + add_to_log(Type, Users, NSD), Res; Err -> Err @@ -2724,12 +2993,18 @@ set_xoption([], Config) -> Config; set_xoption([{"muc#roomconfig_roomname", [Val]} | Opts], Config) -> ?SET_STRING_XOPT(title, Val); +set_xoption([{"muc#roomconfig_roomdesc", [Val]} | Opts], Config) -> + ?SET_STRING_XOPT(description, Val); set_xoption([{"muc#roomconfig_changesubject", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(allow_change_subj, Val); set_xoption([{"allow_query_users", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(allow_query_users, Val); set_xoption([{"allow_private_messages", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(allow_private_messages, Val); +set_xoption([{"muc#roomconfig_allowvisitorstatus", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_visitor_status, Val); +set_xoption([{"muc#roomconfig_allowvisitornickchange", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); set_xoption([{"muc#roomconfig_publicroom", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(public, Val); set_xoption([{"public_list", [Val]} | Opts], Config) -> @@ -2742,6 +3017,8 @@ set_xoption([{"members_by_default", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(members_by_default, Val); set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(members_only, Val); +set_xoption([{"captcha_protected", [Val]} | Opts], Config) -> + ?SET_BOOL_XOPT(captcha_protected, Val); set_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(allow_user_invites, Val); set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) -> @@ -2819,9 +3096,12 @@ set_opts([], StateData) -> set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> StateData#state{config = (StateData#state.config)#config{title = Val}}; + description -> StateData#state{config = (StateData#state.config)#config{description = Val}}; allow_change_subj -> StateData#state{config = (StateData#state.config)#config{allow_change_subj = Val}}; allow_query_users -> StateData#state{config = (StateData#state.config)#config{allow_query_users = Val}}; allow_private_messages -> StateData#state{config = (StateData#state.config)#config{allow_private_messages = Val}}; + allow_visitor_nickchange -> StateData#state{config = (StateData#state.config)#config{allow_visitor_nickchange = Val}}; + allow_visitor_status -> StateData#state{config = (StateData#state.config)#config{allow_visitor_status = Val}}; public -> StateData#state{config = (StateData#state.config)#config{public = Val}}; public_list -> StateData#state{config = (StateData#state.config)#config{public_list = Val}}; persistent -> StateData#state{config = (StateData#state.config)#config{persistent = Val}}; @@ -2830,6 +3110,7 @@ set_opts([{Opt, Val} | Opts], StateData) -> members_only -> StateData#state{config = (StateData#state.config)#config{members_only = Val}}; allow_user_invites -> StateData#state{config = (StateData#state.config)#config{allow_user_invites = Val}}; password_protected -> StateData#state{config = (StateData#state.config)#config{password_protected = Val}}; + captcha_protected -> StateData#state{config = (StateData#state.config)#config{captcha_protected = Val}}; password -> StateData#state{config = (StateData#state.config)#config{password = Val}}; anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; @@ -2858,9 +3139,12 @@ make_opts(StateData) -> Config = StateData#state.config, [ ?MAKE_CONFIG_OPT(title), + ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), ?MAKE_CONFIG_OPT(allow_private_messages), + ?MAKE_CONFIG_OPT(allow_visitor_status), + ?MAKE_CONFIG_OPT(allow_visitor_nickchange), ?MAKE_CONFIG_OPT(public), ?MAKE_CONFIG_OPT(public_list), ?MAKE_CONFIG_OPT(persistent), @@ -2869,6 +3153,7 @@ make_opts(StateData) -> ?MAKE_CONFIG_OPT(members_only), ?MAKE_CONFIG_OPT(allow_user_invites), ?MAKE_CONFIG_OPT(password_protected), + ?MAKE_CONFIG_OPT(captcha_protected), ?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous), ?MAKE_CONFIG_OPT(logging), @@ -2954,9 +3239,12 @@ process_iq_disco_info(_From, get, Lang, StateData) -> iq_disco_info_extras(Lang, StateData) -> Len = length(?DICT:to_list(StateData#state.users)), + RoomDescription = (StateData#state.config)#config.description, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], [?RFIELDT("hidden", "FORM_TYPE", "http://jabber.org/protocol/muc#roominfo"), + ?RFIELD("Room description", "muc#roominfo_description", + RoomDescription), ?RFIELD("Number of occupants", "muc#roominfo_occupants", integer_to_list(Len)) ]}]. @@ -2989,6 +3277,17 @@ process_iq_disco_items(From, get, _Lang, StateData) -> {error, ?ERR_FORBIDDEN} end. +process_iq_captcha(_From, get, _Lang, _SubEl, _StateData) -> + {error, ?ERR_NOT_ALLOWED}; + +process_iq_captcha(_From, set, _Lang, SubEl, StateData) -> + case ejabberd_captcha:process_reply(SubEl) of + ok -> + {result, [], StateData}; + _ -> + {error, ?ERR_NOT_ACCEPTABLE} + end. + get_title(StateData) -> case (StateData#state.config)#config.title of "" -> @@ -3153,6 +3452,12 @@ send_error_only_occupants(Packet, Lang, RoomJID, From) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging +add_to_log(Type, Data, StateData) + when Type == roomconfig_change_disabledlogging -> + %% When logging is disabled, the config change message must be logged: + mod_muc_log:add_to_log( + StateData#state.server_host, roomconfig_change, Data, + StateData#state.jid, make_opts(StateData)); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of true -> diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl new file mode 100644 index 000000000..2ff1c1814 --- /dev/null +++ b/src/mod_muc/mod_muc_room.hrl @@ -0,0 +1,82 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2009 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-define(MAX_USERS_DEFAULT, 200). + +-define(SETS, gb_sets). +-define(DICT, dict). + +-record(lqueue, {queue, len, max}). + +-record(config, {title = "", + description = "", + allow_change_subj = true, + allow_query_users = true, + allow_private_messages = true, + allow_visitor_status = true, + allow_visitor_nickchange = true, + public = true, + public_list = true, + persistent = false, + moderated = true, + captcha_protected = false, + members_by_default = true, + members_only = false, + allow_user_invites = false, + password_protected = false, + password = "", + anonymous = true, + max_users = ?MAX_USERS_DEFAULT, + logging = false + }). + +-record(user, {jid, + nick, + role, + last_presence}). + +-record(activity, {message_time = 0, + presence_time = 0, + message_shaper, + presence_shaper, + message, + presence}). + +-record(state, {room, + host, + server_host, + access, + jid, + config = #config{}, + users = ?DICT:new(), + robots = ?DICT:new(), + affiliations = ?DICT:new(), + history, + subject = "", + subject_author = "", + just_created = false, + activity = treap:empty(), + room_shaper, + room_queue = queue:new()}). + +-record(muc_online_users, {us, + room, + host}). |