aboutsummaryrefslogtreecommitdiff
path: root/src/mod_muc
diff options
context:
space:
mode:
authorBadlop <badlop@process-one.net>2009-10-20 15:28:48 +0000
committerBadlop <badlop@process-one.net>2009-10-20 15:28:48 +0000
commit04545f2668a09c5a235007d663a2de0897dd9d18 (patch)
treebe39dbe996ef704c8a121263e107093f710faa90 /src/mod_muc
parentadd release_notes_2.0.1 (diff)
parentdoes 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.in4
-rw-r--r--src/mod_muc/Makefile.win323
-rw-r--r--src/mod_muc/mod_muc.erl165
-rw-r--r--src/mod_muc/mod_muc_log.erl361
-rw-r--r--src/mod_muc/mod_muc_room.erl797
-rw-r--r--src/mod_muc/mod_muc_room.hrl82
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\">&lt;~s&gt;</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, "\\&", "\\&amp;")),
S3 = element(2, regexp:gsub(S2, "<", "\\&lt;")),
S4 = element(2, regexp:gsub(S3, ">", "\\&gt;")),
- 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, " ", "\\&nbsp;\\&nbsp;")),
+ S7 = element(2, regexp:gsub(S6, "\\t", "\\&nbsp;\\&nbsp;\\&nbsp;\\&nbsp;")),
+ 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}).