aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>2012-05-03 18:34:53 +1000
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>2012-05-03 18:34:53 +1000
commitdb8bd0126b31ec5ffc4363ae7b8a08d504231081 (patch)
treef75074c87129cde71648c82dafe23d17a4d2abed /src
parentDo not trigger item-not-found errors in mod_http_bind (part of EJABS-1827) (diff)
Remove CRLFs introduced in the previous merge
Diffstat (limited to 'src')
-rw-r--r--src/mod_muc/mod_muc.erl2332
-rw-r--r--src/mod_muc/mod_muc_room.erl8452
2 files changed, 5392 insertions, 5392 deletions
diff --git a/src/mod_muc/mod_muc.erl b/src/mod_muc/mod_muc.erl
index 9a6b36187..049296f0e 100644
--- a/src/mod_muc/mod_muc.erl
+++ b/src/mod_muc/mod_muc.erl
@@ -1,1166 +1,1166 @@
-%%%----------------------------------------------------------------------
-%%% File : mod_muc.erl
-%%% Author : Alexey Shchepin <alexey@process-one.net>
-%%% Purpose : MUC support (XEP-0045)
-%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
-%%%
-%%%
-%%% ejabberd, Copyright (C) 2002-2012 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
-%%%
-%%%----------------------------------------------------------------------
-
--module(mod_muc).
--author('alexey@process-one.net').
-
--behaviour(gen_server).
--behaviour(gen_mod).
-
-%% API
--export([start_link/2,
- start/2,
- stop/1,
- room_destroyed/4,
- store_room/4,
- restore_room/3,
- forget_room/3,
- create_room/5,
- process_iq_disco_items/4,
- broadcast_service_message/2,
- register_room/3,
- node_up/1,
- node_down/1,
- migrate/3,
- get_vh_rooms/1,
- is_broadcasted/1,
- moderate_room_history/2,
- persist_recent_messages/1,
- can_use_nick/4]).
-
-%% gen_server callbacks
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--include("ejabberd.hrl").
--include("jlib.hrl").
-
-
--record(muc_room, {name_host, opts}).
--record(muc_online_room, {name_host, pid}).
--record(muc_registered, {us_host, nick}).
-
--record(state, {host,
- server_host,
- access,
- history_size,
- persist_history,
- default_room_opts,
- room_shaper}).
-
--define(PROCNAME, ejabberd_mod_muc).
-
-%%====================================================================
-%% API
-%%====================================================================
-%%--------------------------------------------------------------------
-%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
-%% Description: Starts the server
-%%--------------------------------------------------------------------
-start_link(Host, Opts) ->
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
- gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
-
-start(Host, Opts) ->
- start_supervisor(Host),
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
- ChildSpec =
- {Proc,
- {?MODULE, start_link, [Host, Opts]},
- temporary,
- 1000,
- worker,
- [?MODULE]},
- supervisor:start_child(ejabberd_sup, ChildSpec).
-
-stop(Host) ->
- %% if compiled with no transient supervisor, we need to manually shutdown
- %% the rooms to give them a chance to store persistent messages to DB
- Rooms = shutdown_rooms(Host),
- stop_supervisor(Host),
- Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
- gen_server:call(Proc, stop),
- supervisor:delete_child(ejabberd_sup, Proc),
- {wait, Rooms}. %%wait for rooms shutdown before stopping ejabberd
-
-shutdown_rooms(Host) ->
- MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
- Rooms = mnesia:dirty_select(muc_online_room,
- [{#muc_online_room{name_host = '$1', pid = '$2'},
- [{'==', {element, 2, '$1'}, MyHost}],
- ['$2']}]),
- [Pid ! 'shutdown' || Pid <- Rooms],
- Rooms.
-
-%% Returns {RoomsPersisted, MessagesPersisted}
-persist_recent_messages(Host) ->
- MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
- Rooms = mnesia:dirty_select(muc_online_room,
- [{#muc_online_room{name_host = '$1', pid = '$2'},
- [{'==', {element, 2, '$1'}, MyHost}],
- ['$2']}]),
- lists:foldl(fun(Pid, {NRooms, Messages}) ->
- case mod_muc_room:persist_recent_messages(Pid) of
- {ok, {persisted, N}} -> {NRooms +1, Messages +N};
- {ok, not_persistent} -> {NRooms, Messages}
- end end, {0, 0}, Rooms).
-
-moderate_room_history(RoomStr, Nick) ->
- Room = jlib:string_to_jid(RoomStr),
- Name = Room#jid.luser,
- Host = Room#jid.lserver,
- case mnesia:dirty_read(muc_online_room, {Name, Host}) of
- [] ->
- {error, not_found};
- [R] ->
- Pid = R#muc_online_room.pid,
- mod_muc_room:moderate_room_history(Pid, Nick)
- end.
-
-%% This function is called by a room in three situations:
-%% A) The owner of the room destroyed it
-%% B) The only participant of a temporary room leaves it
-%% C) mod_muc:stop was called, and each room is being terminated
-%% In this case, the mod_muc process died before the room processes
-%% So the message sending must be catched
-room_destroyed(Host, Room, Pid, ServerHost) ->
- catch gen_mod:get_module_proc(ServerHost, ?PROCNAME) !
- {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),
- RoomHost = gen_mod:get_module_opt_host(Host, ?MODULE, "conference.@HOST@"),
- Node = get_node({Name, RoomHost}),
- gen_server:call({Proc, Node}, {create, Name, From, Nick, Opts}).
-
-store_room(_ServerHost, Host, Name, Opts) ->
- F = fun() ->
- mnesia:write(#muc_room{name_host = {Name, Host},
- opts = Opts})
- end,
- mnesia:transaction(F).
-
-restore_room(_ServerHost, Host, Name) ->
- case catch mnesia:dirty_read(muc_room, {Name, Host}) of
- [#muc_room{opts = Opts}] ->
- Opts;
- _ ->
- error
- end.
-
-forget_room(_ServerHost, Host, Name) ->
- F = fun() ->
- mnesia:delete({muc_room, {Name, Host}})
- end,
- 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, Rsm)}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res)).
-
-can_use_nick(_ServerHost, _Host, _JID, "") ->
- false;
-can_use_nick(_ServerHost, Host, JID, Nick) ->
- {LUser, LServer, _} = jlib:jid_tolower(JID),
- LUS = {LUser, LServer},
- case catch mnesia:dirty_select(
- muc_registered,
- [{#muc_registered{us_host = '$1',
- nick = Nick,
- _ = '_'},
- [{'==', {element, 2, '$1'}, Host}],
- ['$_']}]) of
- {'EXIT', _Reason} ->
- true;
- [] ->
- true;
- [#muc_registered{us_host = {U, _Host}}] ->
- U == LUS
- end.
-
-migrate(_Node, _UpOrDown, After) ->
- Rs = mnesia:dirty_select(
- muc_online_room,
- [{#muc_online_room{name_host = '$1', pid = '$2', _ = '_'},
- [],
- ['$$']}]),
- lists:foreach(
- fun([NameHost, Pid]) ->
- case get_node(NameHost) of
- Node when Node /= node() ->
- mod_muc_room:migrate(Pid, Node, random:uniform(After));
- _ ->
- ok
- end
- end, Rs).
-
-node_up(_Node) ->
- copy_rooms(mnesia:dirty_first(muc_online_room)).
-
-node_down(Node) when Node == node() ->
- copy_rooms(mnesia:dirty_first(muc_online_room));
-node_down(_) ->
- ok.
-
-copy_rooms('$end_of_table') ->
- ok;
-copy_rooms(Key) ->
- case mnesia:dirty_read(muc_online_room, Key) of
- [#muc_online_room{name_host = NameHost} = Room] ->
- case get_node_new(NameHost) of
- Node when node() /= Node ->
- rpc:cast(Node, mnesia, dirty_write, [Room]);
- _ ->
- ok
- end;
- _ ->
- ok
- end,
- copy_rooms(mnesia:dirty_next(muc_online_room, Key)).
-
-%%====================================================================
-%% gen_server callbacks
-%%====================================================================
-
-%%--------------------------------------------------------------------
-%% Function: init(Args) -> {ok, State} |
-%% {ok, State, Timeout} |
-%% ignore |
-%% {stop, Reason}
-%% Description: Initiates the server
-%%--------------------------------------------------------------------
-init([Host, Opts]) ->
- update_muc_online_table(),
- mnesia:create_table(muc_room,
- [{disc_copies, [node()]},
- {attributes, record_info(fields, muc_room)}]),
- mnesia:create_table(muc_registered,
- [{disc_copies, [node()]},
- {attributes, record_info(fields, muc_registered)}]),
- mnesia:create_table(muc_online_room,
- [{ram_copies, [node()]},
- {local_content, true},
- {attributes, record_info(fields, muc_online_room)}]),
- mnesia:add_table_copy(muc_online_room, node(), ram_copies),
- catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
- MyHost = gen_mod:get_opt_host(Host, Opts, "conference.@HOST@"),
- update_tables(MyHost),
- mnesia:add_table_index(muc_registered, nick),
- Access = gen_mod:get_opt(access, Opts, all),
- AccessCreate = gen_mod:get_opt(access_create, Opts, all),
- AccessAdmin = gen_mod:get_opt(access_admin, Opts, none),
- AccessPersistent = gen_mod:get_opt(access_persistent, Opts, all),
- HistorySize = gen_mod:get_opt(history_size, Opts, 20),
- PersistHistory = gen_mod:get_opt(persist_history, Opts, false),
- DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, []),
- RoomShaper = gen_mod:get_opt(room_shaper, Opts, none),
- ejabberd_router:register_route(MyHost),
- ejabberd_hooks:add(node_up, ?MODULE, node_up, 100),
- ejabberd_hooks:add(node_down, ?MODULE, node_down, 100),
- ejabberd_hooks:add(node_hash_update, ?MODULE, migrate, 100),
- load_permanent_rooms(MyHost, Host,
- {Access, AccessCreate, AccessAdmin, AccessPersistent},
- HistorySize,
- PersistHistory,
- RoomShaper),
- {ok, #state{host = MyHost,
- server_host = Host,
- access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
- default_room_opts = DefRoomOpts,
- history_size = HistorySize,
- persist_history = PersistHistory,
- room_shaper = RoomShaper}}.
-
-%%--------------------------------------------------------------------
-%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
-%% {reply, Reply, State, Timeout} |
-%% {noreply, State} |
-%% {noreply, State, Timeout} |
-%% {stop, Reason, Reply, State} |
-%% {stop, Reason, State}
-%% Description: Handling call messages
-%%--------------------------------------------------------------------
-handle_call(stop, _From, 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,
- persist_history = PersistHistory,
- 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, PersistHistory,
- RoomShaper, From,
- Nick, NewOpts, ?MODULE),
- register_room(Host, Room, Pid),
- {reply, ok, State}.
-
-%%--------------------------------------------------------------------
-%% Function: handle_cast(Msg, State) -> {noreply, State} |
-%% {noreply, State, Timeout} |
-%% {stop, Reason, State}
-%% Description: Handling cast messages
-%%--------------------------------------------------------------------
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-%%--------------------------------------------------------------------
-%% Function: handle_info(Info, State) -> {noreply, State} |
-%% {noreply, State, Timeout} |
-%% {stop, Reason, State}
-%% Description: Handling all non call/cast messages
-%%--------------------------------------------------------------------
-handle_info({route, From, To, Packet},
- #state{host = Host,
- server_host = ServerHost,
- access = Access,
- default_room_opts = DefRoomOpts,
- history_size = HistorySize,
- persist_history = PersistHistory,
- room_shaper = RoomShaper} = State) ->
- {U, S, _} = jlib:jid_tolower(To),
- case get_node({U, S}) of
- Node when Node == node() ->
- case catch do_route(Host, ServerHost, Access, HistorySize, PersistHistory,
- RoomShaper, From, To, Packet, DefRoomOpts) of
- {'EXIT', Reason} ->
- ?ERROR_MSG("~p", [Reason]);
- _ ->
- ok
- end;
- Node ->
- Proc = gen_mod:get_module_proc(ServerHost, ?PROCNAME),
- {Proc, Node} ! {route, From, To, Packet}
- end,
- {noreply, State};
-handle_info({room_destroyed, RoomHost, Pid}, State) ->
- F = fun() ->
- mnesia:delete_object(#muc_online_room{name_host = RoomHost,
- pid = Pid})
- end,
- mnesia:async_dirty(F),
- case get_node_new(RoomHost) of
- Node when Node /= node() ->
- rpc:cast(Node, mnesia, dirty_delete_object,
- [#muc_online_room{name_host = RoomHost,
- pid = Pid}]);
- _ ->
- ok
- end,
- {noreply, State};
-handle_info(_Info, State) ->
- {noreply, State}.
-
-%%--------------------------------------------------------------------
-%% Function: terminate(Reason, State) -> void()
-%% Description: This function is called by a gen_server when it is about to
-%% terminate. It should be the opposite of Module:init/1 and do any necessary
-%% cleaning up. When it returns, the gen_server terminates with Reason.
-%% The return value is ignored.
-%%--------------------------------------------------------------------
-terminate(_Reason, State) ->
- ejabberd_hooks:delete(node_up, ?MODULE, node_up, 100),
- ejabberd_hooks:delete(node_down, ?MODULE, node_down, 100),
- ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100),
- ejabberd_router:unregister_route(State#state.host),
- ok.
-
-%%--------------------------------------------------------------------
-%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
-%% Description: Convert process state when code is changed
-%%--------------------------------------------------------------------
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
-
-%%--------------------------------------------------------------------
-%%% Internal functions
-%%--------------------------------------------------------------------
-start_supervisor(Host) ->
- Proc = gen_mod:get_module_proc(Host, ejabberd_mod_muc_sup),
- ChildSpec =
- {Proc,
- {ejabberd_tmp_sup, start_link,
- [Proc, mod_muc_room]},
- permanent,
- infinity,
- supervisor,
- [ejabberd_tmp_sup]},
- supervisor:start_child(ejabberd_sup, ChildSpec).
-
-stop_supervisor(Host) ->
- Proc = gen_mod:get_module_proc(Host, ejabberd_mod_muc_sup),
- supervisor:terminate_child(ejabberd_sup, Proc),
- supervisor:delete_child(ejabberd_sup, Proc).
-
-do_route(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
- From, To, Packet, DefRoomOpts) ->
- {AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
- case acl:match_rule(ServerHost, AccessRoute, From) of
- allow ->
- do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
- From, To, Packet, DefRoomOpts);
- _ ->
- {xmlelement, _Name, Attrs, _Els} = Packet,
- Lang = xml:get_attr_s("xml:lang", Attrs),
- ErrText = "Access denied by service policy",
- Err = jlib:make_error_reply(Packet,
- ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route_error(To, From, Err, Packet)
- end.
-
-
-do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
- From, To, Packet, DefRoomOpts) ->
- {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access,
- {Room, _, Nick} = jlib:jid_tolower(To),
- {xmlelement, Name, Attrs, _Els} = Packet,
- case Room of
- "" ->
- case Nick of
- "" ->
- case Name of
- "iq" ->
- 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)
- ++Info}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res));
- #iq{type = get,
- xmlns = ?NS_DISCO_ITEMS} = IQ ->
- spawn(?MODULE,
- process_iq_disco_items,
- [Host, From, To, IQ]);
- #iq{type = get,
- xmlns = ?NS_REGISTER = XMLNS,
- lang = Lang,
- sub_el = _SubEl} = IQ ->
- Res = IQ#iq{type = result,
- sub_el =
- [{xmlelement, "query",
- [{"xmlns", XMLNS}],
- iq_get_register_info(
- Host, From, Lang)}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res));
- #iq{type = set,
- xmlns = ?NS_REGISTER = XMLNS,
- lang = Lang,
- sub_el = SubEl} = IQ ->
- case process_iq_register_set(Host, From, SubEl, Lang) of
- {result, IQRes} ->
- Res = IQ#iq{type = result,
- sub_el =
- [{xmlelement, "query",
- [{"xmlns", XMLNS}],
- IQRes}]},
- ejabberd_router:route(
- To, From, jlib:iq_to_xml(Res));
- {error, Error} ->
- Err = jlib:make_error_reply(
- Packet, Error),
- ejabberd_router:route(
- To, From, Err)
- end;
- #iq{type = get,
- xmlns = ?NS_VCARD = XMLNS,
- lang = Lang,
- sub_el = _SubEl} = IQ ->
- Res = IQ#iq{type = result,
- sub_el =
- [{xmlelement, "vCard",
- [{"xmlns", XMLNS}],
- iq_get_vcard(Lang)}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res));
- #iq{type = get,
- xmlns = ?NS_MUC_UNIQUE
- } = IQ ->
- Res = IQ#iq{type = result,
- sub_el =
- [{xmlelement, "unique",
- [{"xmlns", ?NS_MUC_UNIQUE}],
- [iq_get_unique(From)]}]},
- ejabberd_router:route(To,
- From,
- jlib:iq_to_xml(Res));
- #iq{} ->
- Err = jlib:make_error_reply(
- Packet,
- ?ERR_FEATURE_NOT_IMPLEMENTED),
- ejabberd_router:route(To, From, Err);
- _ ->
- ok
- end;
- "message" ->
- case xml:get_attr_s("type", Attrs) of
- "error" ->
- ok;
- _ ->
- case acl:match_rule(ServerHost, AccessAdmin, From) of
- allow ->
- Msg = xml:get_path_s(
- Packet,
- [{elem, "body"}, cdata]),
- broadcast_service_message(Host, Msg);
- _ ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- ErrText = "Only service administrators "
- "are allowed to send service messages",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route(
- To, From, Err)
- end
- end;
- "presence" ->
- ok
- end;
- _ ->
- case xml:get_attr_s("type", Attrs) of
- "error" ->
- ok;
- "result" ->
- ok;
- _ ->
- Err = jlib:make_error_reply(
- Packet, ?ERR_ITEM_NOT_FOUND),
- ejabberd_router:route(To, From, Err)
- end
- end;
- _ ->
- case mnesia:dirty_read(muc_online_room, {Room, Host}) of
- [] ->
- Type = xml:get_attr_s("type", Attrs),
- case {Name, Type} of
- {"presence", ""} ->
- case check_user_can_create_room(ServerHost,
- AccessCreate, From,
- Room) of
- true ->
- case start_new_room(
- Host, ServerHost, Access,
- Room, HistorySize, PersistHistory,
- RoomShaper, From,
- Nick, DefRoomOpts) of
- {ok, Pid} ->
- mod_muc_room:route(Pid, From, Nick, Packet),
- register_room(Host, Room, Pid),
- ok;
- _Err ->
- Err = jlib:make_error_reply(
- Packet, ?ERR_INTERNAL_SERVER_ERROR),
- ejabberd_router:route(To, From, Err)
- end;
- false ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- ErrText = "Room creation is denied by service policy",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- ejabberd_router:route(To, From, Err)
- end;
- _ ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- ErrText = "Conference room does not exist",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
- ejabberd_router:route(To, From, Err)
- end;
- [R] ->
- Pid = R#muc_online_room.pid,
- ?DEBUG("MUC: send to process ~p~n", [Pid]),
- mod_muc_room:route(Pid, From, Nick, Packet),
- ok
- 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, ?MODULE,
- max_room_id, infinite));
- _ ->
- false
- end.
-
-
-load_permanent_rooms(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper) ->
- case catch mnesia:dirty_select(
- muc_room, [{#muc_room{name_host = {'_', Host}, _ = '_'},
- [],
- ['$_']}]) of
- {'EXIT', Reason} ->
- ?ERROR_MSG("~p", [Reason]),
- ok;
- Rs ->
- lists:foreach(
- fun(R) ->
- {Room, Host} = R#muc_room.name_host,
- case get_node({Room, Host}) of
- Node when Node == node() ->
- case mnesia:dirty_read(muc_online_room, {Room, Host}) of
- [] ->
- case get_room_state_if_broadcasted(
- {Room, Host}) of
- {ok, RoomState} ->
- mod_muc_room:start(
- normal_state, RoomState);
- error ->
- {ok, Pid} = mod_muc_room:start(
- Host,
- ServerHost,
- Access,
- Room,
- HistorySize,
- PersistHistory,
- RoomShaper,
- R#muc_room.opts,
- ?MODULE),
- register_room(Host, Room, Pid)
- end;
- _ ->
- ok
- end;
- _ ->
- ok
- end
- end, Rs)
- end.
-
-start_new_room(Host, ServerHost, Access, Room,
- HistorySize, PersistHistory, RoomShaper, From,
- Nick, DefRoomOpts) ->
- case get_room_state_if_broadcasted({Room, Host}) of
- {ok, RoomState} ->
- ?DEBUG("MUC: restore room '~s' from other node~n", [Room]),
- mod_muc_room:start(normal_state, RoomState);
- error ->
- 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, PersistHistory,
- RoomShaper, From,
- Nick, DefRoomOpts, ?MODULE);
- [#muc_room{opts = Opts}|_] ->
- ?DEBUG("MUC: restore room '~s'~n", [Room]),
- mod_muc_room:start(Host, ServerHost, Access,
- Room, HistorySize, PersistHistory,
- RoomShaper, Opts, ?MODULE)
- end
- end.
-
-register_room(Host, Room, Pid) ->
- F = fun() ->
- mnesia:write(#muc_online_room{name_host = {Room, Host},
- pid = Pid})
- end,
- mnesia:async_dirty(F),
- case get_node_new({Room, Host}) of
- Node when Node /= node() ->
- %% New node has just been added. But we may miss MUC records
- %% copy procedure, so we copy the MUC record manually just
- %% to make sure
- rpc:cast(Node, mnesia, dirty_write,
- [#muc_online_room{name_host = {Room, Host},
- pid = Pid}]),
- case get_node({Room, Host}) of
- Node when node() /= Node ->
- %% Migration to new node has completed, and seems like
- %% we missed it, so we migrate the MUC room pid manually.
- %% It is not a problem if we have already got migration
- %% notification: dups are just ignored by the MUC room pid.
- mod_muc_room:migrate(Pid, Node, 0);
- _ ->
- ok
- end;
- _ ->
- ok
- end.
-
-iq_disco_info(Lang) ->
- [{xmlelement, "identity",
- [{"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_MUC_UNIQUE}], []},
- {xmlelement, "feature", [{"var", ?NS_REGISTER}], []},
- {xmlelement, "feature", [{"var", ?NS_RSM}], []},
- {xmlelement, "feature", [{"var", ?NS_VCARD}], []}].
-
-
-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
- {item, Desc} ->
- flush(),
- {true,
- {xmlelement, "item",
- [{"jid", jlib:jid_to_string({Name, Host, ""})},
- {"name", Desc}], []}};
- _ ->
- false
- end
- end, get_vh_rooms_all_nodes(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 = get_vh_rooms_all_nodes(Host),
- Count = erlang:length(AllRooms),
- L = get_vh_rooms_direction(Direction, I, Index, AllRooms),
- 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.
-
-get_vh_rooms_direction(_Direction, _I, Index, AllRooms) when Index =/= undefined ->
- AllRooms;
-get_vh_rooms_direction(aft, I, _Index, AllRooms) ->
- {_Before, After} =
- lists:splitwith(
- fun(#muc_online_room{name_host = {Na, _}}) ->
- Na < I end, AllRooms),
- case After of
- [] -> [];
- [#muc_online_room{name_host = {I, _Host}} | AfterTail] -> AfterTail;
- _ -> After
- end;
-get_vh_rooms_direction(before, I, _Index, AllRooms) when I =/= []->
- {Before, _} =
- lists:splitwith(
- fun(#muc_online_room{name_host = {Na, _}}) ->
- Na < I end, AllRooms),
- Before;
-get_vh_rooms_direction(_Direction, _I, _Index, AllRooms) ->
- AllRooms.
-
-%% @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
- _ ->
- flush()
- after 0 ->
- ok
- end.
-
--define(XFIELD(Type, Label, Var, Val),
- {xmlelement, "field", [{"type", Type},
- {"label", translate:translate(Lang, Label)},
- {"var", Var}],
- [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
-
-%% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of
-%% the requester JID, the local time and a random salt.
-%%
-%% "pseudo" because we don't verify that there is not a room
-%% with the returned Name already created, nor mark the generated Name
-%% as "already used". But in practice, it is unique enough. See
-%% http://xmpp.org/extensions/xep-0045.html#createroom-unique
-iq_get_unique(From) ->
- {xmlcdata, sha:sha(term_to_binary([From, now(), randoms:get_string()]))}.
-
-iq_get_register_info(Host, From, Lang) ->
- {LUser, LServer, _} = jlib:jid_tolower(From),
- LUS = {LUser, LServer},
- {Nick, Registered} =
- case catch mnesia:dirty_read(muc_registered, {LUS, Host}) of
- {'EXIT', _Reason} ->
- {"", []};
- [] ->
- {"", []};
- [#muc_registered{nick = N}] ->
- {N, [{xmlelement, "registered", [], []}]}
- end,
- Registered ++
- [{xmlelement, "instructions", [],
- [{xmlcdata,
- translate:translate(
- Lang, "You need a client that supports x:data to register the nickname")}]},
- {xmlelement, "x",
- [{"xmlns", ?NS_XDATA}],
- [{xmlelement, "title", [],
- [{xmlcdata,
- translate:translate(
- Lang, "Nickname Registration at ") ++ Host}]},
- {xmlelement, "instructions", [],
- [{xmlcdata,
- translate:translate(
- Lang, "Enter nickname you want to register")}]},
- ?XFIELD("text-single", "Nickname", "nick", Nick)]}].
-
-iq_set_register_info(Host, From, Nick, Lang) ->
- {LUser, LServer, _} = jlib:jid_tolower(From),
- LUS = {LUser, LServer},
- F = fun() ->
- case Nick of
- "" ->
- mnesia:delete({muc_registered, {LUS, Host}}),
- ok;
- _ ->
- Allow =
- case mnesia:select(
- muc_registered,
- [{#muc_registered{us_host = '$1',
- nick = Nick,
- _ = '_'},
- [{'==', {element, 2, '$1'}, Host}],
- ['$_']}]) of
- [] ->
- true;
- [#muc_registered{us_host = {U, _Host}}] ->
- U == LUS
- end,
- if
- Allow ->
- mnesia:write(
- #muc_registered{us_host = {LUS, Host},
- nick = Nick}),
- ok;
- true ->
- false
- end
- end
- end,
- case mnesia:transaction(F) of
- {atomic, ok} ->
- {result, []};
- {atomic, false} ->
- ErrText = "That nickname is registered by another person",
- {error, ?ERRT_CONFLICT(Lang, ErrText)};
- _ ->
- {error, ?ERR_INTERNAL_SERVER_ERROR}
- end.
-
-process_iq_register_set(Host, From, SubEl, Lang) ->
- {xmlelement, _Name, _Attrs, Els} = SubEl,
- case xml:get_subtag(SubEl, "remove") of
- false ->
- case xml:remove_cdata(Els) of
- [{xmlelement, "x", _Attrs1, _Els1} = XEl] ->
- case {xml:get_tag_attr_s("xmlns", XEl),
- xml:get_tag_attr_s("type", XEl)} of
- {?NS_XDATA, "cancel"} ->
- {result, []};
- {?NS_XDATA, "submit"} ->
- XData = jlib:parse_xdata_submit(XEl),
- case XData of
- invalid ->
- {error, ?ERR_BAD_REQUEST};
- _ ->
- case lists:keysearch("nick", 1, XData) of
- {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)}
- end
- end;
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end;
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end;
- _ ->
- iq_set_register_info(Host, From, "", Lang)
- end.
-
-iq_get_vcard(Lang) ->
- [{xmlelement, "FN", [],
- [{xmlcdata, "ejabberd/mod_muc"}]},
- {xmlelement, "URL", [],
- [{xmlcdata, ?EJABBERD_URI}]},
- {xmlelement, "DESC", [],
- [{xmlcdata, translate:translate(Lang, "ejabberd MUC module") ++
- "\nCopyright (c) 2003-2012 ProcessOne"}]}].
-
-
-broadcast_service_message(Host, Msg) ->
- lists:foreach(
- fun(#muc_online_room{pid = Pid}) ->
- gen_fsm:send_all_state_event(
- Pid, {service_message, Msg})
- end, get_vh_rooms_all_nodes(Host)).
-
-get_vh_rooms_all_nodes(Host) ->
- Rooms = lists:foldl(
- fun(Node, Acc) when Node == node() ->
- get_vh_rooms(Host) ++ Acc;
- (Node, Acc) ->
- case catch rpc:call(Node, ?MODULE, get_vh_rooms,
- [Host], 5000) of
- Res when is_list(Res) ->
- Res ++ Acc;
- _ ->
- Acc
- end
- end, [], get_nodes(Host)),
- lists:ukeysort(#muc_online_room.name_host, Rooms).
-
-get_vh_rooms(Host) ->
- mnesia:dirty_select(muc_online_room,
- [{#muc_online_room{name_host = '$1', _ = '_'},
- [{'==', {element, 2, '$1'}, Host}],
- ['$_']}]).
-
-update_tables(Host) ->
- update_muc_room_table(Host),
- update_muc_registered_table(Host).
-
-update_muc_online_table() ->
- case catch mnesia:table_info(muc_online_room, local_content) of
- false ->
- mnesia:delete_table(muc_online_room);
- _ ->
- ok
- end.
-
-update_muc_room_table(Host) ->
- Fields = record_info(fields, muc_room),
- case mnesia:table_info(muc_room, attributes) of
- Fields ->
- ok;
- [name, opts] ->
- ?INFO_MSG("Converting muc_room table from "
- "{name, opts} format", []),
- {atomic, ok} = mnesia:create_table(
- mod_muc_tmp_table,
- [{disc_only_copies, [node()]},
- {type, bag},
- {local_content, true},
- {record_name, muc_room},
- {attributes, record_info(fields, muc_room)}]),
- mnesia:transform_table(muc_room, ignore, Fields),
- F1 = fun() ->
- mnesia:write_lock_table(mod_muc_tmp_table),
- mnesia:foldl(
- fun(#muc_room{name_host = Name} = R, _) ->
- mnesia:dirty_write(
- mod_muc_tmp_table,
- R#muc_room{name_host = {Name, Host}})
- end, ok, muc_room)
- end,
- mnesia:transaction(F1),
- mnesia:clear_table(muc_room),
- F2 = fun() ->
- mnesia:write_lock_table(muc_room),
- mnesia:foldl(
- fun(R, _) ->
- mnesia:dirty_write(R)
- end, ok, mod_muc_tmp_table)
- end,
- mnesia:transaction(F2),
- mnesia:delete_table(mod_muc_tmp_table);
- _ ->
- ?INFO_MSG("Recreating muc_room table", []),
- mnesia:transform_table(muc_room, ignore, Fields)
- end.
-
-
-update_muc_registered_table(Host) ->
- Fields = record_info(fields, muc_registered),
- case mnesia:table_info(muc_registered, attributes) of
- Fields ->
- ok;
- [user, nick] ->
- ?INFO_MSG("Converting muc_registered table from "
- "{user, nick} format", []),
- {atomic, ok} = mnesia:create_table(
- mod_muc_tmp_table,
- [{disc_only_copies, [node()]},
- {type, bag},
- {local_content, true},
- {record_name, muc_registered},
- {attributes, record_info(fields, muc_registered)}]),
- mnesia:del_table_index(muc_registered, nick),
- mnesia:transform_table(muc_registered, ignore, Fields),
- F1 = fun() ->
- mnesia:write_lock_table(mod_muc_tmp_table),
- mnesia:foldl(
- fun(#muc_registered{us_host = US} = R, _) ->
- mnesia:dirty_write(
- mod_muc_tmp_table,
- R#muc_registered{us_host = {US, Host}})
- end, ok, muc_registered)
- end,
- mnesia:transaction(F1),
- mnesia:clear_table(muc_registered),
- F2 = fun() ->
- mnesia:write_lock_table(muc_registered),
- mnesia:foldl(
- fun(R, _) ->
- mnesia:dirty_write(R)
- end, ok, mod_muc_tmp_table)
- end,
- mnesia:transaction(F2),
- mnesia:delete_table(mod_muc_tmp_table);
- _ ->
- ?INFO_MSG("Recreating muc_registered table", []),
- mnesia:transform_table(muc_registered, ignore, Fields)
- end.
-
-is_broadcasted(RoomHost) ->
- case ejabberd_config:get_local_option({domain_balancing, RoomHost}) of
- broadcast ->
- true;
- _ ->
- false
- end.
-
-get_node({_, RoomHost} = Key) ->
- case is_broadcasted(RoomHost) of
- true ->
- node();
- false ->
- ejabberd_cluster:get_node(Key)
- end;
-get_node(RoomHost) ->
- get_node({"", RoomHost}).
-
-get_node_new({_, RoomHost} = Key) ->
- case is_broadcasted(RoomHost) of
- true ->
- node();
- false ->
- ejabberd_cluster:get_node_new(Key)
- end;
-get_node_new(RoomHost) ->
- get_node_new({"", RoomHost}).
-
-get_nodes(RoomHost) ->
- case is_broadcasted(RoomHost) of
- true ->
- [node()];
- false ->
- ejabberd_cluster:get_nodes()
- end.
-
-get_room_state_if_broadcasted({Room, Host}) ->
- case is_broadcasted(Host) of
- true ->
- lists:foldl(
- fun(_, {ok, StateData}) ->
- {ok, StateData};
- (Node, _) when Node /= node() ->
- case catch rpc:call(
- Node, mnesia, dirty_read,
- [muc_online_room, {Room, Host}], 5000) of
- [#muc_online_room{pid = Pid}] ->
- case catch gen_fsm:sync_send_all_state_event(
- Pid, get_state, 5000) of
- {ok, StateData} ->
- {ok, StateData};
- _ ->
- error
- end;
- _ ->
- error
- end;
- (_, Acc) ->
- Acc
- end, error, ejabberd_cluster:get_nodes());
- false ->
- error
- end.
+%%%----------------------------------------------------------------------
+%%% File : mod_muc.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : MUC support (XEP-0045)
+%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2012 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
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_muc).
+-author('alexey@process-one.net').
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2,
+ start/2,
+ stop/1,
+ room_destroyed/4,
+ store_room/4,
+ restore_room/3,
+ forget_room/3,
+ create_room/5,
+ process_iq_disco_items/4,
+ broadcast_service_message/2,
+ register_room/3,
+ node_up/1,
+ node_down/1,
+ migrate/3,
+ get_vh_rooms/1,
+ is_broadcasted/1,
+ moderate_room_history/2,
+ persist_recent_messages/1,
+ can_use_nick/4]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+
+
+-record(muc_room, {name_host, opts}).
+-record(muc_online_room, {name_host, pid}).
+-record(muc_registered, {us_host, nick}).
+
+-record(state, {host,
+ server_host,
+ access,
+ history_size,
+ persist_history,
+ default_room_opts,
+ room_shaper}).
+
+-define(PROCNAME, ejabberd_mod_muc).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ start_supervisor(Host),
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ ChildSpec =
+ {Proc,
+ {?MODULE, start_link, [Host, Opts]},
+ temporary,
+ 1000,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop(Host) ->
+ %% if compiled with no transient supervisor, we need to manually shutdown
+ %% the rooms to give them a chance to store persistent messages to DB
+ Rooms = shutdown_rooms(Host),
+ stop_supervisor(Host),
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:delete_child(ejabberd_sup, Proc),
+ {wait, Rooms}. %%wait for rooms shutdown before stopping ejabberd
+
+shutdown_rooms(Host) ->
+ MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
+ Rooms = mnesia:dirty_select(muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2'},
+ [{'==', {element, 2, '$1'}, MyHost}],
+ ['$2']}]),
+ [Pid ! 'shutdown' || Pid <- Rooms],
+ Rooms.
+
+%% Returns {RoomsPersisted, MessagesPersisted}
+persist_recent_messages(Host) ->
+ MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@HOST@"),
+ Rooms = mnesia:dirty_select(muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2'},
+ [{'==', {element, 2, '$1'}, MyHost}],
+ ['$2']}]),
+ lists:foldl(fun(Pid, {NRooms, Messages}) ->
+ case mod_muc_room:persist_recent_messages(Pid) of
+ {ok, {persisted, N}} -> {NRooms +1, Messages +N};
+ {ok, not_persistent} -> {NRooms, Messages}
+ end end, {0, 0}, Rooms).
+
+moderate_room_history(RoomStr, Nick) ->
+ Room = jlib:string_to_jid(RoomStr),
+ Name = Room#jid.luser,
+ Host = Room#jid.lserver,
+ case mnesia:dirty_read(muc_online_room, {Name, Host}) of
+ [] ->
+ {error, not_found};
+ [R] ->
+ Pid = R#muc_online_room.pid,
+ mod_muc_room:moderate_room_history(Pid, Nick)
+ end.
+
+%% This function is called by a room in three situations:
+%% A) The owner of the room destroyed it
+%% B) The only participant of a temporary room leaves it
+%% C) mod_muc:stop was called, and each room is being terminated
+%% In this case, the mod_muc process died before the room processes
+%% So the message sending must be catched
+room_destroyed(Host, Room, Pid, ServerHost) ->
+ catch gen_mod:get_module_proc(ServerHost, ?PROCNAME) !
+ {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),
+ RoomHost = gen_mod:get_module_opt_host(Host, ?MODULE, "conference.@HOST@"),
+ Node = get_node({Name, RoomHost}),
+ gen_server:call({Proc, Node}, {create, Name, From, Nick, Opts}).
+
+store_room(_ServerHost, Host, Name, Opts) ->
+ F = fun() ->
+ mnesia:write(#muc_room{name_host = {Name, Host},
+ opts = Opts})
+ end,
+ mnesia:transaction(F).
+
+restore_room(_ServerHost, Host, Name) ->
+ case catch mnesia:dirty_read(muc_room, {Name, Host}) of
+ [#muc_room{opts = Opts}] ->
+ Opts;
+ _ ->
+ error
+ end.
+
+forget_room(_ServerHost, Host, Name) ->
+ F = fun() ->
+ mnesia:delete({muc_room, {Name, Host}})
+ end,
+ 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, Rsm)}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res)).
+
+can_use_nick(_ServerHost, _Host, _JID, "") ->
+ false;
+can_use_nick(_ServerHost, Host, JID, Nick) ->
+ {LUser, LServer, _} = jlib:jid_tolower(JID),
+ LUS = {LUser, LServer},
+ case catch mnesia:dirty_select(
+ muc_registered,
+ [{#muc_registered{us_host = '$1',
+ nick = Nick,
+ _ = '_'},
+ [{'==', {element, 2, '$1'}, Host}],
+ ['$_']}]) of
+ {'EXIT', _Reason} ->
+ true;
+ [] ->
+ true;
+ [#muc_registered{us_host = {U, _Host}}] ->
+ U == LUS
+ end.
+
+migrate(_Node, _UpOrDown, After) ->
+ Rs = mnesia:dirty_select(
+ muc_online_room,
+ [{#muc_online_room{name_host = '$1', pid = '$2', _ = '_'},
+ [],
+ ['$$']}]),
+ lists:foreach(
+ fun([NameHost, Pid]) ->
+ case get_node(NameHost) of
+ Node when Node /= node() ->
+ mod_muc_room:migrate(Pid, Node, random:uniform(After));
+ _ ->
+ ok
+ end
+ end, Rs).
+
+node_up(_Node) ->
+ copy_rooms(mnesia:dirty_first(muc_online_room)).
+
+node_down(Node) when Node == node() ->
+ copy_rooms(mnesia:dirty_first(muc_online_room));
+node_down(_) ->
+ ok.
+
+copy_rooms('$end_of_table') ->
+ ok;
+copy_rooms(Key) ->
+ case mnesia:dirty_read(muc_online_room, Key) of
+ [#muc_online_room{name_host = NameHost} = Room] ->
+ case get_node_new(NameHost) of
+ Node when node() /= Node ->
+ rpc:cast(Node, mnesia, dirty_write, [Room]);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
+ copy_rooms(mnesia:dirty_next(muc_online_room, Key)).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% Description: Initiates the server
+%%--------------------------------------------------------------------
+init([Host, Opts]) ->
+ update_muc_online_table(),
+ mnesia:create_table(muc_room,
+ [{disc_copies, [node()]},
+ {attributes, record_info(fields, muc_room)}]),
+ mnesia:create_table(muc_registered,
+ [{disc_copies, [node()]},
+ {attributes, record_info(fields, muc_registered)}]),
+ mnesia:create_table(muc_online_room,
+ [{ram_copies, [node()]},
+ {local_content, true},
+ {attributes, record_info(fields, muc_online_room)}]),
+ mnesia:add_table_copy(muc_online_room, node(), ram_copies),
+ catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
+ MyHost = gen_mod:get_opt_host(Host, Opts, "conference.@HOST@"),
+ update_tables(MyHost),
+ mnesia:add_table_index(muc_registered, nick),
+ Access = gen_mod:get_opt(access, Opts, all),
+ AccessCreate = gen_mod:get_opt(access_create, Opts, all),
+ AccessAdmin = gen_mod:get_opt(access_admin, Opts, none),
+ AccessPersistent = gen_mod:get_opt(access_persistent, Opts, all),
+ HistorySize = gen_mod:get_opt(history_size, Opts, 20),
+ PersistHistory = gen_mod:get_opt(persist_history, Opts, false),
+ DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, []),
+ RoomShaper = gen_mod:get_opt(room_shaper, Opts, none),
+ ejabberd_router:register_route(MyHost),
+ ejabberd_hooks:add(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:add(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:add(node_hash_update, ?MODULE, migrate, 100),
+ load_permanent_rooms(MyHost, Host,
+ {Access, AccessCreate, AccessAdmin, AccessPersistent},
+ HistorySize,
+ PersistHistory,
+ RoomShaper),
+ {ok, #state{host = MyHost,
+ server_host = Host,
+ access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
+ default_room_opts = DefRoomOpts,
+ history_size = HistorySize,
+ persist_history = PersistHistory,
+ room_shaper = RoomShaper}}.
+
+%%--------------------------------------------------------------------
+%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% Description: Handling call messages
+%%--------------------------------------------------------------------
+handle_call(stop, _From, 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,
+ persist_history = PersistHistory,
+ 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, PersistHistory,
+ RoomShaper, From,
+ Nick, NewOpts, ?MODULE),
+ register_room(Host, Room, Pid),
+ {reply, ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling cast messages
+%%--------------------------------------------------------------------
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling all non call/cast messages
+%%--------------------------------------------------------------------
+handle_info({route, From, To, Packet},
+ #state{host = Host,
+ server_host = ServerHost,
+ access = Access,
+ default_room_opts = DefRoomOpts,
+ history_size = HistorySize,
+ persist_history = PersistHistory,
+ room_shaper = RoomShaper} = State) ->
+ {U, S, _} = jlib:jid_tolower(To),
+ case get_node({U, S}) of
+ Node when Node == node() ->
+ case catch do_route(Host, ServerHost, Access, HistorySize, PersistHistory,
+ RoomShaper, From, To, Packet, DefRoomOpts) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p", [Reason]);
+ _ ->
+ ok
+ end;
+ Node ->
+ Proc = gen_mod:get_module_proc(ServerHost, ?PROCNAME),
+ {Proc, Node} ! {route, From, To, Packet}
+ end,
+ {noreply, State};
+handle_info({room_destroyed, RoomHost, Pid}, State) ->
+ F = fun() ->
+ mnesia:delete_object(#muc_online_room{name_host = RoomHost,
+ pid = Pid})
+ end,
+ mnesia:async_dirty(F),
+ case get_node_new(RoomHost) of
+ Node when Node /= node() ->
+ rpc:cast(Node, mnesia, dirty_delete_object,
+ [#muc_online_room{name_host = RoomHost,
+ pid = Pid}]);
+ _ ->
+ ok
+ end,
+ {noreply, State};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description: This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%%--------------------------------------------------------------------
+terminate(_Reason, State) ->
+ ejabberd_hooks:delete(node_up, ?MODULE, node_up, 100),
+ ejabberd_hooks:delete(node_down, ?MODULE, node_down, 100),
+ ejabberd_hooks:delete(node_hash_update, ?MODULE, migrate, 100),
+ ejabberd_router:unregister_route(State#state.host),
+ ok.
+
+%%--------------------------------------------------------------------
+%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+start_supervisor(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ejabberd_mod_muc_sup),
+ ChildSpec =
+ {Proc,
+ {ejabberd_tmp_sup, start_link,
+ [Proc, mod_muc_room]},
+ permanent,
+ infinity,
+ supervisor,
+ [ejabberd_tmp_sup]},
+ supervisor:start_child(ejabberd_sup, ChildSpec).
+
+stop_supervisor(Host) ->
+ Proc = gen_mod:get_module_proc(Host, ejabberd_mod_muc_sup),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+do_route(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
+ From, To, Packet, DefRoomOpts) ->
+ {AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access,
+ case acl:match_rule(ServerHost, AccessRoute, From) of
+ allow ->
+ do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
+ From, To, Packet, DefRoomOpts);
+ _ ->
+ {xmlelement, _Name, Attrs, _Els} = Packet,
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ ErrText = "Access denied by service policy",
+ Err = jlib:make_error_reply(Packet,
+ ?ERRT_FORBIDDEN(Lang, ErrText)),
+ ejabberd_router:route_error(To, From, Err, Packet)
+ end.
+
+
+do_route1(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper,
+ From, To, Packet, DefRoomOpts) ->
+ {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access,
+ {Room, _, Nick} = jlib:jid_tolower(To),
+ {xmlelement, Name, Attrs, _Els} = Packet,
+ case Room of
+ "" ->
+ case Nick of
+ "" ->
+ case Name of
+ "iq" ->
+ 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)
+ ++Info}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res));
+ #iq{type = get,
+ xmlns = ?NS_DISCO_ITEMS} = IQ ->
+ spawn(?MODULE,
+ process_iq_disco_items,
+ [Host, From, To, IQ]);
+ #iq{type = get,
+ xmlns = ?NS_REGISTER = XMLNS,
+ lang = Lang,
+ sub_el = _SubEl} = IQ ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [{xmlelement, "query",
+ [{"xmlns", XMLNS}],
+ iq_get_register_info(
+ Host, From, Lang)}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res));
+ #iq{type = set,
+ xmlns = ?NS_REGISTER = XMLNS,
+ lang = Lang,
+ sub_el = SubEl} = IQ ->
+ case process_iq_register_set(Host, From, SubEl, Lang) of
+ {result, IQRes} ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [{xmlelement, "query",
+ [{"xmlns", XMLNS}],
+ IQRes}]},
+ ejabberd_router:route(
+ To, From, jlib:iq_to_xml(Res));
+ {error, Error} ->
+ Err = jlib:make_error_reply(
+ Packet, Error),
+ ejabberd_router:route(
+ To, From, Err)
+ end;
+ #iq{type = get,
+ xmlns = ?NS_VCARD = XMLNS,
+ lang = Lang,
+ sub_el = _SubEl} = IQ ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [{xmlelement, "vCard",
+ [{"xmlns", XMLNS}],
+ iq_get_vcard(Lang)}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res));
+ #iq{type = get,
+ xmlns = ?NS_MUC_UNIQUE
+ } = IQ ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [{xmlelement, "unique",
+ [{"xmlns", ?NS_MUC_UNIQUE}],
+ [iq_get_unique(From)]}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res));
+ #iq{} ->
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERR_FEATURE_NOT_IMPLEMENTED),
+ ejabberd_router:route(To, From, Err);
+ _ ->
+ ok
+ end;
+ "message" ->
+ case xml:get_attr_s("type", Attrs) of
+ "error" ->
+ ok;
+ _ ->
+ case acl:match_rule(ServerHost, AccessAdmin, From) of
+ allow ->
+ Msg = xml:get_path_s(
+ Packet,
+ [{elem, "body"}, cdata]),
+ broadcast_service_message(Host, Msg);
+ _ ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ ErrText = "Only service administrators "
+ "are allowed to send service messages",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_FORBIDDEN(Lang, ErrText)),
+ ejabberd_router:route(
+ To, From, Err)
+ end
+ end;
+ "presence" ->
+ ok
+ end;
+ _ ->
+ case xml:get_attr_s("type", Attrs) of
+ "error" ->
+ ok;
+ "result" ->
+ ok;
+ _ ->
+ Err = jlib:make_error_reply(
+ Packet, ?ERR_ITEM_NOT_FOUND),
+ ejabberd_router:route(To, From, Err)
+ end
+ end;
+ _ ->
+ case mnesia:dirty_read(muc_online_room, {Room, Host}) of
+ [] ->
+ Type = xml:get_attr_s("type", Attrs),
+ case {Name, Type} of
+ {"presence", ""} ->
+ case check_user_can_create_room(ServerHost,
+ AccessCreate, From,
+ Room) of
+ true ->
+ case start_new_room(
+ Host, ServerHost, Access,
+ Room, HistorySize, PersistHistory,
+ RoomShaper, From,
+ Nick, DefRoomOpts) of
+ {ok, Pid} ->
+ mod_muc_room:route(Pid, From, Nick, Packet),
+ register_room(Host, Room, Pid),
+ ok;
+ _Err ->
+ Err = jlib:make_error_reply(
+ Packet, ?ERR_INTERNAL_SERVER_ERROR),
+ ejabberd_router:route(To, From, Err)
+ end;
+ false ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ ErrText = "Room creation is denied by service policy",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
+ ejabberd_router:route(To, From, Err)
+ end;
+ _ ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ ErrText = "Conference room does not exist",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
+ ejabberd_router:route(To, From, Err)
+ end;
+ [R] ->
+ Pid = R#muc_online_room.pid,
+ ?DEBUG("MUC: send to process ~p~n", [Pid]),
+ mod_muc_room:route(Pid, From, Nick, Packet),
+ ok
+ 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, ?MODULE,
+ max_room_id, infinite));
+ _ ->
+ false
+ end.
+
+
+load_permanent_rooms(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper) ->
+ case catch mnesia:dirty_select(
+ muc_room, [{#muc_room{name_host = {'_', Host}, _ = '_'},
+ [],
+ ['$_']}]) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p", [Reason]),
+ ok;
+ Rs ->
+ lists:foreach(
+ fun(R) ->
+ {Room, Host} = R#muc_room.name_host,
+ case get_node({Room, Host}) of
+ Node when Node == node() ->
+ case mnesia:dirty_read(muc_online_room, {Room, Host}) of
+ [] ->
+ case get_room_state_if_broadcasted(
+ {Room, Host}) of
+ {ok, RoomState} ->
+ mod_muc_room:start(
+ normal_state, RoomState);
+ error ->
+ {ok, Pid} = mod_muc_room:start(
+ Host,
+ ServerHost,
+ Access,
+ Room,
+ HistorySize,
+ PersistHistory,
+ RoomShaper,
+ R#muc_room.opts,
+ ?MODULE),
+ register_room(Host, Room, Pid)
+ end;
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end
+ end, Rs)
+ end.
+
+start_new_room(Host, ServerHost, Access, Room,
+ HistorySize, PersistHistory, RoomShaper, From,
+ Nick, DefRoomOpts) ->
+ case get_room_state_if_broadcasted({Room, Host}) of
+ {ok, RoomState} ->
+ ?DEBUG("MUC: restore room '~s' from other node~n", [Room]),
+ mod_muc_room:start(normal_state, RoomState);
+ error ->
+ 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, PersistHistory,
+ RoomShaper, From,
+ Nick, DefRoomOpts, ?MODULE);
+ [#muc_room{opts = Opts}|_] ->
+ ?DEBUG("MUC: restore room '~s'~n", [Room]),
+ mod_muc_room:start(Host, ServerHost, Access,
+ Room, HistorySize, PersistHistory,
+ RoomShaper, Opts, ?MODULE)
+ end
+ end.
+
+register_room(Host, Room, Pid) ->
+ F = fun() ->
+ mnesia:write(#muc_online_room{name_host = {Room, Host},
+ pid = Pid})
+ end,
+ mnesia:async_dirty(F),
+ case get_node_new({Room, Host}) of
+ Node when Node /= node() ->
+ %% New node has just been added. But we may miss MUC records
+ %% copy procedure, so we copy the MUC record manually just
+ %% to make sure
+ rpc:cast(Node, mnesia, dirty_write,
+ [#muc_online_room{name_host = {Room, Host},
+ pid = Pid}]),
+ case get_node({Room, Host}) of
+ Node when node() /= Node ->
+ %% Migration to new node has completed, and seems like
+ %% we missed it, so we migrate the MUC room pid manually.
+ %% It is not a problem if we have already got migration
+ %% notification: dups are just ignored by the MUC room pid.
+ mod_muc_room:migrate(Pid, Node, 0);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
+
+iq_disco_info(Lang) ->
+ [{xmlelement, "identity",
+ [{"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_MUC_UNIQUE}], []},
+ {xmlelement, "feature", [{"var", ?NS_REGISTER}], []},
+ {xmlelement, "feature", [{"var", ?NS_RSM}], []},
+ {xmlelement, "feature", [{"var", ?NS_VCARD}], []}].
+
+
+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
+ {item, Desc} ->
+ flush(),
+ {true,
+ {xmlelement, "item",
+ [{"jid", jlib:jid_to_string({Name, Host, ""})},
+ {"name", Desc}], []}};
+ _ ->
+ false
+ end
+ end, get_vh_rooms_all_nodes(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 = get_vh_rooms_all_nodes(Host),
+ Count = erlang:length(AllRooms),
+ L = get_vh_rooms_direction(Direction, I, Index, AllRooms),
+ 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.
+
+get_vh_rooms_direction(_Direction, _I, Index, AllRooms) when Index =/= undefined ->
+ AllRooms;
+get_vh_rooms_direction(aft, I, _Index, AllRooms) ->
+ {_Before, After} =
+ lists:splitwith(
+ fun(#muc_online_room{name_host = {Na, _}}) ->
+ Na < I end, AllRooms),
+ case After of
+ [] -> [];
+ [#muc_online_room{name_host = {I, _Host}} | AfterTail] -> AfterTail;
+ _ -> After
+ end;
+get_vh_rooms_direction(before, I, _Index, AllRooms) when I =/= []->
+ {Before, _} =
+ lists:splitwith(
+ fun(#muc_online_room{name_host = {Na, _}}) ->
+ Na < I end, AllRooms),
+ Before;
+get_vh_rooms_direction(_Direction, _I, _Index, AllRooms) ->
+ AllRooms.
+
+%% @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
+ _ ->
+ flush()
+ after 0 ->
+ ok
+ end.
+
+-define(XFIELD(Type, Label, Var, Val),
+ {xmlelement, "field", [{"type", Type},
+ {"label", translate:translate(Lang, Label)},
+ {"var", Var}],
+ [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
+
+%% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of
+%% the requester JID, the local time and a random salt.
+%%
+%% "pseudo" because we don't verify that there is not a room
+%% with the returned Name already created, nor mark the generated Name
+%% as "already used". But in practice, it is unique enough. See
+%% http://xmpp.org/extensions/xep-0045.html#createroom-unique
+iq_get_unique(From) ->
+ {xmlcdata, sha:sha(term_to_binary([From, now(), randoms:get_string()]))}.
+
+iq_get_register_info(Host, From, Lang) ->
+ {LUser, LServer, _} = jlib:jid_tolower(From),
+ LUS = {LUser, LServer},
+ {Nick, Registered} =
+ case catch mnesia:dirty_read(muc_registered, {LUS, Host}) of
+ {'EXIT', _Reason} ->
+ {"", []};
+ [] ->
+ {"", []};
+ [#muc_registered{nick = N}] ->
+ {N, [{xmlelement, "registered", [], []}]}
+ end,
+ Registered ++
+ [{xmlelement, "instructions", [],
+ [{xmlcdata,
+ translate:translate(
+ Lang, "You need a client that supports x:data to register the nickname")}]},
+ {xmlelement, "x",
+ [{"xmlns", ?NS_XDATA}],
+ [{xmlelement, "title", [],
+ [{xmlcdata,
+ translate:translate(
+ Lang, "Nickname Registration at ") ++ Host}]},
+ {xmlelement, "instructions", [],
+ [{xmlcdata,
+ translate:translate(
+ Lang, "Enter nickname you want to register")}]},
+ ?XFIELD("text-single", "Nickname", "nick", Nick)]}].
+
+iq_set_register_info(Host, From, Nick, Lang) ->
+ {LUser, LServer, _} = jlib:jid_tolower(From),
+ LUS = {LUser, LServer},
+ F = fun() ->
+ case Nick of
+ "" ->
+ mnesia:delete({muc_registered, {LUS, Host}}),
+ ok;
+ _ ->
+ Allow =
+ case mnesia:select(
+ muc_registered,
+ [{#muc_registered{us_host = '$1',
+ nick = Nick,
+ _ = '_'},
+ [{'==', {element, 2, '$1'}, Host}],
+ ['$_']}]) of
+ [] ->
+ true;
+ [#muc_registered{us_host = {U, _Host}}] ->
+ U == LUS
+ end,
+ if
+ Allow ->
+ mnesia:write(
+ #muc_registered{us_host = {LUS, Host},
+ nick = Nick}),
+ ok;
+ true ->
+ false
+ end
+ end
+ end,
+ case mnesia:transaction(F) of
+ {atomic, ok} ->
+ {result, []};
+ {atomic, false} ->
+ ErrText = "That nickname is registered by another person",
+ {error, ?ERRT_CONFLICT(Lang, ErrText)};
+ _ ->
+ {error, ?ERR_INTERNAL_SERVER_ERROR}
+ end.
+
+process_iq_register_set(Host, From, SubEl, Lang) ->
+ {xmlelement, _Name, _Attrs, Els} = SubEl,
+ case xml:get_subtag(SubEl, "remove") of
+ false ->
+ case xml:remove_cdata(Els) of
+ [{xmlelement, "x", _Attrs1, _Els1} = XEl] ->
+ case {xml:get_tag_attr_s("xmlns", XEl),
+ xml:get_tag_attr_s("type", XEl)} of
+ {?NS_XDATA, "cancel"} ->
+ {result, []};
+ {?NS_XDATA, "submit"} ->
+ XData = jlib:parse_xdata_submit(XEl),
+ case XData of
+ invalid ->
+ {error, ?ERR_BAD_REQUEST};
+ _ ->
+ case lists:keysearch("nick", 1, XData) of
+ {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)}
+ end
+ end;
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end;
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end;
+ _ ->
+ iq_set_register_info(Host, From, "", Lang)
+ end.
+
+iq_get_vcard(Lang) ->
+ [{xmlelement, "FN", [],
+ [{xmlcdata, "ejabberd/mod_muc"}]},
+ {xmlelement, "URL", [],
+ [{xmlcdata, ?EJABBERD_URI}]},
+ {xmlelement, "DESC", [],
+ [{xmlcdata, translate:translate(Lang, "ejabberd MUC module") ++
+ "\nCopyright (c) 2003-2012 ProcessOne"}]}].
+
+
+broadcast_service_message(Host, Msg) ->
+ lists:foreach(
+ fun(#muc_online_room{pid = Pid}) ->
+ gen_fsm:send_all_state_event(
+ Pid, {service_message, Msg})
+ end, get_vh_rooms_all_nodes(Host)).
+
+get_vh_rooms_all_nodes(Host) ->
+ Rooms = lists:foldl(
+ fun(Node, Acc) when Node == node() ->
+ get_vh_rooms(Host) ++ Acc;
+ (Node, Acc) ->
+ case catch rpc:call(Node, ?MODULE, get_vh_rooms,
+ [Host], 5000) of
+ Res when is_list(Res) ->
+ Res ++ Acc;
+ _ ->
+ Acc
+ end
+ end, [], get_nodes(Host)),
+ lists:ukeysort(#muc_online_room.name_host, Rooms).
+
+get_vh_rooms(Host) ->
+ mnesia:dirty_select(muc_online_room,
+ [{#muc_online_room{name_host = '$1', _ = '_'},
+ [{'==', {element, 2, '$1'}, Host}],
+ ['$_']}]).
+
+update_tables(Host) ->
+ update_muc_room_table(Host),
+ update_muc_registered_table(Host).
+
+update_muc_online_table() ->
+ case catch mnesia:table_info(muc_online_room, local_content) of
+ false ->
+ mnesia:delete_table(muc_online_room);
+ _ ->
+ ok
+ end.
+
+update_muc_room_table(Host) ->
+ Fields = record_info(fields, muc_room),
+ case mnesia:table_info(muc_room, attributes) of
+ Fields ->
+ ok;
+ [name, opts] ->
+ ?INFO_MSG("Converting muc_room table from "
+ "{name, opts} format", []),
+ {atomic, ok} = mnesia:create_table(
+ mod_muc_tmp_table,
+ [{disc_only_copies, [node()]},
+ {type, bag},
+ {local_content, true},
+ {record_name, muc_room},
+ {attributes, record_info(fields, muc_room)}]),
+ mnesia:transform_table(muc_room, ignore, Fields),
+ F1 = fun() ->
+ mnesia:write_lock_table(mod_muc_tmp_table),
+ mnesia:foldl(
+ fun(#muc_room{name_host = Name} = R, _) ->
+ mnesia:dirty_write(
+ mod_muc_tmp_table,
+ R#muc_room{name_host = {Name, Host}})
+ end, ok, muc_room)
+ end,
+ mnesia:transaction(F1),
+ mnesia:clear_table(muc_room),
+ F2 = fun() ->
+ mnesia:write_lock_table(muc_room),
+ mnesia:foldl(
+ fun(R, _) ->
+ mnesia:dirty_write(R)
+ end, ok, mod_muc_tmp_table)
+ end,
+ mnesia:transaction(F2),
+ mnesia:delete_table(mod_muc_tmp_table);
+ _ ->
+ ?INFO_MSG("Recreating muc_room table", []),
+ mnesia:transform_table(muc_room, ignore, Fields)
+ end.
+
+
+update_muc_registered_table(Host) ->
+ Fields = record_info(fields, muc_registered),
+ case mnesia:table_info(muc_registered, attributes) of
+ Fields ->
+ ok;
+ [user, nick] ->
+ ?INFO_MSG("Converting muc_registered table from "
+ "{user, nick} format", []),
+ {atomic, ok} = mnesia:create_table(
+ mod_muc_tmp_table,
+ [{disc_only_copies, [node()]},
+ {type, bag},
+ {local_content, true},
+ {record_name, muc_registered},
+ {attributes, record_info(fields, muc_registered)}]),
+ mnesia:del_table_index(muc_registered, nick),
+ mnesia:transform_table(muc_registered, ignore, Fields),
+ F1 = fun() ->
+ mnesia:write_lock_table(mod_muc_tmp_table),
+ mnesia:foldl(
+ fun(#muc_registered{us_host = US} = R, _) ->
+ mnesia:dirty_write(
+ mod_muc_tmp_table,
+ R#muc_registered{us_host = {US, Host}})
+ end, ok, muc_registered)
+ end,
+ mnesia:transaction(F1),
+ mnesia:clear_table(muc_registered),
+ F2 = fun() ->
+ mnesia:write_lock_table(muc_registered),
+ mnesia:foldl(
+ fun(R, _) ->
+ mnesia:dirty_write(R)
+ end, ok, mod_muc_tmp_table)
+ end,
+ mnesia:transaction(F2),
+ mnesia:delete_table(mod_muc_tmp_table);
+ _ ->
+ ?INFO_MSG("Recreating muc_registered table", []),
+ mnesia:transform_table(muc_registered, ignore, Fields)
+ end.
+
+is_broadcasted(RoomHost) ->
+ case ejabberd_config:get_local_option({domain_balancing, RoomHost}) of
+ broadcast ->
+ true;
+ _ ->
+ false
+ end.
+
+get_node({_, RoomHost} = Key) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ node();
+ false ->
+ ejabberd_cluster:get_node(Key)
+ end;
+get_node(RoomHost) ->
+ get_node({"", RoomHost}).
+
+get_node_new({_, RoomHost} = Key) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ node();
+ false ->
+ ejabberd_cluster:get_node_new(Key)
+ end;
+get_node_new(RoomHost) ->
+ get_node_new({"", RoomHost}).
+
+get_nodes(RoomHost) ->
+ case is_broadcasted(RoomHost) of
+ true ->
+ [node()];
+ false ->
+ ejabberd_cluster:get_nodes()
+ end.
+
+get_room_state_if_broadcasted({Room, Host}) ->
+ case is_broadcasted(Host) of
+ true ->
+ lists:foldl(
+ fun(_, {ok, StateData}) ->
+ {ok, StateData};
+ (Node, _) when Node /= node() ->
+ case catch rpc:call(
+ Node, mnesia, dirty_read,
+ [muc_online_room, {Room, Host}], 5000) of
+ [#muc_online_room{pid = Pid}] ->
+ case catch gen_fsm:sync_send_all_state_event(
+ Pid, get_state, 5000) of
+ {ok, StateData} ->
+ {ok, StateData};
+ _ ->
+ error
+ end;
+ _ ->
+ error
+ end;
+ (_, Acc) ->
+ Acc
+ end, error, ejabberd_cluster:get_nodes());
+ false ->
+ error
+ end.
diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl
index 79b9c70ac..16ba52f67 100644
--- a/src/mod_muc/mod_muc_room.erl
+++ b/src/mod_muc/mod_muc_room.erl
@@ -1,4226 +1,4226 @@
-%%%----------------------------------------------------------------------
-%%% File : mod_muc_room.erl
-%%% Author : Alexey Shchepin <alexey@process-one.net>
-%%% Purpose : MUC room stuff
-%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
-%%%
-%%%
-%%% ejabberd, Copyright (C) 2002-2012 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
-%%%
-%%%----------------------------------------------------------------------
-
--module(mod_muc_room).
--author('alexey@process-one.net').
-
--define(GEN_FSM, p1_fsm).
-
--behaviour(?GEN_FSM).
-
-
-%% External exports
--export([start_link/11,
- start_link/9,
- start_link/2,
- start/11,
- start/9,
- start/2,
- migrate/3,
- route/4,
- moderate_room_history/2,
- persist_recent_messages/1]).
-
-%% gen_fsm callbacks
--export([init/1,
- normal_state/2,
- handle_event/3,
- handle_sync_event/4,
- handle_info/3,
- terminate/3,
- print_state/1,
- code_change/4]).
-
--include("ejabberd.hrl").
--include("jlib.hrl").
--include("mod_muc_room.hrl").
-
--define(MAX_USERS_DEFAULT_LIST,
- [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
-
-%-define(DBGFSM, true).
-
--ifdef(DBGFSM).
--define(FSMOPTS, [{debug, [trace]}]).
--else.
--define(FSMOPTS, []).
--endif.
-
-%% Module start with or without supervisor:
--ifdef(NO_TRANSIENT_SUPERVISORS).
--define(SUPERVISOR_START(Args),
- ?GEN_FSM:start(?MODULE, Args, ?FSMOPTS)).
--else.
--define(SUPERVISOR_START(Args),
- Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
- supervisor:start_child(Supervisor, Args)).
--endif.
-
-%%%----------------------------------------------------------------------
-%%% API
-%%%----------------------------------------------------------------------
-start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
- Creator, Nick, DefRoomOpts, Mod) ->
- ?SUPERVISOR_START([Host, ServerHost, Access, Room, HistorySize, PersistHistory,
- RoomShaper, Creator, Nick, DefRoomOpts, Mod]).
-
-start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod) ->
- Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
- supervisor:start_child(
- Supervisor, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
- Opts, Mod]).
-
-start(StateName, StateData) ->
- ServerHost = StateData#state.server_host,
- ?SUPERVISOR_START([StateName, StateData]).
-
-start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
- Creator, Nick, DefRoomOpts, Mod) ->
- ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
- RoomShaper, Creator, Nick, DefRoomOpts, Mod],
- ?FSMOPTS).
-
-start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod) ->
- ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
- RoomShaper, Opts, Mod],
- ?FSMOPTS).
-
-start_link(StateName, StateData) ->
- ?GEN_FSM:start_link(?MODULE, [StateName, StateData], ?FSMOPTS).
-
-migrate(FsmRef, Node, After) ->
- erlang:send_after(After, FsmRef, {migrate, Node}).
-
-moderate_room_history(FsmRef, Nick) ->
- ?GEN_FSM:sync_send_all_state_event(FsmRef, {moderate_room_history, Nick}).
-
-persist_recent_messages(FsmRef) ->
- ?GEN_FSM:sync_send_all_state_event(FsmRef, persist_recent_messages).
-%%%----------------------------------------------------------------------
-%%% Callback functions from gen_fsm
-%%%----------------------------------------------------------------------
-
-%%----------------------------------------------------------------------
-%% Func: init/1
-%% Returns: {ok, StateName, StateData} |
-%% {ok, StateName, StateData, Timeout} |
-%% ignore |
-%% {stop, StopReason}
-%%----------------------------------------------------------------------
-init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Creator, _Nick, DefRoomOpts, Mod]) ->
- process_flag(trap_exit, true),
- Shaper = shaper:new(RoomShaper),
- State = set_affiliation(Creator, owner,
- #state{host = Host,
- server_host = ServerHost,
- mod = Mod,
- access = Access,
- room = Room,
- history = lqueue_new(HistorySize),
- persist_history = PersistHistory,
- jid = jlib:make_jid(Room, Host, ""),
- just_created = true,
- room_shaper = Shaper}),
- State1 = set_opts(DefRoomOpts, State),
- %% this will trigger a write of the muc to disc if it is persistent.
- %% we need to do this because otherwise if muc are persistent by default,
- %% but never configured in any way by the client, we were never
- %% storing it on disc to be recreated on startup.
- if
- (State1#state.config)#config.persistent ->
- mod_muc:store_room(State1#state.host, State1#state.room, make_opts(State1));
- true ->
- ok
- end,
- ?INFO_MSG("Created MUC room ~s@~s by ~s",
- [Room, Host, jlib:jid_to_string(Creator)]),
- add_to_log(room_existence, created, State1),
- add_to_log(room_existence, started, State1),
- {ok, normal_state, State1};
-init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod]) ->
- process_flag(trap_exit, true),
- Shaper = shaper:new(RoomShaper),
- State = set_opts(Opts, #state{host = Host,
- server_host = ServerHost,
- mod = Mod,
- access = Access,
- room = Room,
- history = load_history(ServerHost, Room, PersistHistory, lqueue_new(HistorySize)),
- persist_history = PersistHistory,
- jid = jlib:make_jid(Room, Host, ""),
- room_shaper = Shaper}),
- add_to_log(room_existence, started, State),
- {ok, normal_state, State};
-init([StateName, #state{room = Room, host = Host} = StateData]) ->
- process_flag(trap_exit, true),
- mod_muc:register_room(Host, Room, self()),
- {ok, StateName, StateData}.
-
-%%----------------------------------------------------------------------
-%% Func: StateName/2
-%% Returns: {next_state, NextStateName, NextStateData} |
-%% {next_state, NextStateName, NextStateData, Timeout} |
-%% {stop, Reason, NewStateData}
-%%----------------------------------------------------------------------
-normal_state({route, From, "",
- {xmlelement, "message", Attrs, Els} = Packet},
- StateData) ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- 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" ->
- Activity = get_user_activity(From, StateData),
- Now = now_to_usec(now()),
- MinMessageInterval =
- trunc(gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod,
- min_message_interval, 0) * 1000000),
- Size = element_size(Packet),
- {MessageShaper, MessageShaperInterval} =
- shaper:update(Activity#activity.message_shaper, Size),
- if
- Activity#activity.message /= undefined ->
- ErrText = "Traffic rate limit is exceeded",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- {next_state, normal_state, StateData};
- Now >= Activity#activity.message_time + MinMessageInterval,
- MessageShaperInterval == 0 ->
- {RoomShaper, RoomShaperInterval} =
- shaper:update(StateData#state.room_shaper, Size),
- RoomQueueEmpty = queue:is_empty(
- StateData#state.room_queue),
- if
- RoomShaperInterval == 0,
- RoomQueueEmpty ->
- NewActivity = Activity#activity{
- message_time = Now,
- message_shaper = MessageShaper},
- StateData1 =
- store_user_activity(
- From, NewActivity, StateData),
- StateData2 =
- StateData1#state{
- room_shaper = RoomShaper},
- process_groupchat_message(From, Packet, StateData2);
- true ->
- StateData1 =
- if
- RoomQueueEmpty ->
- erlang:send_after(
- RoomShaperInterval, self(),
- process_room_queue),
- StateData#state{
- room_shaper = RoomShaper};
- true ->
- StateData
- end,
- NewActivity = Activity#activity{
- message_time = Now,
- message_shaper = MessageShaper,
- message = Packet},
- RoomQueue = queue:in(
- {message, From},
- StateData#state.room_queue),
- StateData2 =
- store_user_activity(
- From, NewActivity, StateData1),
- StateData3 =
- StateData2#state{
- room_queue = RoomQueue},
- {next_state, normal_state, StateData3}
- end;
- true ->
- MessageInterval =
- (Activity#activity.message_time +
- MinMessageInterval - Now) div 1000,
- Interval = lists:max([MessageInterval,
- MessageShaperInterval]),
- erlang:send_after(
- Interval, self(), {process_user_message, From}),
- NewActivity = Activity#activity{
- message = Packet,
- message_shaper = MessageShaper},
- StateData1 =
- store_user_activity(
- From, NewActivity, StateData),
- {next_state, normal_state, StateData1}
- end;
- "error" ->
- case is_user_online(From, StateData) of
- true ->
- ErrorText = "This participant is kicked from the room because "
- "he sent an error message",
- NewState = expulse_participant(Packet, From, StateData,
- translate:translate(Lang, ErrorText)),
- {next_state, normal_state, NewState};
- _ ->
- {next_state, normal_state, StateData}
- end;
- "chat" ->
- ErrText = "It is not allowed to send private messages to the conference",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- {next_state, normal_state, StateData};
- Type when (Type == "") or (Type == "normal") ->
- IsInvitation = is_invitation(Els),
- IsVoiceRequest = is_voice_request(Els)
- and is_visitor(From, StateData),
- IsVoiceApprovement = is_voice_approvement(Els)
- and not is_visitor(From, StateData),
- if IsInvitation ->
- case catch check_invitation(From, Els, Lang, StateData) of
- {error, Error} ->
- Err = jlib:make_error_reply(
- Packet, Error),
- route_stanza(
- StateData#state.jid,
- From, Err),
- {next_state, normal_state, StateData};
- IJID ->
- Config = StateData#state.config,
- case Config#config.members_only of
- true ->
- case get_affiliation(IJID, StateData) of
- none ->
- NSD = set_affiliation(
- IJID,
- member,
- StateData),
- case (NSD#state.config)#config.persistent of
- true ->
- (NSD#state.mod):store_room(
- NSD#state.server_host,
- NSD#state.host,
- NSD#state.room,
- make_opts(NSD));
- _ ->
- ok
- end,
- {next_state, normal_state, NSD};
- _ ->
- {next_state, normal_state,
- StateData}
- end;
- false ->
- {next_state, normal_state, StateData}
- end
- end;
- IsVoiceRequest ->
- NewStateData =
- case (StateData#state.config)#config.allow_voice_requests of
- true ->
- MinInterval = (StateData#state.config)
- #config.voice_request_min_interval,
- BareFrom = jlib:jid_remove_resource(
- jlib:jid_tolower(From)),
- NowPriority = -now_to_usec(now()),
- CleanPriority =
- NowPriority + MinInterval*1000000,
- Times = clean_treap(
- StateData#state.last_voice_request_time,
- CleanPriority),
- case treap:lookup(BareFrom, Times) of
- error ->
- Times1 = treap:insert(
- BareFrom,
- NowPriority,
- true, Times),
- NSD = StateData#state{
- last_voice_request_time =
- Times1},
- send_voice_request(From, NSD),
- NSD;
- {ok, _, _} ->
- ErrText = "Please, wait for "
- "a while before sending "
- "new voice request",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_NOT_ACCEPTABLE(
- Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- StateData#state{
- last_voice_request_time =
- Times}
- end;
- false ->
- ErrText = "Voice requests are "
- "disabled in this conference",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_FORBIDDEN(
- Lang, ErrText)),
- route_stanza(
- StateData#state.jid, From, Err),
- StateData
- end,
- {next_state, normal_state, NewStateData};
- IsVoiceApprovement ->
- NewStateData =
- case is_moderator(From, StateData) of
- true ->
- case extract_jid_from_voice_approvement(Els) of
- error ->
- ErrText = "Failed to extract "
- "JID from your voice "
- "request approval",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_BAD_REQUEST(
- Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- StateData;
- {ok, TargetJid} ->
- case is_visitor(
- TargetJid, StateData) of
- true ->
- Reason = [],
- NSD = set_role(
- TargetJid,
- participant,
- StateData),
- catch send_new_presence(
- TargetJid,
- Reason, NSD),
- NSD;
- _ ->
- StateData
- end
- end;
- _ ->
- ErrText = "Only moderators can "
- "approve voice requests",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_NOT_ALLOWED(
- Lang, ErrText)),
- route_stanza(
- StateData#state.jid, From, Err),
- StateData
- end,
- {next_state, normal_state, NewStateData};
- true ->
- {next_state, normal_state, StateData}
- end;
- _ ->
- ErrText = "Improper message type",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- {next_state, normal_state, StateData}
- end;
- _ ->
- case xml:get_attr_s("type", Attrs) of
- "error" ->
- ok;
- _ ->
- handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From)
- end,
- {next_state, normal_state, StateData}
- end;
-
-normal_state({route, From, "",
- {xmlelement, "iq", _Attrs, _Els} = Packet},
- StateData) ->
- case jlib:iq_query_info(Packet) of
- #iq{type = Type, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ when
- (XMLNS == ?NS_MUC_ADMIN) or
- (XMLNS == ?NS_MUC_OWNER) or
- (XMLNS == ?NS_DISCO_INFO) or
- (XMLNS == ?NS_DISCO_ITEMS) or
- (XMLNS == ?NS_CAPTCHA) ->
- Res1 = case XMLNS of
- ?NS_MUC_ADMIN ->
- process_iq_admin(From, Type, Lang, SubEl, StateData);
- ?NS_MUC_OWNER ->
- process_iq_owner(From, Type, Lang, SubEl, StateData);
- ?NS_DISCO_INFO ->
- process_iq_disco_info(From, Type, Lang, StateData);
- ?NS_DISCO_ITEMS ->
- process_iq_disco_items(From, Type, Lang, StateData);
- ?NS_CAPTCHA ->
- process_iq_captcha(From, Type, Lang, SubEl, StateData)
- end,
- {IQRes, NewStateData} =
- case Res1 of
- {result, Res, SD} ->
- {IQ#iq{type = result,
- sub_el = [{xmlelement, "query",
- [{"xmlns", XMLNS}],
- Res
- }]},
- SD};
- {error, Error} ->
- {IQ#iq{type = error,
- sub_el = [SubEl, Error]},
- StateData}
- end,
- route_stanza(StateData#state.jid,
- From,
- jlib:iq_to_xml(IQRes)),
- case NewStateData of
- stop ->
- {stop, normal, StateData};
- _ ->
- {next_state, normal_state, NewStateData}
- end;
- reply ->
- {next_state, normal_state, StateData};
- _ ->
- Err = jlib:make_error_reply(
- Packet, ?ERR_FEATURE_NOT_IMPLEMENTED),
- route_stanza(StateData#state.jid, From, Err),
- {next_state, normal_state, StateData}
- end;
-
-normal_state({route, From, Nick,
- {xmlelement, "presence", _Attrs, _Els} = Packet},
- StateData) ->
- Activity = get_user_activity(From, StateData),
- Now = now_to_usec(now()),
- MinPresenceInterval =
- trunc(gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, min_presence_interval, 0) * 1000000),
- if
- (Now >= Activity#activity.presence_time + MinPresenceInterval) and
- (Activity#activity.presence == undefined) ->
- NewActivity = Activity#activity{presence_time = Now},
- StateData1 = store_user_activity(From, NewActivity, StateData),
- process_presence(From, Nick, Packet, StateData1);
- true ->
- if
- Activity#activity.presence == undefined ->
- Interval = (Activity#activity.presence_time +
- MinPresenceInterval - Now) div 1000,
- erlang:send_after(
- Interval, self(), {process_user_presence, From});
- true ->
- ok
- end,
- NewActivity = Activity#activity{presence = {Nick, Packet}},
- StateData1 = store_user_activity(From, NewActivity, StateData),
- {next_state, normal_state, StateData1}
- end;
-
-normal_state({route, From, ToNick,
- {xmlelement, "message", Attrs, _} = Packet},
- StateData) ->
- Type = xml:get_attr_s("type", Attrs),
- Lang = xml:get_attr_s("xml:lang", Attrs),
- case decide_fate_message(Type, Packet, From, StateData) of
- {expulse_sender, Reason} ->
- ?DEBUG(Reason, []),
- ErrorText = "This participant is kicked from the room because "
- "he sent an error message to another participant",
- NewState = expulse_participant(Packet, From, StateData,
- translate:translate(Lang, ErrorText)),
- {next_state, normal_state, NewState};
- forget_message ->
- {next_state, normal_state, StateData};
- continue_delivery ->
- 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 "
- "messages of type \"groupchat\"",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- ToNick),
- From, Err);
- _ ->
- case find_jids_by_nick(ToNick, StateData) of
- false ->
- ErrText = "Recipient is not in the conference room",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- ToNick),
- From, Err);
- ToJIDs ->
- SrcIsVisitor = is_visitor(From, StateData),
- DstIsModerator = is_moderator(hd(ToJIDs), StateData),
- PmFromVisitors = (StateData#state.config)#config.allow_private_messages_from_visitors,
- if SrcIsVisitor == false;
- PmFromVisitors == anyone;
- (PmFromVisitors == moderators) and (DstIsModerator) ->
- {ok, #user{nick = FromNick}} =
- ?DICT:find(jlib:jid_tolower(From),
- StateData#state.users),
- FromNickJID = jlib:jid_replace_resource(StateData#state.jid, FromNick),
- [route_stanza(FromNickJID, ToJID, Packet) || ToJID <- ToJIDs];
- true ->
- ErrText = "It is not allowed to send private messages",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- ToNick),
- From, Err)
- end
- 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)),
- route_stanza(
- 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)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- ToNick),
- From, Err)
- end,
- {next_state, normal_state, StateData}
- end;
-
-normal_state({route, From, ToNick,
- {xmlelement, "iq", Attrs, _Els} = Packet},
- StateData) ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- StanzaId = xml:get_attr_s("id", Attrs),
- case {(StateData#state.config)#config.allow_query_users,
- is_user_online_iq(StanzaId, From, StateData)} of
- {true, {true, NewId, FromFull}} ->
- case find_jid_by_nick(ToNick, StateData) of
- false ->
- case jlib:iq_query_info(Packet) of
- reply ->
- ok;
- _ ->
- ErrText = "Recipient is not in the conference room",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid, ToNick),
- From, Err)
- end;
- ToJID ->
- {ok, #user{nick = FromNick}} =
- ?DICT:find(jlib:jid_tolower(FromFull),
- StateData#state.users),
- {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID,
- StanzaId, NewId,Packet),
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, FromNick),
- ToJID2, Packet2)
- end;
- {_, {false, _, _}} ->
- case jlib:iq_query_info(Packet) of
- reply ->
- ok;
- _ ->
- ErrText = "Only occupants are allowed to send queries to the conference",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, ToNick),
- From, Err)
- end;
- _ ->
- case jlib:iq_query_info(Packet) of
- reply ->
- ok;
- _ ->
- ErrText = "Queries to the conference members are not allowed in this room",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_ALLOWED(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, ToNick),
- From, Err)
- end
- end,
- {next_state, normal_state, StateData};
-
-normal_state(_Event, StateData) ->
- {next_state, normal_state, StateData}.
-
-
-
-%%----------------------------------------------------------------------
-%% Func: handle_event/3
-%% Returns: {next_state, NextStateName, NextStateData} |
-%% {next_state, NextStateName, NextStateData, Timeout} |
-%% {stop, Reason, NewStateData}
-%%----------------------------------------------------------------------
-handle_event({service_message, Msg}, _StateName, StateData) ->
- MessagePkt = {xmlelement, "message",
- [{"type", "groupchat"}],
- [{xmlelement, "body", [], [{xmlcdata, Msg}]}]},
- lists:foreach(
- fun({_LJID, Info}) ->
- route_stanza(
- StateData#state.jid,
- Info#user.jid,
- MessagePkt)
- end,
- ?DICT:to_list(StateData#state.users)),
- NSD = add_message_to_history("",
- StateData#state.jid,
- MessagePkt,
- StateData),
- {next_state, normal_state, NSD};
-
-handle_event({destroy, Reason}, _StateName, StateData) ->
- {result, [], stop} =
- destroy_room(
- {xmlelement, "destroy",
- [{"xmlns", ?NS_MUC_OWNER}],
- case Reason of
- none -> [];
- _Else ->
- [{xmlelement, "reason",
- [], [{xmlcdata, Reason}]}]
- end}, StateData),
- ?INFO_MSG("Destroyed MUC room ~s with reason: ~p",
- [jlib:jid_to_string(StateData#state.jid), Reason]),
- add_to_log(room_existence, destroyed, StateData),
- {stop, shutdown, StateData};
-handle_event(destroy, StateName, StateData) ->
- ?INFO_MSG("Destroyed MUC room ~s",
- [jlib:jid_to_string(StateData#state.jid)]),
- handle_event({destroy, none}, StateName, StateData);
-
-handle_event({set_affiliations, Affiliations}, StateName, StateData) ->
- {next_state, StateName, StateData#state{affiliations = Affiliations}};
-
-handle_event(_Event, StateName, StateData) ->
- {next_state, StateName, StateData}.
-
-%%----------------------------------------------------------------------
-%% Func: handle_sync_event/4
-%% Returns: {next_state, NextStateName, NextStateData} |
-%% {next_state, NextStateName, NextStateData, Timeout} |
-%% {reply, Reply, NextStateName, NextStateData} |
-%% {reply, Reply, NextStateName, NextStateData, Timeout} |
-%% {stop, Reason, NewStateData} |
-%% {stop, Reason, Reply, NewStateData}
-%%----------------------------------------------------------------------
-handle_sync_event({moderate_room_history, Nick}, _From, StateName, #state{history = History} = StateData) ->
- NewHistory = lqueue_filter(fun({FromNick, _TSPacket, _HaveSubject, _Timestamp, _Size}) ->
- FromNick /= Nick
- end, History),
- Moderated = History#lqueue.len - NewHistory#lqueue.len,
- {reply, {ok, integer_to_list(Moderated)}, StateName, StateData#state{history = NewHistory}};
-
-handle_sync_event(persist_recent_messages, _From, StateName, StateData) ->
- {reply, persist_muc_history(StateData), StateName, StateData};
-
-handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
- Reply = get_roomdesc_reply(JID, StateData,
- get_roomdesc_tail(StateData, Lang)),
- {reply, Reply, StateName, StateData};
-handle_sync_event(get_config, _From, StateName, StateData) ->
- {reply, {ok, StateData#state.config}, StateName, StateData};
-handle_sync_event(get_state, _From, StateName, StateData) ->
- {reply, {ok, StateData}, 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}.
-
-code_change(_OldVsn, StateName, StateData, _Extra) ->
- {ok, StateName, StateData}.
-
-print_state(StateData) ->
- StateData.
-
-%%----------------------------------------------------------------------
-%% Func: handle_info/3
-%% Returns: {next_state, NextStateName, NextStateData} |
-%% {next_state, NextStateName, NextStateData, Timeout} |
-%% {stop, Reason, NewStateData}
-%%----------------------------------------------------------------------
-handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
- 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) ->
- 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} ->
- Activity = get_user_activity(From, StateData),
- Packet = Activity#activity.message,
- NewActivity = Activity#activity{message = undefined},
- StateData1 =
- store_user_activity(
- From, NewActivity, StateData),
- StateData2 =
- StateData1#state{
- room_queue = RoomQueue},
- 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 =
- store_user_activity(
- From, NewActivity, StateData),
- StateData2 =
- StateData1#state{
- room_queue = RoomQueue},
- 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),
- route_stanza( % 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({migrate, Node}, StateName, StateData) ->
- if Node /= node() ->
- {migrate, StateData,
- {Node, ?MODULE, start, [StateName, StateData]}, 0};
- true ->
- {next_state, StateName, StateData}
- end;
-handle_info('shutdown', _StateName, StateData) ->
- {stop, 'shutdown', StateData};
-handle_info(_Info, StateName, StateData) ->
- {next_state, StateName, StateData}.
-
-%%----------------------------------------------------------------------
-%% Func: terminate/3
-%% Purpose: Shutdown the fsm
-%% Returns: any
-%%----------------------------------------------------------------------
-terminate({migrated, Clone}, _StateName, StateData) ->
- ?INFO_MSG("Migrating room ~s@~s to ~p on node ~p",
- [StateData#state.room, StateData#state.host,
- Clone, node(Clone)]),
- mod_muc:room_destroyed(StateData#state.host, StateData#state.room,
- self(), StateData#state.server_host),
- ok;
-terminate(Reason, _StateName, StateData) ->
- ?INFO_MSG("Stopping MUC room ~s@~s",
- [StateData#state.room, StateData#state.host]),
- ReasonT = case Reason of
- shutdown -> "You are being removed from the room because"
- " of a system shutdown";
- _ -> "Room terminates"
- end,
- ItemAttrs = [{"affiliation", "none"}, {"role", "none"}],
- ReasonEl = {xmlelement, "reason", [], [{xmlcdata, ReasonT}]},
- Packet = {xmlelement, "presence", [{"type", "unavailable"}],
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs, [ReasonEl]},
- {xmlelement, "status", [{"code", "332"}], []}
- ]}]},
- ?DICT:fold(
- fun(LJID, Info, _) ->
- Nick = Info#user.nick,
- case Reason of
- shutdown ->
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- Info#user.jid,
- Packet);
- _ -> ok
- end,
- tab_remove_online_user(LJID, StateData)
- end, [], StateData#state.users),
- add_to_log(room_existence, stopped, StateData),
- if
- Reason == 'shutdown' ->
- persist_muc_history(StateData);
- true ->
- ok
- end,
- (StateData#state.mod):room_destroyed(
- StateData#state.host, StateData#state.room, self(),
- StateData#state.server_host),
- ok.
-
-%%%----------------------------------------------------------------------
-%%% Internal functions
-%%%----------------------------------------------------------------------
-
-load_history(_Host, _Room, false, Queue) ->
- Queue;
-load_history(Host, Room, true, Queue) ->
- ?INFO_MSG("Loading history for room ~s on host ~s", [Room, Host]),
- case odbc_queries:load_roomhistory(Host, ejabberd_odbc:escape(Room)) of
- {selected, ["nick", "packet", "have_subject", "timestamp", "size"], Items} ->
- ?DEBUG("Found ~p messages on history for ~s", [length(Items), Room]),
- lists:foldl(fun(I, Q) ->
- {Nick, XML, HS, Ts, Size} = I,
- Item = {Nick,
- xml_stream:parse_element(XML),
- HS /= "0",
- calendar:gregorian_seconds_to_datetime(list_to_integer(Ts)),
- list_to_integer(Size)},
- lqueue_in(Item, Q)
- end, Queue, Items);
- _ ->
- Queue
- end.
-
-
-persist_muc_history(#state{room = Room, server_host = Server, config = #config{persistent = true} ,persist_history = true, history = Q}) ->
- ?INFO_MSG("Persisting history for room ~s on host ~s", [Room, Server]),
- Queries = lists:map(fun({FromNick, Packet, HaveSubject, Timestamp, Size}) ->
- odbc_queries:add_roomhistory_sql(
- ejabberd_odbc:escape(Room),
- ejabberd_odbc:escape(FromNick),
- ejabberd_odbc:escape(xml:element_to_binary(Packet)),
- atom_to_list(HaveSubject),
- integer_to_list(calendar:datetime_to_gregorian_seconds(Timestamp)),
- integer_to_list(Size))
- end, lqueue_to_list(Q)),
- odbc_queries:clear_and_add_roomhistory(Server,ejabberd_odbc:escape(Room), Queries),
- {ok, {persisted, length(Queries)}};
- %% en mod_muc, cuando se levantan los muc persistentes, si se crea, y el flag persist_history esta en true,
- %% se levantan los mensajes persistentes tb.
-
-persist_muc_history(_) ->
- {ok, not_persistent}.
-
-route(Pid, From, ToNick, Packet) ->
- ?GEN_FSM:send_event(Pid, {route, 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) orelse
- is_user_allowed_message_nonparticipant(From, StateData) of
- true ->
- {FromNick, Role} = get_participant_data(From, StateData),
- if
- (Role == moderator) or (Role == participant)
- or ((StateData#state.config)#config.moderated == false) ->
- {NewStateData1, IsAllowed} =
- case check_subject(Packet) of
- false ->
- {StateData, true};
- Subject ->
- case can_change_subject(Role,
- StateData) of
- true ->
- NSD =
- StateData#state{
- subject = Subject,
- subject_author =
- FromNick},
- case (NSD#state.config)#config.persistent of
- true ->
- (NSD#state.mod):store_room(
- NSD#state.server_host,
- NSD#state.host,
- NSD#state.room,
- make_opts(NSD));
- _ ->
- ok
- end,
- {NSD, true};
- _ ->
- {StateData, false}
- end
- end,
- case IsAllowed of
- true ->
- lists:foreach(
- fun({_LJID, Info}) ->
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- FromNick),
- Info#user.jid,
- Packet)
- end,
- ?DICT:to_list(StateData#state.users)),
- NewStateData2 =
- add_message_to_history(FromNick,
- From,
- Packet,
- NewStateData1),
- {next_state, normal_state, NewStateData2};
- _ ->
- Err =
- case (StateData#state.config)#config.allow_change_subj of
- true ->
- ?ERRT_FORBIDDEN(
- Lang,
- "Only moderators and participants "
- "are allowed to change the subject in this room");
- _ ->
- ?ERRT_FORBIDDEN(
- Lang,
- "Only moderators "
- "are allowed to change the subject in this room")
- end,
- route_stanza(
- StateData#state.jid,
- From,
- jlib:make_error_reply(Packet, Err)),
- {next_state, normal_state, StateData}
- end;
- true ->
- ErrText = "Visitors are not allowed to send messages to all occupants",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
- route_stanza(
- StateData#state.jid,
- From, Err),
- {next_state, normal_state, StateData}
- end;
- false ->
- ErrText = "Only occupants are allowed to send messages to the conference",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- route_stanza(StateData#state.jid, From, Err),
- {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.
-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),
- Lang = xml:get_attr_s("xml:lang", Attrs),
- StateData1 =
- case Type of
- "unavailable" ->
- case is_user_online(From, StateData) of
- true ->
- NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
- is_visitor(From, StateData)} of
- {false, true} ->
- strip_status(Packet);
- _ ->
- Packet
- end,
- NewState =
- add_user_presence_un(From, NewPacket, StateData),
- case ?DICT:find(Nick, StateData#state.nicks) of
- {ok, [_, _ | _]} -> ok;
- _ -> send_new_presence(From, NewState)
- end,
- Reason = case xml:get_subtag(NewPacket, "status") of
- false -> "";
- Status_el -> xml:get_tag_cdata(Status_el)
- end,
- remove_online_user(From, NewState, Reason);
- _ ->
- StateData
- end;
- "error" ->
- case is_user_online(From, StateData) of
- true ->
- ErrorText = "This participant is kicked from the room because "
- "he sent an error presence",
- expulse_participant(Packet, From, StateData,
- translate:translate(Lang, ErrorText));
- _ ->
- StateData
- end;
- "" ->
- case is_user_online(From, StateData) of
- true ->
- case is_nick_change(From, Nick, StateData) of
- true ->
- case {nick_collision(From, Nick, StateData),
- (StateData#state.mod):can_use_nick(
- StateData#state.server_host,
- 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)),
- route_stanza(
- % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid,
- Nick),
- From, Err),
- StateData;
- {true, _, _} ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- ErrText = "That nickname is already in use by another occupant",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_CONFLICT(Lang, ErrText)),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid,
- Nick), % TODO: s/Nick/""/
- From, Err),
- StateData;
- {_, false, _} ->
- ErrText = "That nickname is registered by another person",
- Err = jlib:make_error_reply(
- Packet,
- ?ERRT_CONFLICT(Lang, ErrText)),
- route_stanza(
- % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid,
- Nick),
- From, Err),
- StateData;
- _ ->
- change_nick(From, Nick, StateData)
- end;
- _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)
- end;
- _ ->
- StateData
- end,
- case (not (StateData1#state.config)#config.persistent) andalso
- (?DICT:to_list(StateData1#state.users) == []) of
- true ->
- ?INFO_MSG("Destroyed MUC room ~s because it's temporary and empty",
- [jlib:jid_to_string(StateData#state.jid)]),
- add_to_log(room_existence, destroyed, StateData),
- {stop, normal, StateData1};
- _ ->
- {next_state, normal_state, StateData1}
- end.
-
-is_user_online(JID, StateData) ->
- LJID = jlib:jid_tolower(JID),
- ?DICT:is_key(LJID, StateData#state.users).
-
-%% Check if the user is occupant of the room, or at least is an admin or owner.
-is_occupant_or_admin(JID, StateData) ->
- FAffiliation = get_affiliation(JID, StateData),
- FRole = get_role(JID, StateData),
- case (FRole /= none) orelse
- (FAffiliation == admin) orelse
- (FAffiliation == owner) of
- true ->
- true;
- _ ->
- false
- end.
-
-%%%
-%%% Handle IQ queries of vCard
-%%%
-is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource /= "" ->
- {is_user_online(JID, StateData), StanzaId, JID};
-is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == "" ->
- try stanzaid_unpack(StanzaId) of
- {OriginalId, Resource} ->
- JIDWithResource = jlib:jid_replace_resource(JID, Resource),
- {is_user_online(JIDWithResource, StateData),
- OriginalId, JIDWithResource}
- catch
- _:_ ->
- {is_user_online(JID, StateData), StanzaId, JID}
- end.
-
-handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) ->
- ToBareJID = jlib:jid_remove_resource(ToJID),
- IQ = jlib:iq_query_info(Packet),
- handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, NewId, IQ, Packet).
-handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, _NewId,
- #iq{type = get, xmlns = ?NS_VCARD}, Packet)
- when ToBareJID /= ToJID ->
- {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)};
-handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, _StanzaId, NewId, _IQ, Packet) ->
- {ToJID, change_stanzaid(NewId, Packet)}.
-
-stanzaid_pack(OriginalId, Resource) ->
- "berd"++base64:encode_to_string("ejab\0" ++ OriginalId ++ "\0" ++ Resource).
-stanzaid_unpack("berd"++StanzaIdBase64) ->
- StanzaId = base64:decode_to_string(StanzaIdBase64),
- ["ejab", OriginalId, Resource] = string:tokens(StanzaId, "\0"),
- {OriginalId, Resource}.
-
-change_stanzaid(NewId, Packet) ->
- {xmlelement, Name, Attrs, Els} = jlib:remove_attr("id", Packet),
- {xmlelement, Name, [{"id", NewId} | Attrs], Els}.
-change_stanzaid(PreviousId, ToJID, Packet) ->
- NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource),
- change_stanzaid(NewId, Packet).
-%%%
-%%%
-
-role_to_list(Role) ->
- case Role of
- moderator -> "moderator";
- participant -> "participant";
- visitor -> "visitor";
- none -> "none"
- end.
-
-affiliation_to_list(Affiliation) ->
- case Affiliation of
- owner -> "owner";
- admin -> "admin";
- member -> "member";
- outcast -> "outcast";
- none -> "none"
- end.
-
-list_to_role(Role) ->
- case Role of
- "moderator" -> moderator;
- "participant" -> participant;
- "visitor" -> visitor;
- "none" -> none
- end.
-
-list_to_affiliation(Affiliation) ->
- case Affiliation of
- "owner" -> owner;
- "admin" -> admin;
- "member" -> member;
- "outcast" -> outcast;
- "none" -> none
- end.
-
-%% Decide the fate of the message and its sender
-%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-decide_fate_message("error", Packet, From, StateData) ->
- %% Make a preliminary decision
- PD = case check_error_kick(Packet) of
- %% If this is an error stanza and its condition matches a criteria
- true ->
- Reason = io_lib:format("This participant is considered a ghost and is expulsed: ~s",
- [jlib:jid_to_string(From)]),
- {expulse_sender, Reason};
- false ->
- continue_delivery
- end,
- case PD of
- {expulse_sender, R} ->
- case is_user_online(From, StateData) of
- true ->
- {expulse_sender, R};
- false ->
- forget_message
- end;
- Other ->
- Other
- end;
-
-decide_fate_message(_, _, _, _) ->
- continue_delivery.
-
-%% Check if the elements of this error stanza indicate
-%% that the sender is a dead participant.
-%% If so, return true to kick the participant.
-check_error_kick(Packet) ->
- case get_error_condition(Packet) of
- "gone" -> true;
- "internal-server-error" -> true;
- "item-not-found" -> true;
- "jid-malformed" -> true;
- "recipient-unavailable" -> true;
- "redirect" -> true;
- "remote-server-not-found" -> true;
- "remote-server-timeout" -> true;
- "service-unavailable" -> true;
- _ -> false
- end.
-
-get_error_condition(Packet) ->
- case catch get_error_condition2(Packet) of
- {condition, ErrorCondition} ->
- ErrorCondition;
- {'EXIT', _} ->
- "badformed error stanza"
- end.
-get_error_condition2(Packet) ->
- {xmlelement, _, _, EEls} = xml:get_subtag(Packet, "error"),
- [Condition] = [Name || {xmlelement, Name, [{"xmlns", ?NS_STANZAS}], []} <- EEls],
- {condition, Condition}.
-
-expulse_participant(Packet, From, StateData, Reason1) ->
- ErrorCondition = get_error_condition(Packet),
- Reason2 = io_lib:format(Reason1 ++ ": " ++ "~s", [ErrorCondition]),
- NewState = add_user_presence_un(
- From,
- {xmlelement, "presence",
- [{"type", "unavailable"}],
- [{xmlelement, "status", [],
- [{xmlcdata, Reason2}]
- }]},
- StateData),
- send_new_presence(From, NewState),
- remove_online_user(From, NewState).
-
-
-set_affiliation(JID, Affiliation, StateData) ->
- set_affiliation(JID, Affiliation, StateData, "").
-
-set_affiliation(JID, Affiliation, StateData, Reason) ->
- LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)),
- Affiliations = case Affiliation of
- none ->
- ?DICT:erase(LJID,
- StateData#state.affiliations);
- _ ->
- ?DICT:store(LJID,
- {Affiliation, Reason},
- StateData#state.affiliations)
- end,
- StateData#state{affiliations = Affiliations}.
-
-get_affiliation(JID, StateData) ->
- {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = StateData#state.access,
- Res =
- case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of
- allow ->
- owner;
- _ ->
- LJID = jlib:jid_tolower(JID),
- case ?DICT:find(LJID, StateData#state.affiliations) of
- {ok, Affiliation} ->
- Affiliation;
- _ ->
- LJID1 = jlib:jid_remove_resource(LJID),
- case ?DICT:find(LJID1, StateData#state.affiliations) of
- {ok, Affiliation} ->
- Affiliation;
- _ ->
- LJID2 = setelement(1, LJID, ""),
- case ?DICT:find(LJID2, StateData#state.affiliations) of
- {ok, Affiliation} ->
- Affiliation;
- _ ->
- LJID3 = jlib:jid_remove_resource(LJID2),
- case ?DICT:find(LJID3, StateData#state.affiliations) of
- {ok, Affiliation} ->
- Affiliation;
- _ ->
- none
- end
- end
- end
- end
- end,
- case Res of
- {A, _Reason} ->
- A;
- _ ->
- Res
- end.
-
-get_service_affiliation(JID, StateData) ->
- {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} =
- StateData#state.access,
- case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of
- allow ->
- owner;
- _ ->
- none
- end.
-
-set_role(JID, Role, StateData) ->
- LJID = jlib:jid_tolower(JID),
- LJIDs = case LJID of
- {U, S, ""} ->
- ?DICT:fold(
- fun(J, _, Js) ->
- case J of
- {U, S, _} ->
- [J | Js];
- _ ->
- Js
- end
- end, [], StateData#state.users);
- _ ->
- case ?DICT:is_key(LJID, StateData#state.users) of
- true ->
- [LJID];
- _ ->
- []
- end
- end,
- {Users, Nicks}
- = case Role of
- none ->
- lists:foldl(fun(J, {Us, Ns}) ->
- NewNs =
- case ?DICT:find(J, Us) of
- {ok, #user{nick = Nick}} ->
- ?DICT:erase(Nick, Ns);
- _ ->
- Ns
- end,
- {?DICT:erase(J, Us), NewNs}
- end,
- {StateData#state.users, StateData#state.nicks},
- LJIDs);
- _ ->
- {lists:foldl(fun(J, Us) ->
- {ok, User} = ?DICT:find(J, Us),
- ?DICT:store(J,
- User#user{role = Role},
- Us)
- end, StateData#state.users, LJIDs),
- StateData#state.nicks}
- end,
- StateData#state{users = Users, nicks = Nicks}.
-
-get_role(JID, StateData) ->
- LJID = jlib:jid_tolower(JID),
- case ?DICT:find(LJID, StateData#state.users) of
- {ok, #user{role = Role}} ->
- Role;
- _ ->
- none
- end.
-
-get_default_role(Affiliation, StateData) ->
- case Affiliation of
- owner -> moderator;
- admin -> moderator;
- member -> participant;
- outcast -> none;
- none ->
- case (StateData#state.config)#config.members_only of
- true ->
- none;
- _ ->
- case (StateData#state.config)#config.members_by_default of
- true ->
- participant;
- _ ->
- visitor
- end
- end
- end.
-
-is_visitor(Jid, StateData) ->
- get_role(Jid, StateData) =:= visitor.
-
-is_moderator(Jid, StateData) ->
- get_role(Jid, StateData) =:= moderator.
-
-get_max_users(StateData) ->
- MaxUsers = (StateData#state.config)#config.max_users,
- ServiceMaxUsers = get_service_max_users(StateData),
- if
- MaxUsers =< ServiceMaxUsers -> MaxUsers;
- true -> ServiceMaxUsers
- end.
-
-get_service_max_users(StateData) ->
- gen_mod:get_module_opt(StateData#state.server_host,
- StateData#state.mod, max_users, ?MAX_USERS_DEFAULT).
-
-get_max_users_admin_threshold(StateData) ->
- gen_mod:get_module_opt(StateData#state.server_host,
- StateData#state.mod, max_users_admin_threshold, 5).
-
-get_user_activity(JID, StateData) ->
- case treap:lookup(jlib:jid_tolower(JID),
- StateData#state.activity) of
- {ok, _P, A} -> A;
- error ->
- MessageShaper =
- shaper:new(gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, user_message_shaper, none)),
- PresenceShaper =
- shaper:new(gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, user_presence_shaper, none)),
- #activity{message_shaper = MessageShaper,
- presence_shaper = PresenceShaper}
- end.
-
-store_user_activity(JID, UserActivity, StateData) ->
- MinMessageInterval =
- gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, min_message_interval, 0),
- MinPresenceInterval =
- gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, 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} ->
- Activity = get_user_activity(From, StateData),
- Packet = Activity#activity.message,
- Size = element_size(Packet),
- {RoomShaper, RoomShaperInterval} =
- shaper:update(StateData#state.room_shaper, Size),
- erlang:send_after(
- RoomShaperInterval, self(),
- process_room_queue),
- StateData#state{
- room_shaper = RoomShaper};
- {{value, {presence, From}}, _RoomQueue} ->
- Activity = get_user_activity(From, StateData),
- {_Nick, Packet} = Activity#activity.presence,
- Size = element_size(Packet),
- {RoomShaper, RoomShaperInterval} =
- shaper:update(StateData#state.room_shaper, Size),
- erlang:send_after(
- RoomShaperInterval, self(),
- process_room_queue),
- StateData#state{
- room_shaper = RoomShaper};
- {empty, _} ->
- StateData
- end.
-
-
-add_online_user(JID, Nick, Role, StateData) ->
- LJID = jlib:jid_tolower(JID),
- Users = ?DICT:store(LJID,
- #user{jid = JID,
- nick = Nick,
- role = Role},
- StateData#state.users),
- add_to_log(join, Nick, StateData),
- Nicks = ?DICT:update(Nick,
- fun(Entry) ->
- case lists:member(LJID, Entry) of
- true ->
- Entry;
- false ->
- [LJID|Entry]
- end
- end,
- [LJID],
- StateData#state.nicks),
- tab_add_online_user(JID, StateData),
- StateData#state{users = Users, nicks = Nicks}.
-
-remove_online_user(JID, StateData) ->
- remove_online_user(JID, StateData, "").
-
-remove_online_user(JID, StateData, Reason) ->
- LJID = jlib:jid_tolower(JID),
- {ok, #user{nick = Nick}} =
- ?DICT:find(LJID, StateData#state.users),
- add_to_log(leave, {Nick, Reason}, StateData),
- tab_remove_online_user(JID, StateData),
- Users = ?DICT:erase(LJID, StateData#state.users),
- Nicks = case ?DICT:find(Nick, StateData#state.nicks) of
- {ok, [LJID]} ->
- ?DICT:erase(Nick, StateData#state.nicks);
- {ok, U} ->
- ?DICT:store(Nick, U -- [LJID], StateData#state.nicks);
- error ->
- StateData#state.nicks
- end,
- StateData#state{users = Users, nicks = Nicks}.
-
-
-filter_presence({xmlelement, "presence", Attrs, Els}) ->
- FEls = lists:filter(
- fun(El) ->
- case El of
- {xmlcdata, _} ->
- false;
- {xmlelement, _Name1, Attrs1, _Els1} ->
- XMLNS = xml:get_attr_s("xmlns", Attrs1),
- case XMLNS of
- ?NS_MUC ++ _ ->
- false;
- _ ->
- true
- end
- end
- 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),
- FPresence = filter_presence(Presence),
- Users =
- ?DICT:update(
- LJID,
- fun(#user{} = User) ->
- User#user{last_presence = FPresence}
- end, StateData#state.users),
- StateData#state{users = Users}.
-
-add_user_presence_un(JID, Presence, StateData) ->
- LJID = jlib:jid_tolower(JID),
- FPresence = filter_presence(Presence),
- Users =
- ?DICT:update(
- LJID,
- fun(#user{} = User) ->
- User#user{last_presence = FPresence,
- role = none}
- end, StateData#state.users),
- StateData#state{users = Users}.
-
-
-%% Find and return a list of the full JIDs of the users of Nick.
-%% Return jid record.
-find_jids_by_nick(Nick, StateData) ->
- case ?DICT:find(Nick, StateData#state.nicks) of
- {ok, [User]} ->
- [jlib:make_jid(User)];
- {ok, Users} ->
- [jlib:make_jid(LJID) || LJID <- Users];
- error ->
- false
- end.
-
-%% Find and return the full JID of the user of Nick with
-%% highest-priority presence. Return jid record.
-find_jid_by_nick(Nick, StateData) ->
- case ?DICT:find(Nick, StateData#state.nicks) of
- {ok, [User]} ->
- jlib:make_jid(User);
- {ok, [FirstUser|Users]} ->
- #user{last_presence = FirstPresence} =
- ?DICT:fetch(FirstUser, StateData#state.users),
- {LJID, _} =
- lists:foldl(fun(Compare, {HighestUser, HighestPresence}) ->
- #user{last_presence = P1} =
- ?DICT:fetch(Compare, StateData#state.users),
- case higher_presence(P1, HighestPresence) of
- true ->
- {Compare, P1};
- false ->
- {HighestUser, HighestPresence}
- end
- end, {FirstUser, FirstPresence}, Users),
- jlib:make_jid(LJID);
- error ->
- false
- end.
-
-higher_presence(Pres1, Pres2) ->
- Pri1 = get_priority_from_presence(Pres1),
- Pri2 = get_priority_from_presence(Pres2),
- Pri1 > Pri2.
-
-get_priority_from_presence(PresencePacket) ->
- case xml:get_subtag(PresencePacket, "priority") of
- false ->
- 0;
- SubEl ->
- case catch list_to_integer(xml:get_tag_cdata(SubEl)) of
- P when is_integer(P) ->
- P;
- _ ->
- 0
- end
- end.
-
-find_nick_by_jid(Jid, StateData) ->
- [{_, #user{nick = Nick}}] = lists:filter(
- fun({_, #user{jid = FJid}}) -> FJid == Jid end,
- ?DICT:to_list(StateData#state.users)),
- Nick.
-
-is_nick_change(JID, Nick, StateData) ->
- LJID = jlib:jid_tolower(JID),
- case Nick of
- "" ->
- false;
- _ ->
- {ok, #user{nick = OldNick}} =
- ?DICT:find(LJID, StateData#state.users),
- Nick /= OldNick
- end.
-
-nick_collision(User, Nick, StateData) ->
- UserOfNick = find_jid_by_nick(Nick, StateData),
- %% if nick is not used, or is used by another resource of the same
- %% user, it's ok.
- UserOfNick /= false andalso
- jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick)) /=
- jlib:jid_remove_resource(jlib:jid_tolower(User)).
-
-add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
- Lang = xml:get_attr_s("xml:lang", Attrs),
- MaxUsers = get_max_users(StateData),
- MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData),
- NUsers = dict:fold(fun(_, _, Acc) -> Acc + 1 end, 0,
- StateData#state.users),
- Affiliation = get_affiliation(From, StateData),
- ServiceAffiliation = get_service_affiliation(From, StateData),
- NConferences = tab_count_user(From),
- MaxConferences = gen_mod:get_module_opt(
- StateData#state.server_host,
- StateData#state.mod, max_user_conferences, 10),
- Collision = nick_collision(From, Nick, StateData),
- case {(ServiceAffiliation == owner orelse
- ((Affiliation == admin orelse Affiliation == owner) andalso
- NUsers < MaxAdminUsers) orelse
- NUsers < MaxUsers) andalso
- NConferences < MaxConferences,
- Collision,
- (StateData#state.mod):can_use_nick(StateData#state.server_host,
- StateData#state.host, From, Nick),
- get_default_role(Affiliation, StateData)} of
- {false, _, _, _} ->
- % max user reached and user is not admin or owner
- Err = jlib:make_error_reply(
- Packet,
- ?ERR_SERVICE_UNAVAILABLE),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
- {_, _, _, none} ->
- Err = jlib:make_error_reply(
- Packet,
- case Affiliation of
- outcast ->
- ErrText = "You have been banned from this room",
- ?ERRT_FORBIDDEN(Lang, ErrText);
- _ ->
- ErrText = "Membership is required to enter this room",
- ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText)
- end),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
- {_, true, _, _} ->
- ErrText = "That nickname is already in use by another occupant",
- Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
- route_stanza(
- % TODO: s/Nick/""/
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
- {_, _, false, _} ->
- ErrText = "That nickname is registered by another person",
- Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
- route_stanza(
- % TODO: s/Nick/""/
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
- {_, _, _, Role} ->
- case check_password(ServiceAffiliation, Affiliation,
- Els, From, StateData) of
- true ->
- NewState =
- add_user_presence(
- From, Packet,
- add_online_user(From, Nick, Role, StateData)),
- if not (NewState#state.config)#config.anonymous ->
- WPacket = {xmlelement, "message", [{"type", "groupchat"}],
- [{xmlelement, "body", [],
- [{xmlcdata, translate:translate(
- Lang,
- "This room is not anonymous")}]},
- {xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "status", [{"code", "100"}], []}]}]},
- route_stanza(
- StateData#state.jid,
- From, WPacket);
- true ->
- ok
- end,
- send_existing_presences(From, NewState),
- send_new_presence(From, NewState),
- Shift = count_stanza_shift(Nick, Els, NewState),
- case send_history(From, Shift, NewState) of
- true ->
- ok;
- _ ->
- send_subject(From, Lang, StateData)
- end,
- case NewState#state.just_created of
- true ->
- NewState#state{just_created = false};
- false ->
- Robots = ?DICT:erase(From, StateData#state.robots),
- NewState#state{robots = Robots}
- end;
- nopass ->
- ErrText = "A password is required to enter this room",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid, Nick),
- From, Err),
- StateData;
- captcha_required ->
- SID = xml:get_attr_s("id", Attrs),
- RoomJID = StateData#state.jid,
- To = jlib:jid_replace_resource(RoomJID, Nick),
- Limiter = {From#jid.luser, From#jid.lserver},
- case ejabberd_captcha:create_captcha(
- SID, RoomJID, To, Lang, Limiter, From) of
- {ok, ID, CaptchaEls} ->
- MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
- Robots = ?DICT:store(From,
- {Nick, Packet}, StateData#state.robots),
- route_stanza(RoomJID, From, MsgPkt),
- StateData#state{robots = Robots};
- {error, limit} ->
- ErrText = "Too many CAPTCHA requests",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid, Nick),
- From, Err),
- StateData;
- _ ->
- ErrText = "Unable to generate a CAPTCHA",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid, Nick),
- From, Err),
- StateData
- end;
- _ ->
- ErrText = "Incorrect password",
- Err = jlib:make_error_reply(
- Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
- route_stanza( % TODO: s/Nick/""/
- jlib:jid_replace_resource(
- StateData#state.jid, Nick),
- From, Err),
- StateData
- end
- end.
-
-check_password(owner, _Affiliation, _Els, _From, _StateData) ->
- %% Don't check pass if user is owner in MUC service (access_admin option)
- true;
-check_password(_ServiceAffiliation, Affiliation, Els, From, StateData) ->
- case (StateData#state.config)#config.password_protected of
- false ->
- check_captcha(Affiliation, From, StateData);
- true ->
- Pass = extract_password(Els),
- case Pass of
- false ->
- nopass;
- _ ->
- case (StateData#state.config)#config.password of
- Pass ->
- true;
- _ ->
- 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;
- _ ->
- WList = (StateData#state.config)#config.captcha_whitelist,
- #jid{luser = U, lserver = S, lresource = R} = From,
- case ?SETS:is_element({U, S, R}, WList) of
- true ->
- true;
- false ->
- case ?SETS:is_element({U, S, ""}, WList) of
- true ->
- true;
- false ->
- case ?SETS:is_element({"", S, ""}, WList) of
- true ->
- true;
- false ->
- captcha_required
- end
- end
- end
- end;
- _ ->
- true
- end.
-
-extract_password([]) ->
- false;
-extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) ->
- case xml:get_attr_s("xmlns", Attrs) of
- ?NS_MUC ->
- case xml:get_subtag(El, "password") of
- false ->
- false;
- SubEl ->
- xml:get_tag_cdata(SubEl)
- end;
- _ ->
- extract_password(Els)
- end;
-extract_password([_ | Els]) ->
- extract_password(Els).
-
-count_stanza_shift(Nick, Els, StateData) ->
- HL = lqueue_to_list(StateData#state.history),
- Since = extract_history(Els, "since"),
- Shift0 = case Since of
- false ->
- 0;
- _ ->
- Sin = calendar:datetime_to_gregorian_seconds(Since),
- count_seconds_shift(Sin, HL)
- end,
- Seconds = extract_history(Els, "seconds"),
- Shift1 = case Seconds of
- false ->
- 0;
- _ ->
- Sec = calendar:datetime_to_gregorian_seconds(
- calendar:now_to_universal_time(now())) - Seconds,
- count_seconds_shift(Sec, HL)
- end,
- MaxStanzas = extract_history(Els, "maxstanzas"),
- Shift2 = case MaxStanzas of
- false ->
- 0;
- _ ->
- count_maxstanzas_shift(MaxStanzas, HL)
- end,
- MaxChars = extract_history(Els, "maxchars"),
- Shift3 = case MaxChars of
- false ->
- 0;
- _ ->
- count_maxchars_shift(Nick, MaxChars, HL)
- end,
- lists:max([Shift0, Shift1, Shift2, Shift3]).
-
-count_seconds_shift(Seconds, HistoryList) ->
- lists:sum(
- lists:map(
- fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) ->
- T = calendar:datetime_to_gregorian_seconds(TimeStamp),
- if
- T < Seconds ->
- 1;
- true ->
- 0
- end
- end, HistoryList)).
-
-count_maxstanzas_shift(MaxStanzas, HistoryList) ->
- S = length(HistoryList) - MaxStanzas,
- if
- S =< 0 ->
- 0;
- true ->
- S
- end.
-
-count_maxchars_shift(Nick, MaxSize, HistoryList) ->
- NLen = string:len(Nick) + 1,
- Sizes = lists:map(
- fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) ->
- Size + NLen
- end, HistoryList),
- calc_shift(MaxSize, Sizes).
-
-calc_shift(MaxSize, Sizes) ->
- Total = lists:sum(Sizes),
- calc_shift(MaxSize, Total, 0, Sizes).
-
-calc_shift(_MaxSize, _Size, Shift, []) ->
- Shift;
-calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
- if
- MaxSize >= Size ->
- Shift;
- true ->
- calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
- end.
-
-extract_history([], _Type) ->
- false;
-extract_history([{xmlelement, _Name, Attrs, _SubEls} = El | Els], Type) ->
- case xml:get_attr_s("xmlns", Attrs) of
- ?NS_MUC ->
- AttrVal = xml:get_path_s(El,
- [{elem, "history"}, {attr, Type}]),
- case Type of
- "since" ->
- case jlib:datetime_string_to_timestamp(AttrVal) of
- undefined ->
- false;
- TS ->
- calendar:now_to_universal_time(TS)
- end;
- _ ->
- case catch list_to_integer(AttrVal) of
- IntVal when is_integer(IntVal) and (IntVal >= 0) ->
- IntVal;
- _ ->
- false
- end
- end;
- _ ->
- extract_history(Els, Type)
- end;
-extract_history([_ | Els], Type) ->
- 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, ""} ->
- ?DICT:fold(
- fun(J, _, Js) ->
- case J of
- {U, S, _} ->
- [J | Js];
- _ ->
- Js
- end
- end, [], StateData#state.users);
- _ ->
- case ?DICT:is_key(LJID, StateData#state.users) of
- true ->
- [LJID];
- _ ->
- []
- end
- end,
- lists:foreach(fun(J) ->
- send_new_presence(J, Reason, StateData)
- end, LJIDs).
-
-send_new_presence(NJID, StateData) ->
- send_new_presence(NJID, "", StateData).
-
-send_new_presence(NJID, Reason, StateData) ->
- %% First, find the nick associated with this JID.
- #user{nick = Nick} = ?DICT:fetch(jlib:jid_tolower(NJID), StateData#state.users),
- %% Then find the JID using this nick with highest priority.
- LJID = find_jid_by_nick(Nick, StateData),
- %% Then we get the presence data we're supposed to send.
- {ok, #user{jid = RealJID,
- role = Role,
- last_presence = Presence}} =
- ?DICT:find(jlib:jid_tolower(LJID), StateData#state.users),
- Affiliation = get_affiliation(LJID, StateData),
- SAffiliation = affiliation_to_list(Affiliation),
- SRole = role_to_list(Role),
- lists:foreach(
- fun({_LJID, Info}) ->
- ItemAttrs =
- case (Info#user.role == moderator) orelse
- ((StateData#state.config)#config.anonymous == false) of
- true ->
- [{"jid", jlib:jid_to_string(RealJID)},
- {"affiliation", SAffiliation},
- {"role", SRole}];
- _ ->
- [{"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,
- Status2 = case ((StateData#state.config)#config.anonymous==false)
- andalso (NJID == Info#user.jid) of
- true ->
- [{xmlelement, "status", [{"code", "100"}], []}
- | Status];
- false ->
- Status
- end,
- Status3 = case NJID == Info#user.jid of
- true ->
- [{xmlelement, "status", [{"code", "110"}], []}
- | Status2];
- false ->
- Status2
- end,
- Packet = xml:append_subtags(
- Presence,
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs, ItemEls} | Status3]}]),
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- Info#user.jid,
- Packet)
- end, ?DICT:to_list(StateData#state.users)).
-
-
-send_existing_presences(ToJID, StateData) ->
- LToJID = jlib:jid_tolower(ToJID),
- {ok, #user{jid = RealToJID,
- role = Role}} =
- ?DICT:find(LToJID, StateData#state.users),
- lists:foreach(
- fun({FromNick, _Users}) ->
- LJID = find_jid_by_nick(FromNick, StateData),
- #user{jid = FromJID,
- role = FromRole,
- last_presence = Presence
- } = ?DICT:fetch(jlib:jid_tolower(LJID), StateData#state.users),
- case RealToJID of
- FromJID ->
- ok;
- _ ->
- FromAffiliation = get_affiliation(LJID, StateData),
- ItemAttrs =
- case (Role == moderator) orelse
- ((StateData#state.config)#config.anonymous ==
- false) of
- true ->
- [{"jid", jlib:jid_to_string(FromJID)},
- {"affiliation",
- affiliation_to_list(FromAffiliation)},
- {"role", role_to_list(FromRole)}];
- _ ->
- [{"affiliation",
- affiliation_to_list(FromAffiliation)},
- {"role", role_to_list(FromRole)}]
- end,
- Packet = xml:append_subtags(
- Presence,
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs, []}]}]),
- route_stanza(
- jlib:jid_replace_resource(
- StateData#state.jid, FromNick),
- RealToJID,
- Packet)
- end
- end, ?DICT:to_list(StateData#state.nicks)).
-
-
-now_to_usec({MSec, Sec, USec}) ->
- (MSec*1000000 + Sec)*1000000 + USec.
-
-
-change_nick(JID, Nick, StateData) ->
- LJID = jlib:jid_tolower(JID),
- {ok, #user{nick = OldNick}} =
- ?DICT:find(LJID, StateData#state.users),
- Users =
- ?DICT:update(
- LJID,
- fun(#user{} = User) ->
- User#user{nick = Nick}
- end, StateData#state.users),
- OldNickUsers = ?DICT:fetch(OldNick, StateData#state.nicks),
- NewNickUsers = case ?DICT:find(Nick, StateData#state.nicks) of
- {ok, U} -> U;
- error -> []
- end,
- %% Send unavailable presence from the old nick if it's no longer
- %% used.
- SendOldUnavailable = length(OldNickUsers) == 1,
- %% If we send unavailable presence from the old nick, we should
- %% probably send presence from the new nick, in order not to
- %% confuse clients. Otherwise, do it only if the new nick was
- %% unused.
- SendNewAvailable = SendOldUnavailable orelse
- NewNickUsers == [],
- Nicks =
- case OldNickUsers of
- [LJID] ->
- ?DICT:store(Nick, [LJID|NewNickUsers],
- ?DICT:erase(OldNick, StateData#state.nicks));
- [_|_] ->
- ?DICT:store(Nick, [LJID|NewNickUsers],
- ?DICT:store(OldNick, OldNickUsers -- [LJID],
- StateData#state.nicks))
- end,
- NewStateData = StateData#state{users = Users, nicks = Nicks},
- send_nick_changing(JID, OldNick, NewStateData, SendOldUnavailable, SendNewAvailable),
- add_to_log(nickchange, {OldNick, Nick}, StateData),
- NewStateData.
-
-send_nick_changing(JID, OldNick, StateData,
- SendOldUnavailable, SendNewAvailable) ->
- {ok, #user{jid = RealJID,
- nick = Nick,
- role = Role,
- last_presence = Presence}} =
- ?DICT:find(jlib:jid_tolower(JID), StateData#state.users),
- Affiliation = get_affiliation(JID, StateData),
- SAffiliation = affiliation_to_list(Affiliation),
- SRole = role_to_list(Role),
- lists:foreach(
- fun({_LJID, Info}) ->
- ItemAttrs1 =
- case (Info#user.role == moderator) orelse
- ((StateData#state.config)#config.anonymous == false) of
- true ->
- [{"jid", jlib:jid_to_string(RealJID)},
- {"affiliation", SAffiliation},
- {"role", SRole},
- {"nick", Nick}];
- _ ->
- [{"affiliation", SAffiliation},
- {"role", SRole},
- {"nick", Nick}]
- end,
- ItemAttrs2 =
- case (Info#user.role == moderator) orelse
- ((StateData#state.config)#config.anonymous == false) of
- true ->
- [{"jid", jlib:jid_to_string(RealJID)},
- {"affiliation", SAffiliation},
- {"role", SRole}];
- _ ->
- [{"affiliation", SAffiliation},
- {"role", SRole}]
- end,
- Packet1 =
- {xmlelement, "presence", [{"type", "unavailable"}],
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs1, []},
- {xmlelement, "status", [{"code", "303"}], []}]}]},
- Packet2 = xml:append_subtags(
- Presence,
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs2, []}]}]),
- if SendOldUnavailable ->
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, OldNick),
- Info#user.jid,
- Packet1);
- true ->
- ok
- end,
- if SendNewAvailable ->
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- Info#user.jid,
- Packet2);
- true ->
- ok
- end
- end, ?DICT:to_list(StateData#state.users)).
-
-
-lqueue_new(Max) ->
- #lqueue{queue = queue:new(),
- len = 0,
- max = Max}.
-
-%% If the message queue limit is set to 0, do not store messages.
-lqueue_in(_Item, LQ = #lqueue{max = 0}) ->
- LQ;
-%% Otherwise, rotate messages in the queue store.
-lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) ->
- Q2 = queue:in(Item, Q1),
- if
- Len >= Max ->
- Q3 = lqueue_cut(Q2, Len - Max + 1),
- #lqueue{queue = Q3, len = Max, max = Max};
- true ->
- #lqueue{queue = Q2, len = Len + 1, max = Max}
- end.
-
-lqueue_cut(Q, 0) ->
- Q;
-lqueue_cut(Q, N) ->
- {_, Q1} = queue:out(Q),
- lqueue_cut(Q1, N - 1).
-
-lqueue_to_list(#lqueue{queue = Q1}) ->
- queue:to_list(Q1).
-
-lqueue_filter(F, #lqueue{queue = Q1} = LQ) ->
- Q2 = queue:filter(F, Q1),
- LQ#lqueue{queue = Q2, len = queue:len(Q2)}.
-
-add_message_to_history(FromNick, FromJID, Packet, StateData) ->
- HaveSubject = case xml:get_subtag(Packet, "subject") of
- false ->
- false;
- _ ->
- true
- end,
- TimeStamp = calendar:now_to_universal_time(now()),
- %% 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,
- TSPacket),
- Size = element_size(SPacket),
- Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size},
- StateData#state.history),
- add_to_log(text, {FromNick, Packet}, StateData),
- StateData#state{history = Q1}.
-
-send_history(JID, Shift, StateData) ->
- lists:foldl(
- fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) ->
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- JID,
- Packet),
- B or HaveSubject
- end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))).
-
-
-send_subject(JID, Lang, StateData) ->
- case StateData#state.subject_author of
- "" ->
- ok;
- Nick ->
- Subject = StateData#state.subject,
- Packet = {xmlelement, "message", [{"type", "groupchat"}],
- [{xmlelement, "subject", [], [{xmlcdata, Subject}]},
- {xmlelement, "body", [],
- [{xmlcdata,
- Nick ++
- translate:translate(Lang,
- " has set the subject to: ") ++
- Subject}]}]},
- route_stanza(
- StateData#state.jid,
- JID,
- Packet)
- end.
-
-check_subject(Packet) ->
- case xml:get_subtag(Packet, "subject") of
- false ->
- false;
- SubjEl ->
- xml:get_tag_cdata(SubjEl)
- end.
-
-can_change_subject(Role, StateData) ->
- case (StateData#state.config)#config.allow_change_subj of
- true ->
- (Role == moderator) orelse (Role == participant);
- _ ->
- Role == moderator
- end.
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Admin stuff
-
-process_iq_admin(From, set, Lang, SubEl, StateData) ->
- {xmlelement, _, _, Items} = SubEl,
- process_admin_items_set(From, Items, Lang, StateData);
-
-process_iq_admin(From, get, Lang, SubEl, StateData) ->
- case xml:get_subtag(SubEl, "item") of
- false ->
- {error, ?ERR_BAD_REQUEST};
- Item ->
- FAffiliation = get_affiliation(From, StateData),
- FRole = get_role(From, StateData),
- case xml:get_tag_attr("role", Item) of
- false ->
- case xml:get_tag_attr("affiliation", Item) of
- false ->
- {error, ?ERR_BAD_REQUEST};
- {value, StrAffiliation} ->
- case catch list_to_affiliation(StrAffiliation) of
- {'EXIT', _} ->
- {error, ?ERR_BAD_REQUEST};
- SAffiliation ->
- if
- (FAffiliation == owner) or
- (FAffiliation == admin) ->
- Items = items_with_affiliation(
- SAffiliation, StateData),
- {result, Items, StateData};
- true ->
- ErrText = "Administrator privileges required",
- {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
- end
- end
- end;
- {value, StrRole} ->
- case catch list_to_role(StrRole) of
- {'EXIT', _} ->
- {error, ?ERR_BAD_REQUEST};
- SRole ->
- if
- FRole == moderator ->
- Items = items_with_role(SRole, StateData),
- {result, Items, StateData};
- true ->
- ErrText = "Moderator privileges required",
- {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
- end
- end
- end
- end.
-
-
-items_with_role(SRole, StateData) ->
- lists:map(
- fun({_, U}) ->
- user_to_item(U, StateData)
- end, search_role(SRole, StateData)).
-
-items_with_affiliation(SAffiliation, StateData) ->
- lists:map(
- fun({JID, {Affiliation, Reason}}) ->
- {xmlelement, "item",
- [{"affiliation", affiliation_to_list(Affiliation)},
- {"jid", jlib:jid_to_string(JID)}],
- [{xmlelement, "reason", [], [{xmlcdata, Reason}]}]};
- ({JID, Affiliation}) ->
- {xmlelement, "item",
- [{"affiliation", affiliation_to_list(Affiliation)},
- {"jid", jlib:jid_to_string(JID)}],
- []}
- end, search_affiliation(SAffiliation, StateData)).
-
-user_to_item(#user{role = Role,
- nick = Nick,
- jid = JID
- }, StateData) ->
- Affiliation = get_affiliation(JID, StateData),
- {xmlelement, "item",
- [{"role", role_to_list(Role)},
- {"affiliation", affiliation_to_list(Affiliation)},
- {"nick", Nick},
- {"jid", jlib:jid_to_string(JID)}],
- []}.
-
-search_role(Role, StateData) ->
- lists:filter(
- fun({_, #user{role = R}}) ->
- Role == R
- end, ?DICT:to_list(StateData#state.users)).
-
-search_affiliation(Affiliation, StateData) ->
- lists:filter(
- fun({_, A}) ->
- case A of
- {A1, _Reason} ->
- Affiliation == A1;
- _ ->
- Affiliation == A
- end
- end, ?DICT:to_list(StateData#state.affiliations)).
-
-
-process_admin_items_set(UJID, Items, Lang, StateData) ->
- UAffiliation = get_affiliation(UJID, StateData),
- URole = get_role(UJID, StateData),
- case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of
- {result, Res} ->
- ?INFO_MSG("Processing MUC admin query from ~s in room ~s:~n ~p",
- [jlib:jid_to_string(UJID), jlib:jid_to_string(StateData#state.jid), Res]),
- NSD =
- lists:foldl(
- fun(E, SD) ->
- case catch (
- case E of
- {JID, affiliation, owner, _}
- when (JID#jid.luser == "") ->
- %% If the provided JID does not have username,
- %% forget the affiliation completely
- SD;
- {JID, role, none, Reason} ->
- catch send_kickban_presence(
- JID, Reason, "307", SD),
- set_role(JID, none, SD);
- {JID, affiliation, none, Reason} ->
- case (SD#state.config)#config.members_only of
- true ->
- catch send_kickban_presence(
- JID, Reason, "321", none, SD),
- SD1 = set_affiliation(JID, none, SD),
- set_role(JID, none, SD1);
- _ ->
- SD1 = set_affiliation(JID, none, SD),
- send_update_presence(JID, SD1),
- SD1
- end;
- {JID, affiliation, outcast, Reason} ->
- catch send_kickban_presence(
- JID, Reason, "301", outcast, SD),
- set_affiliation(
- JID, outcast,
- set_role(JID, none, SD), Reason);
- {JID, affiliation, A, Reason} when
- (A == admin) or (A == owner) ->
- SD1 = set_affiliation(JID, A, SD, Reason),
- SD2 = set_role(JID, moderator, SD1),
- send_update_presence(JID, Reason, SD2),
- SD2;
- {JID, affiliation, member, Reason} ->
- SD1 = set_affiliation(
- JID, member, SD, Reason),
- SD2 = set_role(JID, participant, SD1),
- send_update_presence(JID, Reason, SD2),
- SD2;
- {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),
- send_update_presence(JID, SD1),
- SD1
- end
- ) of
- {'EXIT', ErrReason} ->
- ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n",
- [ErrReason]),
- SD;
- NSD ->
- NSD
- end
- end, StateData, lists:flatten(Res)),
- case (NSD#state.config)#config.persistent of
- true ->
- (NSD#state.mod):store_room(NSD#state.server_host,
- NSD#state.host, NSD#state.room,
- make_opts(NSD));
- _ ->
- ok
- end,
- {result, [], NSD};
- Err ->
- Err
- end.
-
-
-find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) ->
- {result, Res};
-find_changed_items(UJID, UAffiliation, URole, [{xmlcdata, _} | Items],
- Lang, StateData, Res) ->
- find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res);
-find_changed_items(UJID, UAffiliation, URole,
- [{xmlelement, "item", Attrs, _Els} = Item | Items],
- Lang, StateData, Res) ->
- TJID = case xml:get_attr("jid", Attrs) of
- {value, S} ->
- case jlib:string_to_jid(S) of
- error ->
- ErrText = io_lib:format(
- translate:translate(
- Lang,
- "Jabber ID ~s is invalid"), [S]),
- {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
- J ->
- {value, [J]}
- end;
- _ ->
- case xml:get_attr("nick", Attrs) of
- {value, N} ->
- case find_jids_by_nick(N, StateData) of
- false ->
- ErrText =
- io_lib:format(
- translate:translate(
- Lang,
- "Nickname ~s does not exist in the room"),
- [N]),
- {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
- J ->
- {value, J}
- end;
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end
- end,
- case TJID of
- {value, [JID|_]=JIDs} ->
- TAffiliation = get_affiliation(JID, StateData),
- TRole = get_role(JID, StateData),
- case xml:get_attr("role", Attrs) of
- false ->
- case xml:get_attr("affiliation", Attrs) of
- false ->
- {error, ?ERR_BAD_REQUEST};
- {value, StrAffiliation} ->
- case catch list_to_affiliation(StrAffiliation) of
- {'EXIT', _} ->
- ErrText1 =
- io_lib:format(
- translate:translate(
- Lang,
- "Invalid affiliation: ~s"),
- [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,
- ServiceAf) of
- nothing ->
- nothing;
- true ->
- true;
- check_owner ->
- case search_affiliation(
- owner, StateData) of
- [{OJID, _}] ->
- jlib:jid_remove_resource(OJID) /=
- jlib:jid_tolower(jlib:jid_remove_resource(UJID));
- _ ->
- true
- end;
- _ ->
- false
- end,
- case CanChangeRA of
- nothing ->
- find_changed_items(
- UJID,
- UAffiliation, URole,
- Items, Lang, StateData,
- Res);
- true ->
- Reason = xml:get_path_s(Item, [{elem, "reason"}, cdata]),
- MoreRes = [{jlib:jid_remove_resource(Jidx), affiliation, SAffiliation, Reason} || Jidx <- JIDs],
- find_changed_items(
- UJID,
- UAffiliation, URole,
- Items, Lang, StateData,
- [MoreRes | Res]);
- false ->
- {error, ?ERR_NOT_ALLOWED}
- end
- end
- end;
- {value, StrRole} ->
- case catch list_to_role(StrRole) of
- {'EXIT', _} ->
- ErrText1 =
- io_lib:format(
- translate:translate(
- Lang,
- "Invalid role: ~s"),
- [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,
- ServiceAf) of
- nothing ->
- nothing;
- true ->
- true;
- check_owner ->
- case search_affiliation(
- owner, StateData) of
- [{OJID, _}] ->
- jlib:jid_remove_resource(OJID) /=
- jlib:jid_tolower(jlib:jid_remove_resource(UJID));
- _ ->
- true
- end;
- _ ->
- false
- end,
- case CanChangeRA of
- nothing ->
- find_changed_items(
- UJID,
- UAffiliation, URole,
- Items, Lang, StateData,
- Res);
- true ->
- Reason = xml:get_path_s(Item, [{elem, "reason"}, cdata]),
- MoreRes = [{Jidx, role, SRole, Reason} || Jidx <- JIDs],
- find_changed_items(
- UJID,
- UAffiliation, URole,
- Items, Lang, StateData,
- [MoreRes | Res]);
- _ ->
- {error, ?ERR_NOT_ALLOWED}
- end
- end
- end;
- Err ->
- Err
- end;
-find_changed_items(_UJID, _UAffiliation, _URole, _Items,
- _Lang, _StateData, _Res) ->
- {error, ?ERR_BAD_REQUEST}.
-
-
-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,
- _RoleorAffiliation, _Value, owner) ->
- %% Nobody can decrease MUC admin's role/affiliation
- false;
-can_change_ra(_FAffiliation, _FRole,
- TAffiliation, _TRole,
- affiliation, Value, _ServiceAf)
- when (TAffiliation == Value) ->
- nothing;
-can_change_ra(_FAffiliation, _FRole,
- _TAffiliation, TRole,
- role, Value, _ServiceAf)
- when (TRole == Value) ->
- nothing;
-can_change_ra(FAffiliation, _FRole,
- outcast, _TRole,
- affiliation, none, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- outcast, _TRole,
- affiliation, member, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(owner, _FRole,
- outcast, _TRole,
- affiliation, admin, _ServiceAf) ->
- true;
-can_change_ra(owner, _FRole,
- outcast, _TRole,
- affiliation, owner, _ServiceAf) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- none, _TRole,
- affiliation, outcast, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- none, _TRole,
- affiliation, member, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(owner, _FRole,
- none, _TRole,
- affiliation, admin, _ServiceAf) ->
- true;
-can_change_ra(owner, _FRole,
- none, _TRole,
- affiliation, owner, _ServiceAf) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- member, _TRole,
- affiliation, outcast, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- member, _TRole,
- affiliation, none, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(owner, _FRole,
- member, _TRole,
- affiliation, admin, _ServiceAf) ->
- true;
-can_change_ra(owner, _FRole,
- member, _TRole,
- affiliation, owner, _ServiceAf) ->
- true;
-can_change_ra(owner, _FRole,
- admin, _TRole,
- affiliation, _Affiliation, _ServiceAf) ->
- true;
-can_change_ra(owner, _FRole,
- owner, _TRole,
- affiliation, _Affiliation, _ServiceAf) ->
- check_owner;
-can_change_ra(_FAffiliation, _FRole,
- _TAffiliation, _TRole,
- affiliation, _Value, _ServiceAf) ->
- false;
-can_change_ra(_FAffiliation, moderator,
- _TAffiliation, visitor,
- role, none, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, moderator,
- _TAffiliation, visitor,
- role, participant, _ServiceAf) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- _TAffiliation, visitor,
- role, moderator, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(_FAffiliation, moderator,
- _TAffiliation, participant,
- role, none, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, moderator,
- _TAffiliation, participant,
- role, visitor, _ServiceAf) ->
- true;
-can_change_ra(FAffiliation, _FRole,
- _TAffiliation, participant,
- role, moderator, _ServiceAf)
- when (FAffiliation == owner) or (FAffiliation == admin) ->
- true;
-can_change_ra(_FAffiliation, _FRole,
- owner, moderator,
- role, visitor, _ServiceAf) ->
- false;
-can_change_ra(owner, _FRole,
- _TAffiliation, moderator,
- role, visitor, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, _FRole,
- admin, moderator,
- role, visitor, _ServiceAf) ->
- false;
-can_change_ra(admin, _FRole,
- _TAffiliation, moderator,
- role, visitor, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, _FRole,
- owner, moderator,
- role, participant, _ServiceAf) ->
- false;
-can_change_ra(owner, _FRole,
- _TAffiliation, moderator,
- role, participant, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, _FRole,
- admin, moderator,
- role, participant, _ServiceAf) ->
- false;
-can_change_ra(admin, _FRole,
- _TAffiliation, moderator,
- role, participant, _ServiceAf) ->
- true;
-can_change_ra(_FAffiliation, _FRole,
- _TAffiliation, _TRole,
- role, _Value, _ServiceAf) ->
- false.
-
-
-send_kickban_presence(JID, Reason, Code, StateData) ->
- NewAffiliation = get_affiliation(JID, StateData),
- send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData).
-
-send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) ->
- LJID = jlib:jid_tolower(JID),
- LJIDs = case LJID of
- {U, S, ""} ->
- ?DICT:fold(
- fun(J, _, Js) ->
- case J of
- {U, S, _} ->
- [J | Js];
- _ ->
- Js
- end
- end, [], StateData#state.users);
- _ ->
- case ?DICT:is_key(LJID, StateData#state.users) of
- true ->
- [LJID];
- _ ->
- []
- end
- end,
- lists:foreach(fun(J) ->
- {ok, #user{nick = Nick}} =
- ?DICT:find(J, StateData#state.users),
- add_to_log(kickban, {Nick, Reason, Code}, StateData),
- tab_remove_online_user(J, StateData),
- send_kickban_presence1(J, Reason, Code, NewAffiliation, StateData)
- end, LJIDs).
-
-send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) ->
- {ok, #user{jid = RealJID,
- nick = Nick}} =
- ?DICT:find(jlib:jid_tolower(UJID), StateData#state.users),
- SAffiliation = affiliation_to_list(Affiliation),
- BannedJIDString = jlib:jid_to_string(RealJID),
- lists:foreach(
- fun({_LJID, Info}) ->
- JidAttrList = case (Info#user.role == moderator) orelse
- ((StateData#state.config)#config.anonymous
- == false) of
- true -> [{"jid", BannedJIDString}];
- false -> []
- end,
- ItemAttrs = [{"affiliation", SAffiliation},
- {"role", "none"}] ++ JidAttrList,
- ItemEls = case Reason of
- "" ->
- [];
- _ ->
- [{xmlelement, "reason", [],
- [{xmlcdata, Reason}]}]
- end,
- Packet = {xmlelement, "presence", [{"type", "unavailable"}],
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs, ItemEls},
- {xmlelement, "status", [{"code", Code}], []}]}]},
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- Info#user.jid,
- Packet)
- end, ?DICT:to_list(StateData#state.users)).
-
-
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Owner stuff
-
-process_iq_owner(From, set, Lang, SubEl, StateData) ->
- FAffiliation = get_affiliation(From, StateData),
- case FAffiliation of
- owner ->
- {xmlelement, _Name, _Attrs, Els} = SubEl,
- case xml:remove_cdata(Els) of
- [{xmlelement, "x", _Attrs1, _Els1} = XEl] ->
- case {xml:get_tag_attr_s("xmlns", XEl),
- xml:get_tag_attr_s("type", XEl)} of
- {?NS_XDATA, "cancel"} ->
- {result, [], StateData};
- {?NS_XDATA, "submit"} ->
- 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;
- [{xmlelement, "destroy", _Attrs1, _Els1} = SubEl1] ->
- ?INFO_MSG("Destroyed MUC room ~s by the owner ~s",
- [jlib:jid_to_string(StateData#state.jid), jlib:jid_to_string(From)]),
- add_to_log(room_existence, destroyed, StateData),
- destroy_room(SubEl1, StateData);
- Items ->
- process_admin_items_set(From, Items, Lang, StateData)
- end;
- _ ->
- ErrText = "Owner privileges required",
- {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
- end;
-
-process_iq_owner(From, get, Lang, SubEl, StateData) ->
- FAffiliation = get_affiliation(From, StateData),
- case FAffiliation of
- owner ->
- {xmlelement, _Name, _Attrs, Els} = SubEl,
- case xml:remove_cdata(Els) of
- [] ->
- get_config(Lang, StateData, From);
- [Item] ->
- case xml:get_tag_attr("affiliation", Item) of
- false ->
- {error, ?ERR_BAD_REQUEST};
- {value, StrAffiliation} ->
- case catch list_to_affiliation(StrAffiliation) of
- {'EXIT', _} ->
- ErrText =
- io_lib:format(
- translate:translate(
- Lang,
- "Invalid affiliation: ~s"),
- [StrAffiliation]),
- {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
- SAffiliation ->
- Items = items_with_affiliation(
- SAffiliation, StateData),
- {result, Items, StateData}
- end
- end;
- _ ->
- {error, ?ERR_FEATURE_NOT_IMPLEMENTED}
- end;
- _ ->
- ErrText = "Owner privileges required",
- {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
- end.
-
-is_allowed_log_change(XEl, StateData, From) ->
- case lists:keymember("muc#roomconfig_enablelogging", 1,
- jlib:parse_xdata_submit(XEl)) of
- false ->
- true;
- true ->
- (allow == mod_muc_log:check_access_log(
- StateData#state.server_host, From))
- end.
-
-is_allowed_persistent_change(XEl, StateData, From) ->
- case lists:keymember("muc#roomconfig_persistentroom", 1,
- jlib:parse_xdata_submit(XEl)) of
- false ->
- true;
- true ->
- {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access,
- (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,
- StateData#state.mod,
- 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,
- StateData#state.mod,
- 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)},
- {"var", Var}],
- [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
-
--define(BOOLXFIELD(Label, Var, Val),
- ?XFIELD("boolean", Label, Var,
- case Val of
- true -> "1";
- _ -> "0"
- end)).
-
--define(STRINGXFIELD(Label, Var, Val),
- ?XFIELD("text-single", Label, Var, Val)).
-
--define(PRIVATEXFIELD(Label, Var, Val),
- ?XFIELD("text-private", Label, Var, Val)).
-
--define(JIDMULTIXFIELD(Label, Var, JIDList),
- {xmlelement, "field", [{"type", "jid-multi"},
- {"label", translate:translate(Lang, Label)},
- {"var", Var}],
- [{xmlelement, "value", [], [{xmlcdata, jlib:jid_to_string(JID)}]}
- || JID <- JIDList]}).
-
-get_default_room_maxusers(RoomState) ->
- DefRoomOpts = gen_mod:get_module_opt(
- RoomState#state.server_host,
- RoomState#state.mod, 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, 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),
- ?STRINGXFIELD("Room description",
- "muc#roomconfig_roomdesc",
- Config#config.description)
- ] ++
- case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
- allow ->
- [?BOOLXFIELD(
- "Make room persistent",
- "muc#roomconfig_persistentroom",
- Config#config.persistent)];
- _ -> []
- end ++ [
- ?BOOLXFIELD("Make room public searchable",
- "muc#roomconfig_publicroom",
- Config#config.public),
- ?BOOLXFIELD("Make participants list public",
- "public_list",
- Config#config.public_list),
- ?BOOLXFIELD("Make room password protected",
- "muc#roomconfig_passwordprotectedroom",
- Config#config.password_protected),
- ?PRIVATEXFIELD("Password",
- "muc#roomconfig_roomsecret",
- case Config#config.password_protected of
- true -> Config#config.password;
- false -> ""
- end),
- {xmlelement, "field",
- [{"type", "list-single"},
- {"label", translate:translate(Lang, "Maximum Number of Occupants")},
- {"var", "muc#roomconfig_maxusers"}],
- [{xmlelement, "value", [], [{xmlcdata, MaxUsersRoomString}]}] ++
- if
- is_integer(ServiceMaxUsers) -> [];
- true ->
- [{xmlelement, "option",
- [{"label", translate:translate(Lang, "No limit")}],
- [{xmlelement, "value", [], [{xmlcdata, "none"}]}]}]
- end ++
- [{xmlelement, "option", [{"label", erlang:integer_to_list(N)}],
- [{xmlelement, "value", [],
- [{xmlcdata, erlang:integer_to_list(N)}]}]} ||
- N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger |
- ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers]
- },
- {xmlelement, "field",
- [{"type", "list-single"},
- {"label", translate:translate(Lang, "Present real Jabber IDs to")},
- {"var", "muc#roomconfig_whois"}],
- [{xmlelement, "value", [], [{xmlcdata,
- if Config#config.anonymous ->
- "moderators";
- true ->
- "anyone"
- end}]},
- {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}],
- [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]},
- {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}],
- [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]},
- ?BOOLXFIELD("Make room members-only",
- "muc#roomconfig_membersonly",
- Config#config.members_only),
- ?BOOLXFIELD("Make room moderated",
- "muc#roomconfig_moderatedroom",
- Config#config.moderated),
- ?BOOLXFIELD("Default users as participants",
- "members_by_default",
- Config#config.members_by_default),
- ?BOOLXFIELD("Allow users to change the subject",
- "muc#roomconfig_changesubject",
- Config#config.allow_change_subj),
- ?BOOLXFIELD("Allow users to send private messages",
- "allow_private_messages",
- Config#config.allow_private_messages),
- {xmlelement, "field",
- [{"type", "list-single"},
- {"label", translate:translate(Lang, "Allow visitors to send private messages to")},
- {"var", "allow_private_messages_from_visitors"}],
- [{xmlelement, "value", [], [{xmlcdata,
- case Config#config.allow_private_messages_from_visitors of
- anyone ->
- "anyone";
- moderators ->
- "moderators";
- nobody ->
- "nobody"
- end}]},
- {xmlelement, "option", [{"label", translate:translate(Lang, "nobody")}],
- [{xmlelement, "value", [], [{xmlcdata, "nobody"}]}]},
- {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}],
- [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]},
- {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}],
- [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]},
- ?BOOLXFIELD("Allow users to query other users",
- "allow_query_users",
- Config#config.allow_query_users),
- ?BOOLXFIELD("Allow users to send invites",
- "muc#roomconfig_allowinvites",
- 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),
- ?BOOLXFIELD("Allow visitors to send voice requests",
- "muc#roomconfig_allowvoicerequests",
- Config#config.allow_voice_requests),
- ?STRINGXFIELD("Minimum interval between voice requests (in seconds)",
- "muc#roomconfig_voicerequestmininterval",
- erlang:integer_to_list(Config#config.voice_request_min_interval))
- ] ++
- case ejabberd_captcha:is_feature_available() of
- true ->
- [?BOOLXFIELD("Make room CAPTCHA protected",
- "captcha_protected",
- Config#config.captcha_protected)];
- false -> []
- end ++
- [?JIDMULTIXFIELD("Exclude Jabber IDs from CAPTCHA challenge",
- "muc#roomconfig_captcha_whitelist",
- ?SETS:to_list(Config#config.captcha_whitelist))] ++
- case mod_muc_log:check_access_log(
- StateData#state.server_host, From) of
- allow ->
- [?BOOLXFIELD(
- "Enable logging",
- "muc#roomconfig_enablelogging",
- Config#config.logging)];
- _ -> []
- end,
- {result, [{xmlelement, "instructions", [],
- [{xmlcdata,
- translate:translate(
- Lang, "You need an x:data capable client to configure room")}]},
- {xmlelement, "x", [{"xmlns", ?NS_XDATA},
- {"type", "form"}],
- Res}],
- StateData}.
-
-
-
-set_config(XEl, StateData) ->
- XData = jlib:parse_xdata_submit(XEl),
- case XData of
- invalid ->
- {error, ?ERR_BAD_REQUEST};
- _ ->
- case set_xoption(XData, StateData#state.config) of
- #config{} = Config ->
- Res = change_config(Config, StateData),
- {result, _, NSD} = Res,
- 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
- end
- end.
-
--define(SET_BOOL_XOPT(Opt, Val),
- case Val of
- "0" -> set_xoption(Opts, Config#config{Opt = false});
- "false" -> set_xoption(Opts, Config#config{Opt = false});
- "1" -> set_xoption(Opts, Config#config{Opt = true});
- "true" -> set_xoption(Opts, Config#config{Opt = true});
- _ -> {error, ?ERR_BAD_REQUEST}
- end).
-
--define(SET_NAT_XOPT(Opt, Val),
- case catch list_to_integer(Val) of
- I when is_integer(I),
- I > 0 ->
- set_xoption(Opts, Config#config{Opt = I});
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end).
-
--define(SET_STRING_XOPT(Opt, Val),
- set_xoption(Opts, Config#config{Opt = Val})).
-
--define(SET_JIDMULTI_XOPT(Opt, Vals),
- begin
- Set = lists:foldl(
- fun({U, S, R}, Set1) ->
- ?SETS:add_element({U, S, R}, Set1);
- (#jid{luser = U, lserver = S, lresource = R}, Set1) ->
- ?SETS:add_element({U, S, R}, Set1);
- (_, Set1) ->
- Set1
- end, ?SETS:empty(), Vals),
- set_xoption(Opts, Config#config{Opt = Set})
- end).
-
-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([{"allow_private_messages_from_visitors", [Val]} | Opts], Config) ->
- case Val of
- "anyone" ->
- ?SET_STRING_XOPT(allow_private_messages_from_visitors, anyone);
- "moderators" ->
- ?SET_STRING_XOPT(allow_private_messages_from_visitors, moderators);
- "nobody" ->
- ?SET_STRING_XOPT(allow_private_messages_from_visitors, nobody);
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end;
-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) ->
- ?SET_BOOL_XOPT(public_list, Val);
-set_xoption([{"muc#roomconfig_persistentroom", [Val]} | Opts], Config) ->
- ?SET_BOOL_XOPT(persistent, Val);
-set_xoption([{"muc#roomconfig_moderatedroom", [Val]} | Opts], Config) ->
- ?SET_BOOL_XOPT(moderated, Val);
-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) ->
- ?SET_BOOL_XOPT(password_protected, Val);
-set_xoption([{"muc#roomconfig_roomsecret", [Val]} | Opts], Config) ->
- ?SET_STRING_XOPT(password, Val);
-set_xoption([{"anonymous", [Val]} | Opts], Config) ->
- ?SET_BOOL_XOPT(anonymous, Val);
-set_xoption([{"muc#roomconfig_allowvoicerequests", [Val]} | Opts], Config) ->
- ?SET_BOOL_XOPT(allow_voice_requests, Val);
-set_xoption([{"muc#roomconfig_voicerequestmininterval", [Val]} | Opts], Config) ->
- ?SET_NAT_XOPT(voice_request_min_interval, Val);
-set_xoption([{"muc#roomconfig_whois", [Val]} | Opts], Config) ->
- case Val of
- "moderators" ->
- ?SET_BOOL_XOPT(anonymous, integer_to_list(1));
- "anyone" ->
- ?SET_BOOL_XOPT(anonymous, integer_to_list(0));
- _ ->
- {error, ?ERR_BAD_REQUEST}
- end;
-set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) ->
- case Val of
- "none" ->
- ?SET_STRING_XOPT(max_users, none);
- _ ->
- ?SET_NAT_XOPT(max_users, Val)
- end;
-set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) ->
- ?SET_BOOL_XOPT(logging, Val);
-set_xoption([{"muc#roomconfig_captcha_whitelist", Vals} | Opts], Config) ->
- JIDs = [jlib:string_to_jid(Val) || Val <- Vals],
- ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
-set_xoption([{"FORM_TYPE", _} | Opts], Config) ->
- %% Ignore our FORM_TYPE
- set_xoption(Opts, Config);
-set_xoption([_ | _Opts], _Config) ->
- {error, ?ERR_BAD_REQUEST}.
-
-
-change_config(Config, StateData) ->
- NSD = StateData#state{config = Config},
- Mod = StateData#state.mod,
- case {(StateData#state.config)#config.persistent,
- Config#config.persistent} of
- {_, true} ->
- Mod:store_room(NSD#state.server_host, NSD#state.host,
- NSD#state.room, make_opts(NSD));
- {true, false} ->
- Mod:forget_room(NSD#state.server_host, NSD#state.host, NSD#state.room);
- {false, false} ->
- ok
- end,
- case {(StateData#state.config)#config.members_only,
- Config#config.members_only} of
- {false, true} ->
- NSD1 = remove_nonmembers(NSD),
- {result, [], NSD1};
- _ ->
- {result, [], NSD}
- end.
-
-remove_nonmembers(StateData) ->
- lists:foldl(
- fun({_LJID, #user{jid = JID}}, SD) ->
- Affiliation = get_affiliation(JID, SD),
- case Affiliation of
- none ->
- catch send_kickban_presence(
- JID, "", "322", SD),
- set_role(JID, none, SD);
- _ ->
- SD
- end
- end, StateData, ?DICT:to_list(StateData#state.users)).
-
-
--define(CASE_CONFIG_OPT(Opt),
- Opt -> StateData#state{
- config = (StateData#state.config)#config{Opt = Val}}).
-
-set_opts([], StateData) ->
- 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_private_messages_from_visitors -> StateData#state{config = (StateData#state.config)#config{allow_private_messages_from_visitors = 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}};
- moderated -> StateData#state{config = (StateData#state.config)#config{moderated = Val}};
- members_by_default -> StateData#state{config = (StateData#state.config)#config{members_by_default = Val}};
- 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}};
- captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = ?SETS:from_list(Val)}};
- allow_voice_requests -> StateData#state{config = (StateData#state.config)#config{allow_voice_requests = Val}};
- voice_request_min_interval -> StateData#state{config = (StateData#state.config)#config{voice_request_min_interval = Val}};
- max_users ->
- ServiceMaxUsers = get_service_max_users(StateData),
- MaxUsers = if
- Val =< ServiceMaxUsers -> Val;
- true -> ServiceMaxUsers
- end,
- StateData#state{
- config = (StateData#state.config)#config{
- max_users = MaxUsers}};
- affiliations ->
- StateData#state{affiliations = ?DICT:from_list(Val)};
- subject ->
- StateData#state{subject = Val};
- subject_author ->
- StateData#state{subject_author = Val};
- _ -> StateData
- end,
- set_opts(Opts, NSD).
-
--define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
-
-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_private_messages_from_visitors),
- ?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),
- ?MAKE_CONFIG_OPT(moderated),
- ?MAKE_CONFIG_OPT(members_by_default),
- ?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),
- ?MAKE_CONFIG_OPT(max_users),
- ?MAKE_CONFIG_OPT(allow_voice_requests),
- ?MAKE_CONFIG_OPT(voice_request_min_interval),
- {captcha_whitelist,
- ?SETS:to_list((StateData#state.config)#config.captcha_whitelist)},
- {affiliations, ?DICT:to_list(StateData#state.affiliations)},
- {subject, StateData#state.subject},
- {subject_author, StateData#state.subject_author}
- ].
-
-
-
-destroy_room(DEl, StateData) ->
- lists:foreach(
- fun({_LJID, Info}) ->
- Nick = Info#user.nick,
- ItemAttrs = [{"affiliation", "none"},
- {"role", "none"}],
- Packet = {xmlelement, "presence", [{"type", "unavailable"}],
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
- [{xmlelement, "item", ItemAttrs, []}, DEl]}]},
- route_stanza(
- jlib:jid_replace_resource(StateData#state.jid, Nick),
- Info#user.jid,
- Packet)
- end, ?DICT:to_list(StateData#state.users)),
- case (StateData#state.config)#config.persistent of
- true ->
- (StateData#state.mod):forget_room(
- StateData#state.server_host,
- StateData#state.host, StateData#state.room);
- false ->
- ok
- end,
- {result, [], stop}.
-
-
-
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Disco
-
--define(FEATURE(Var), {xmlelement, "feature", [{"var", Var}], []}).
-
--define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse),
- case Opt of
- true ->
- ?FEATURE(Fiftrue);
- false ->
- ?FEATURE(Fiffalse)
- end).
-
-process_iq_disco_info(_From, set, _Lang, _StateData) ->
- {error, ?ERR_NOT_ALLOWED};
-
-process_iq_disco_info(_From, get, Lang, StateData) ->
- Config = StateData#state.config,
- {result, [{xmlelement, "identity",
- [{"category", "conference"},
- {"type", "text"},
- {"name", get_title(StateData)}], []},
- {xmlelement, "feature",
- [{"var", ?NS_MUC}], []},
- ?CONFIG_OPT_TO_FEATURE(Config#config.public,
- "muc_public", "muc_hidden"),
- ?CONFIG_OPT_TO_FEATURE(Config#config.persistent,
- "muc_persistent", "muc_temporary"),
- ?CONFIG_OPT_TO_FEATURE(Config#config.members_only,
- "muc_membersonly", "muc_open"),
- ?CONFIG_OPT_TO_FEATURE(Config#config.anonymous,
- "muc_semianonymous", "muc_nonanonymous"),
- ?CONFIG_OPT_TO_FEATURE(Config#config.moderated,
- "muc_moderated", "muc_unmoderated"),
- ?CONFIG_OPT_TO_FEATURE(Config#config.password_protected,
- "muc_passwordprotected", "muc_unsecured")
- ] ++ iq_disco_info_extras(Lang, StateData), StateData}.
-
--define(RFIELDT(Type, Var, Val),
- {xmlelement, "field", [{"type", Type}, {"var", Var}],
- [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
-
--define(RFIELD(Label, Var, Val),
- {xmlelement, "field", [{"label", translate:translate(Lang, Label)},
- {"var", Var}],
- [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
-
-iq_disco_info_extras(Lang, StateData) ->
- Len = ?DICT:size(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))
- ]}].
-
-process_iq_disco_items(_From, set, _Lang, _StateData) ->
- {error, ?ERR_NOT_ALLOWED};
-
-process_iq_disco_items(From, get, _Lang, StateData) ->
- case (StateData#state.config)#config.public_list of
- true ->
- {result, get_mucroom_disco_items(StateData), StateData};
- _ ->
- case is_occupant_or_admin(From, StateData) of
- true ->
- {result, get_mucroom_disco_items(StateData), StateData};
- _ ->
- {error, ?ERR_FORBIDDEN}
- end
- 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
- "" ->
- StateData#state.room;
- Name ->
- Name
- end.
-
-get_roomdesc_reply(JID, StateData, Tail) ->
- IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData),
- if (StateData#state.config)#config.public or IsOccupantOrAdmin ->
- if (StateData#state.config)#config.public_list or IsOccupantOrAdmin ->
- {item, get_title(StateData) ++ Tail};
- true ->
- {item, get_title(StateData)}
- end;
- true ->
- false
- end.
-
-get_roomdesc_tail(StateData, Lang) ->
- Desc = case (StateData#state.config)#config.public of
- true ->
- "";
- _ ->
- translate:translate(Lang, "private, ")
- end,
- Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, StateData#state.users),
- " (" ++ Desc ++ integer_to_list(Len) ++ ")".
-
-get_mucroom_disco_items(StateData) ->
- lists:map(
- fun({_LJID, Info}) ->
- Nick = Info#user.nick,
- {xmlelement, "item",
- [{"jid", jlib:jid_to_string({StateData#state.room,
- StateData#state.host, Nick})},
- {"name", Nick}], []}
- end,
- ?DICT:to_list(StateData#state.users)).
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Voice request support
-
-is_voice_request(Els) ->
- lists:foldl(
- fun({xmlelement, "x", Attrs, _} = El, false) ->
- case xml:get_attr_s("xmlns", Attrs) of
- ?NS_XDATA ->
- case jlib:parse_xdata_submit(El) of
- [_|_] = Fields ->
- case {lists:keysearch("FORM_TYPE", 1, Fields),
- lists:keysearch("muc#role", 1, Fields)} of
- {{value,
- {_, ["http://jabber.org/protocol/muc#request"]}},
- {value, {_, ["participant"]}}} ->
- true;
- _ ->
- false
- end;
- _ ->
- false
- end;
- _ ->
- false
- end;
- (_, Acc) ->
- Acc
- end, false, Els).
-
-prepare_request_form(Requester, Nick, Lang) ->
- {xmlelement, "message", [{"type", "normal"}],
- [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}],
- [{xmlelement, "title", [],
- [{xmlcdata, translate:translate(Lang, "Voice request")}]},
- {xmlelement, "instructions", [],
- [{xmlcdata,
- translate:translate(
- Lang, "Either approve or decline the voice request.")}]},
- {xmlelement, "field", [{"var", "FORM_TYPE"}, {"type", "hidden"}],
- [{xmlelement, "value", [],
- [{xmlcdata, "http://jabber.org/protocol/muc#request"}]}]},
- {xmlelement, "field", [{"var", "muc#role"}, {"type", "hidden"}],
- [{xmlelement, "value", [], [{xmlcdata, "participant"}]}]},
- ?STRINGXFIELD("User JID", "muc#jid", jlib:jid_to_string(Requester)),
- ?STRINGXFIELD("Nickname", "muc#roomnick", Nick),
- ?BOOLXFIELD("Grant voice to this person?", "muc#request_allow",
- list_to_atom("false"))
- ]}]}.
-
-send_voice_request(From, StateData) ->
- Moderators = search_role(moderator, StateData),
- FromNick = find_nick_by_jid(From, StateData),
- lists:foreach(
- fun({_, User}) ->
- route_stanza(
- StateData#state.jid,
- User#user.jid,
- prepare_request_form(From, FromNick, ""))
- end, Moderators).
-
-is_voice_approvement(Els) ->
- lists:foldl(
- fun({xmlelement, "x", Attrs, _} = El, false) ->
- case xml:get_attr_s("xmlns", Attrs) of
- ?NS_XDATA ->
- case jlib:parse_xdata_submit(El) of
- [_|_] = Fs ->
- case {lists:keysearch("FORM_TYPE", 1, Fs),
- lists:keysearch("muc#role", 1, Fs),
- lists:keysearch("muc#request_allow", 1, Fs)} of
- {{value,
- {_, ["http://jabber.org/protocol/muc#request"]}},
- {value, {_, ["participant"]}},
- {value, {_, [Flag]}}}
- when Flag == "true"; Flag == "1" ->
- true;
- _ ->
- false
- end;
- _ ->
- false
- end;
- _ ->
- false
- end;
- (_, Acc) ->
- Acc
- end, false, Els).
-
-extract_jid_from_voice_approvement(Els) ->
- lists:foldl(
- fun({xmlelement, "x", _, _} = El, error) ->
- Fields = case jlib:parse_xdata_submit(El) of
- invalid -> [];
- Res -> Res
- end,
- lists:foldl(
- fun({"muc#jid", [JIDStr]}, error) ->
- case jlib:string_to_jid(JIDStr) of
- error -> error;
- J -> {ok, J}
- end;
- (_, Acc) ->
- Acc
- end, error, Fields);
- (_, Acc) ->
- Acc
- end, error, Els).
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% Invitation support
-
-is_invitation(Els) ->
- lists:foldl(
- fun({xmlelement, "x", Attrs, _} = El, false) ->
- case xml:get_attr_s("xmlns", Attrs) of
- ?NS_MUC_USER ->
- case xml:get_subtag(El, "invite") of
- false ->
- false;
- _ ->
- true
- end;
- _ ->
- false
- end;
- (_, Acc) ->
- Acc
- end, false, Els).
-
-check_invitation(From, Els, Lang, StateData) ->
- FAffiliation = get_affiliation(From, StateData),
- CanInvite = (StateData#state.config)#config.allow_user_invites
- orelse (FAffiliation == admin) orelse (FAffiliation == owner),
- InviteEl = case xml:remove_cdata(Els) of
- [{xmlelement, "x", _Attrs1, Els1} = XEl] ->
- case xml:get_tag_attr_s("xmlns", XEl) of
- ?NS_MUC_USER ->
- ok;
- _ ->
- throw({error, ?ERR_BAD_REQUEST})
- end,
- case xml:remove_cdata(Els1) of
- [{xmlelement, "invite", _Attrs2, _Els2} = InviteEl1] ->
- InviteEl1;
- _ ->
- throw({error, ?ERR_BAD_REQUEST})
- end;
- _ ->
- throw({error, ?ERR_BAD_REQUEST})
- end,
- JID = case jlib:string_to_jid(
- xml:get_tag_attr_s("to", InviteEl)) of
- error ->
- throw({error, ?ERR_JID_MALFORMED});
- JID1 ->
- JID1
- end,
- case CanInvite of
- false ->
- throw({error, ?ERR_NOT_ALLOWED});
- true ->
- Reason =
- xml:get_path_s(
- InviteEl,
- [{elem, "reason"}, cdata]),
- ContinueEl =
- case xml:get_path_s(
- InviteEl,
- [{elem, "continue"}]) of
- [] -> [];
- Continue1 -> [Continue1]
- end,
- IEl =
- [{xmlelement, "invite",
- [{"from",
- jlib:jid_to_string(From)}],
- [{xmlelement, "reason", [],
- [{xmlcdata, Reason}]}] ++ ContinueEl}],
- PasswdEl =
- case (StateData#state.config)#config.password_protected of
- true ->
- [{xmlelement, "password", [],
- [{xmlcdata, (StateData#state.config)#config.password}]}];
- _ ->
- []
- end,
- Body =
- {xmlelement, "body", [],
- [{xmlcdata,
- lists:flatten(
- io_lib:format(
- translate:translate(
- Lang,
- "~s invites you to the room ~s"),
- [jlib:jid_to_string(From),
- jlib:jid_to_string({StateData#state.room,
- StateData#state.host,
- ""})
- ])) ++
- case (StateData#state.config)#config.password_protected of
- true ->
- ", " ++
- translate:translate(Lang, "the password is") ++
- " '" ++
- (StateData#state.config)#config.password ++ "'";
- _ ->
- ""
- end ++
- case Reason of
- "" -> "";
- _ -> " (" ++ Reason ++ ") "
- end
- }]},
- Msg =
- {xmlelement, "message",
- [{"type", "normal"}],
- [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], IEl ++ PasswdEl},
- {xmlelement, "x",
- [{"xmlns", ?NS_XCONFERENCE},
- {"jid", jlib:jid_to_string(
- {StateData#state.room,
- StateData#state.host,
- ""})}],
- [{xmlcdata, Reason}]},
- Body]},
- route_stanza(StateData#state.jid, JID, Msg),
- JID
- end.
-
-%% Handle a message sent to the room by a non-participant.
-%% If it is a decline, send to the inviter.
-%% Otherwise, an error message is sent to the sender.
-handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From) ->
- case catch check_decline_invitation(Packet) of
- {true, Decline_data} ->
- send_decline_invitation(Decline_data, StateData#state.jid, From);
- _ ->
- send_error_only_occupants(Packet, Lang, StateData#state.jid, From)
- end.
-
-%% Check in the packet is a decline.
-%% If so, also returns the splitted packet.
-%% This function must be catched,
-%% because it crashes when the packet is not a decline message.
-check_decline_invitation(Packet) ->
- {xmlelement, "message", _, _} = Packet,
- XEl = xml:get_subtag(Packet, "x"),
- ?NS_MUC_USER = xml:get_tag_attr_s("xmlns", XEl),
- DEl = xml:get_subtag(XEl, "decline"),
- ToString = xml:get_tag_attr_s("to", DEl),
- ToJID = jlib:string_to_jid(ToString),
- {true, {Packet, XEl, DEl, ToJID}}.
-
-%% Send the decline to the inviter user.
-%% The original stanza must be slightly modified.
-send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) ->
- FromString = jlib:jid_to_string(jlib:jid_remove_resource(FromJID)),
- {xmlelement, "decline", DAttrs, DEls} = DEl,
- DAttrs2 = lists:keydelete("to", 1, DAttrs),
- DAttrs3 = [{"from", FromString} | DAttrs2],
- DEl2 = {xmlelement, "decline", DAttrs3, DEls},
- XEl2 = replace_subelement(XEl, DEl2),
- Packet2 = replace_subelement(Packet, XEl2),
- route_stanza(RoomJID, ToJID, Packet2).
-
-%% Given an element and a new subelement,
-%% replace the instance of the subelement in element with the new subelement.
-replace_subelement({xmlelement, Name, Attrs, SubEls}, NewSubEl) ->
- {_, NameNewSubEl, _, _} = NewSubEl,
- SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl),
- {xmlelement, Name, Attrs, SubEls2}.
-
-send_error_only_occupants(Packet, Lang, RoomJID, From) ->
- ErrText = "Only occupants are allowed to send messages to the conference",
- Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
- route_stanza(RoomJID, From, Err).
-
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-% 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 ->
- mod_muc_log:add_to_log(
- StateData#state.server_host, Type, Data,
- StateData#state.jid, make_opts(StateData));
- false ->
- ok
- end.
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%% Users number checking
-
-tab_add_online_user(JID, StateData) ->
- {LUser, LServer, LResource} = jlib:jid_tolower(JID),
- US = {LUser, LServer},
- Room = StateData#state.room,
- Host = StateData#state.host,
- catch ets:insert(
- muc_online_users,
- #muc_online_users{us = US, resource = LResource, room = Room, host = Host}).
-
-
-tab_remove_online_user(JID, StateData) ->
- {LUser, LServer, LResource} = jlib:jid_tolower(JID),
- US = {LUser, LServer},
- Room = StateData#state.room,
- Host = StateData#state.host,
- catch ets:delete_object(
- muc_online_users,
- #muc_online_users{us = US, resource = LResource, room = Room, host = Host}).
-
-tab_count_user(JID) ->
- {LUser, LServer, _} = jlib:jid_tolower(JID),
- US = {LUser, LServer},
- case catch ets:select(
- muc_online_users,
- [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) of
- Res when is_list(Res) ->
- length(Res);
- _ ->
- 0
- end.
-
-element_size(El) ->
- size(xml:element_to_binary(El)).
-
-route_stanza(From, To, El) ->
- case mod_muc:is_broadcasted(From#jid.lserver) of
- true ->
- #jid{luser = LUser, lserver = LServer} = To,
- case ejabberd_cluster:get_node({LUser, LServer}) of
- Node when Node == node() ->
- ejabberd_router:route(From, To, El);
- _ ->
- ok
- end;
- false ->
- ejabberd_router:route(From, To, El)
- end.
+%%%----------------------------------------------------------------------
+%%% File : mod_muc_room.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : MUC room stuff
+%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2012 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
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_muc_room).
+-author('alexey@process-one.net').
+
+-define(GEN_FSM, p1_fsm).
+
+-behaviour(?GEN_FSM).
+
+
+%% External exports
+-export([start_link/11,
+ start_link/9,
+ start_link/2,
+ start/11,
+ start/9,
+ start/2,
+ migrate/3,
+ route/4,
+ moderate_room_history/2,
+ persist_recent_messages/1]).
+
+%% gen_fsm callbacks
+-export([init/1,
+ normal_state/2,
+ handle_event/3,
+ handle_sync_event/4,
+ handle_info/3,
+ terminate/3,
+ print_state/1,
+ code_change/4]).
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include("mod_muc_room.hrl").
+
+-define(MAX_USERS_DEFAULT_LIST,
+ [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
+
+%-define(DBGFSM, true).
+
+-ifdef(DBGFSM).
+-define(FSMOPTS, [{debug, [trace]}]).
+-else.
+-define(FSMOPTS, []).
+-endif.
+
+%% Module start with or without supervisor:
+-ifdef(NO_TRANSIENT_SUPERVISORS).
+-define(SUPERVISOR_START(Args),
+ ?GEN_FSM:start(?MODULE, Args, ?FSMOPTS)).
+-else.
+-define(SUPERVISOR_START(Args),
+ Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
+ supervisor:start_child(Supervisor, Args)).
+-endif.
+
+%%%----------------------------------------------------------------------
+%%% API
+%%%----------------------------------------------------------------------
+start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
+ Creator, Nick, DefRoomOpts, Mod) ->
+ ?SUPERVISOR_START([Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Creator, Nick, DefRoomOpts, Mod]).
+
+start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod) ->
+ Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
+ supervisor:start_child(
+ Supervisor, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
+ Opts, Mod]).
+
+start(StateName, StateData) ->
+ ServerHost = StateData#state.server_host,
+ ?SUPERVISOR_START([StateName, StateData]).
+
+start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper,
+ Creator, Nick, DefRoomOpts, Mod) ->
+ ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Creator, Nick, DefRoomOpts, Mod],
+ ?FSMOPTS).
+
+start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod) ->
+ ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory,
+ RoomShaper, Opts, Mod],
+ ?FSMOPTS).
+
+start_link(StateName, StateData) ->
+ ?GEN_FSM:start_link(?MODULE, [StateName, StateData], ?FSMOPTS).
+
+migrate(FsmRef, Node, After) ->
+ erlang:send_after(After, FsmRef, {migrate, Node}).
+
+moderate_room_history(FsmRef, Nick) ->
+ ?GEN_FSM:sync_send_all_state_event(FsmRef, {moderate_room_history, Nick}).
+
+persist_recent_messages(FsmRef) ->
+ ?GEN_FSM:sync_send_all_state_event(FsmRef, persist_recent_messages).
+%%%----------------------------------------------------------------------
+%%% Callback functions from gen_fsm
+%%%----------------------------------------------------------------------
+
+%%----------------------------------------------------------------------
+%% Func: init/1
+%% Returns: {ok, StateName, StateData} |
+%% {ok, StateName, StateData, Timeout} |
+%% ignore |
+%% {stop, StopReason}
+%%----------------------------------------------------------------------
+init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Creator, _Nick, DefRoomOpts, Mod]) ->
+ process_flag(trap_exit, true),
+ Shaper = shaper:new(RoomShaper),
+ State = set_affiliation(Creator, owner,
+ #state{host = Host,
+ server_host = ServerHost,
+ mod = Mod,
+ access = Access,
+ room = Room,
+ history = lqueue_new(HistorySize),
+ persist_history = PersistHistory,
+ jid = jlib:make_jid(Room, Host, ""),
+ just_created = true,
+ room_shaper = Shaper}),
+ State1 = set_opts(DefRoomOpts, State),
+ %% this will trigger a write of the muc to disc if it is persistent.
+ %% we need to do this because otherwise if muc are persistent by default,
+ %% but never configured in any way by the client, we were never
+ %% storing it on disc to be recreated on startup.
+ if
+ (State1#state.config)#config.persistent ->
+ mod_muc:store_room(State1#state.host, State1#state.room, make_opts(State1));
+ true ->
+ ok
+ end,
+ ?INFO_MSG("Created MUC room ~s@~s by ~s",
+ [Room, Host, jlib:jid_to_string(Creator)]),
+ add_to_log(room_existence, created, State1),
+ add_to_log(room_existence, started, State1),
+ {ok, normal_state, State1};
+init([Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts, Mod]) ->
+ process_flag(trap_exit, true),
+ Shaper = shaper:new(RoomShaper),
+ State = set_opts(Opts, #state{host = Host,
+ server_host = ServerHost,
+ mod = Mod,
+ access = Access,
+ room = Room,
+ history = load_history(ServerHost, Room, PersistHistory, lqueue_new(HistorySize)),
+ persist_history = PersistHistory,
+ jid = jlib:make_jid(Room, Host, ""),
+ room_shaper = Shaper}),
+ add_to_log(room_existence, started, State),
+ {ok, normal_state, State};
+init([StateName, #state{room = Room, host = Host} = StateData]) ->
+ process_flag(trap_exit, true),
+ mod_muc:register_room(Host, Room, self()),
+ {ok, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: StateName/2
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+normal_state({route, From, "",
+ {xmlelement, "message", Attrs, Els} = Packet},
+ StateData) ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ 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" ->
+ Activity = get_user_activity(From, StateData),
+ Now = now_to_usec(now()),
+ MinMessageInterval =
+ trunc(gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod,
+ min_message_interval, 0) * 1000000),
+ Size = element_size(Packet),
+ {MessageShaper, MessageShaperInterval} =
+ shaper:update(Activity#activity.message_shaper, Size),
+ if
+ Activity#activity.message /= undefined ->
+ ErrText = "Traffic rate limit is exceeded",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ {next_state, normal_state, StateData};
+ Now >= Activity#activity.message_time + MinMessageInterval,
+ MessageShaperInterval == 0 ->
+ {RoomShaper, RoomShaperInterval} =
+ shaper:update(StateData#state.room_shaper, Size),
+ RoomQueueEmpty = queue:is_empty(
+ StateData#state.room_queue),
+ if
+ RoomShaperInterval == 0,
+ RoomQueueEmpty ->
+ NewActivity = Activity#activity{
+ message_time = Now,
+ message_shaper = MessageShaper},
+ StateData1 =
+ store_user_activity(
+ From, NewActivity, StateData),
+ StateData2 =
+ StateData1#state{
+ room_shaper = RoomShaper},
+ process_groupchat_message(From, Packet, StateData2);
+ true ->
+ StateData1 =
+ if
+ RoomQueueEmpty ->
+ erlang:send_after(
+ RoomShaperInterval, self(),
+ process_room_queue),
+ StateData#state{
+ room_shaper = RoomShaper};
+ true ->
+ StateData
+ end,
+ NewActivity = Activity#activity{
+ message_time = Now,
+ message_shaper = MessageShaper,
+ message = Packet},
+ RoomQueue = queue:in(
+ {message, From},
+ StateData#state.room_queue),
+ StateData2 =
+ store_user_activity(
+ From, NewActivity, StateData1),
+ StateData3 =
+ StateData2#state{
+ room_queue = RoomQueue},
+ {next_state, normal_state, StateData3}
+ end;
+ true ->
+ MessageInterval =
+ (Activity#activity.message_time +
+ MinMessageInterval - Now) div 1000,
+ Interval = lists:max([MessageInterval,
+ MessageShaperInterval]),
+ erlang:send_after(
+ Interval, self(), {process_user_message, From}),
+ NewActivity = Activity#activity{
+ message = Packet,
+ message_shaper = MessageShaper},
+ StateData1 =
+ store_user_activity(
+ From, NewActivity, StateData),
+ {next_state, normal_state, StateData1}
+ end;
+ "error" ->
+ case is_user_online(From, StateData) of
+ true ->
+ ErrorText = "This participant is kicked from the room because "
+ "he sent an error message",
+ NewState = expulse_participant(Packet, From, StateData,
+ translate:translate(Lang, ErrorText)),
+ {next_state, normal_state, NewState};
+ _ ->
+ {next_state, normal_state, StateData}
+ end;
+ "chat" ->
+ ErrText = "It is not allowed to send private messages to the conference",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ {next_state, normal_state, StateData};
+ Type when (Type == "") or (Type == "normal") ->
+ IsInvitation = is_invitation(Els),
+ IsVoiceRequest = is_voice_request(Els)
+ and is_visitor(From, StateData),
+ IsVoiceApprovement = is_voice_approvement(Els)
+ and not is_visitor(From, StateData),
+ if IsInvitation ->
+ case catch check_invitation(From, Els, Lang, StateData) of
+ {error, Error} ->
+ Err = jlib:make_error_reply(
+ Packet, Error),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ {next_state, normal_state, StateData};
+ IJID ->
+ Config = StateData#state.config,
+ case Config#config.members_only of
+ true ->
+ case get_affiliation(IJID, StateData) of
+ none ->
+ NSD = set_affiliation(
+ IJID,
+ member,
+ StateData),
+ case (NSD#state.config)#config.persistent of
+ true ->
+ (NSD#state.mod):store_room(
+ NSD#state.server_host,
+ NSD#state.host,
+ NSD#state.room,
+ make_opts(NSD));
+ _ ->
+ ok
+ end,
+ {next_state, normal_state, NSD};
+ _ ->
+ {next_state, normal_state,
+ StateData}
+ end;
+ false ->
+ {next_state, normal_state, StateData}
+ end
+ end;
+ IsVoiceRequest ->
+ NewStateData =
+ case (StateData#state.config)#config.allow_voice_requests of
+ true ->
+ MinInterval = (StateData#state.config)
+ #config.voice_request_min_interval,
+ BareFrom = jlib:jid_remove_resource(
+ jlib:jid_tolower(From)),
+ NowPriority = -now_to_usec(now()),
+ CleanPriority =
+ NowPriority + MinInterval*1000000,
+ Times = clean_treap(
+ StateData#state.last_voice_request_time,
+ CleanPriority),
+ case treap:lookup(BareFrom, Times) of
+ error ->
+ Times1 = treap:insert(
+ BareFrom,
+ NowPriority,
+ true, Times),
+ NSD = StateData#state{
+ last_voice_request_time =
+ Times1},
+ send_voice_request(From, NSD),
+ NSD;
+ {ok, _, _} ->
+ ErrText = "Please, wait for "
+ "a while before sending "
+ "new voice request",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_NOT_ACCEPTABLE(
+ Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ StateData#state{
+ last_voice_request_time =
+ Times}
+ end;
+ false ->
+ ErrText = "Voice requests are "
+ "disabled in this conference",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_FORBIDDEN(
+ Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid, From, Err),
+ StateData
+ end,
+ {next_state, normal_state, NewStateData};
+ IsVoiceApprovement ->
+ NewStateData =
+ case is_moderator(From, StateData) of
+ true ->
+ case extract_jid_from_voice_approvement(Els) of
+ error ->
+ ErrText = "Failed to extract "
+ "JID from your voice "
+ "request approval",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_BAD_REQUEST(
+ Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ StateData;
+ {ok, TargetJid} ->
+ case is_visitor(
+ TargetJid, StateData) of
+ true ->
+ Reason = [],
+ NSD = set_role(
+ TargetJid,
+ participant,
+ StateData),
+ catch send_new_presence(
+ TargetJid,
+ Reason, NSD),
+ NSD;
+ _ ->
+ StateData
+ end
+ end;
+ _ ->
+ ErrText = "Only moderators can "
+ "approve voice requests",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_NOT_ALLOWED(
+ Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid, From, Err),
+ StateData
+ end,
+ {next_state, normal_state, NewStateData};
+ true ->
+ {next_state, normal_state, StateData}
+ end;
+ _ ->
+ ErrText = "Improper message type",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ {next_state, normal_state, StateData}
+ end;
+ _ ->
+ case xml:get_attr_s("type", Attrs) of
+ "error" ->
+ ok;
+ _ ->
+ handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From)
+ end,
+ {next_state, normal_state, StateData}
+ end;
+
+normal_state({route, From, "",
+ {xmlelement, "iq", _Attrs, _Els} = Packet},
+ StateData) ->
+ case jlib:iq_query_info(Packet) of
+ #iq{type = Type, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ when
+ (XMLNS == ?NS_MUC_ADMIN) or
+ (XMLNS == ?NS_MUC_OWNER) or
+ (XMLNS == ?NS_DISCO_INFO) or
+ (XMLNS == ?NS_DISCO_ITEMS) or
+ (XMLNS == ?NS_CAPTCHA) ->
+ Res1 = case XMLNS of
+ ?NS_MUC_ADMIN ->
+ process_iq_admin(From, Type, Lang, SubEl, StateData);
+ ?NS_MUC_OWNER ->
+ process_iq_owner(From, Type, Lang, SubEl, StateData);
+ ?NS_DISCO_INFO ->
+ process_iq_disco_info(From, Type, Lang, StateData);
+ ?NS_DISCO_ITEMS ->
+ process_iq_disco_items(From, Type, Lang, StateData);
+ ?NS_CAPTCHA ->
+ process_iq_captcha(From, Type, Lang, SubEl, StateData)
+ end,
+ {IQRes, NewStateData} =
+ case Res1 of
+ {result, Res, SD} ->
+ {IQ#iq{type = result,
+ sub_el = [{xmlelement, "query",
+ [{"xmlns", XMLNS}],
+ Res
+ }]},
+ SD};
+ {error, Error} ->
+ {IQ#iq{type = error,
+ sub_el = [SubEl, Error]},
+ StateData}
+ end,
+ route_stanza(StateData#state.jid,
+ From,
+ jlib:iq_to_xml(IQRes)),
+ case NewStateData of
+ stop ->
+ {stop, normal, StateData};
+ _ ->
+ {next_state, normal_state, NewStateData}
+ end;
+ reply ->
+ {next_state, normal_state, StateData};
+ _ ->
+ Err = jlib:make_error_reply(
+ Packet, ?ERR_FEATURE_NOT_IMPLEMENTED),
+ route_stanza(StateData#state.jid, From, Err),
+ {next_state, normal_state, StateData}
+ end;
+
+normal_state({route, From, Nick,
+ {xmlelement, "presence", _Attrs, _Els} = Packet},
+ StateData) ->
+ Activity = get_user_activity(From, StateData),
+ Now = now_to_usec(now()),
+ MinPresenceInterval =
+ trunc(gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, min_presence_interval, 0) * 1000000),
+ if
+ (Now >= Activity#activity.presence_time + MinPresenceInterval) and
+ (Activity#activity.presence == undefined) ->
+ NewActivity = Activity#activity{presence_time = Now},
+ StateData1 = store_user_activity(From, NewActivity, StateData),
+ process_presence(From, Nick, Packet, StateData1);
+ true ->
+ if
+ Activity#activity.presence == undefined ->
+ Interval = (Activity#activity.presence_time +
+ MinPresenceInterval - Now) div 1000,
+ erlang:send_after(
+ Interval, self(), {process_user_presence, From});
+ true ->
+ ok
+ end,
+ NewActivity = Activity#activity{presence = {Nick, Packet}},
+ StateData1 = store_user_activity(From, NewActivity, StateData),
+ {next_state, normal_state, StateData1}
+ end;
+
+normal_state({route, From, ToNick,
+ {xmlelement, "message", Attrs, _} = Packet},
+ StateData) ->
+ Type = xml:get_attr_s("type", Attrs),
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ case decide_fate_message(Type, Packet, From, StateData) of
+ {expulse_sender, Reason} ->
+ ?DEBUG(Reason, []),
+ ErrorText = "This participant is kicked from the room because "
+ "he sent an error message to another participant",
+ NewState = expulse_participant(Packet, From, StateData,
+ translate:translate(Lang, ErrorText)),
+ {next_state, normal_state, NewState};
+ forget_message ->
+ {next_state, normal_state, StateData};
+ continue_delivery ->
+ 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 "
+ "messages of type \"groupchat\"",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ ToNick),
+ From, Err);
+ _ ->
+ case find_jids_by_nick(ToNick, StateData) of
+ false ->
+ ErrText = "Recipient is not in the conference room",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ ToNick),
+ From, Err);
+ ToJIDs ->
+ SrcIsVisitor = is_visitor(From, StateData),
+ DstIsModerator = is_moderator(hd(ToJIDs), StateData),
+ PmFromVisitors = (StateData#state.config)#config.allow_private_messages_from_visitors,
+ if SrcIsVisitor == false;
+ PmFromVisitors == anyone;
+ (PmFromVisitors == moderators) and (DstIsModerator) ->
+ {ok, #user{nick = FromNick}} =
+ ?DICT:find(jlib:jid_tolower(From),
+ StateData#state.users),
+ FromNickJID = jlib:jid_replace_resource(StateData#state.jid, FromNick),
+ [route_stanza(FromNickJID, ToJID, Packet) || ToJID <- ToJIDs];
+ true ->
+ ErrText = "It is not allowed to send private messages",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ ToNick),
+ From, Err)
+ end
+ 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)),
+ route_stanza(
+ 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)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ ToNick),
+ From, Err)
+ end,
+ {next_state, normal_state, StateData}
+ end;
+
+normal_state({route, From, ToNick,
+ {xmlelement, "iq", Attrs, _Els} = Packet},
+ StateData) ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ StanzaId = xml:get_attr_s("id", Attrs),
+ case {(StateData#state.config)#config.allow_query_users,
+ is_user_online_iq(StanzaId, From, StateData)} of
+ {true, {true, NewId, FromFull}} ->
+ case find_jid_by_nick(ToNick, StateData) of
+ false ->
+ case jlib:iq_query_info(Packet) of
+ reply ->
+ ok;
+ _ ->
+ ErrText = "Recipient is not in the conference room",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid, ToNick),
+ From, Err)
+ end;
+ ToJID ->
+ {ok, #user{nick = FromNick}} =
+ ?DICT:find(jlib:jid_tolower(FromFull),
+ StateData#state.users),
+ {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID,
+ StanzaId, NewId,Packet),
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, FromNick),
+ ToJID2, Packet2)
+ end;
+ {_, {false, _, _}} ->
+ case jlib:iq_query_info(Packet) of
+ reply ->
+ ok;
+ _ ->
+ ErrText = "Only occupants are allowed to send queries to the conference",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, ToNick),
+ From, Err)
+ end;
+ _ ->
+ case jlib:iq_query_info(Packet) of
+ reply ->
+ ok;
+ _ ->
+ ErrText = "Queries to the conference members are not allowed in this room",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_ALLOWED(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, ToNick),
+ From, Err)
+ end
+ end,
+ {next_state, normal_state, StateData};
+
+normal_state(_Event, StateData) ->
+ {next_state, normal_state, StateData}.
+
+
+
+%%----------------------------------------------------------------------
+%% Func: handle_event/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+handle_event({service_message, Msg}, _StateName, StateData) ->
+ MessagePkt = {xmlelement, "message",
+ [{"type", "groupchat"}],
+ [{xmlelement, "body", [], [{xmlcdata, Msg}]}]},
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ route_stanza(
+ StateData#state.jid,
+ Info#user.jid,
+ MessagePkt)
+ end,
+ ?DICT:to_list(StateData#state.users)),
+ NSD = add_message_to_history("",
+ StateData#state.jid,
+ MessagePkt,
+ StateData),
+ {next_state, normal_state, NSD};
+
+handle_event({destroy, Reason}, _StateName, StateData) ->
+ {result, [], stop} =
+ destroy_room(
+ {xmlelement, "destroy",
+ [{"xmlns", ?NS_MUC_OWNER}],
+ case Reason of
+ none -> [];
+ _Else ->
+ [{xmlelement, "reason",
+ [], [{xmlcdata, Reason}]}]
+ end}, StateData),
+ ?INFO_MSG("Destroyed MUC room ~s with reason: ~p",
+ [jlib:jid_to_string(StateData#state.jid), Reason]),
+ add_to_log(room_existence, destroyed, StateData),
+ {stop, shutdown, StateData};
+handle_event(destroy, StateName, StateData) ->
+ ?INFO_MSG("Destroyed MUC room ~s",
+ [jlib:jid_to_string(StateData#state.jid)]),
+ handle_event({destroy, none}, StateName, StateData);
+
+handle_event({set_affiliations, Affiliations}, StateName, StateData) ->
+ {next_state, StateName, StateData#state{affiliations = Affiliations}};
+
+handle_event(_Event, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: handle_sync_event/4
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {reply, Reply, NextStateName, NextStateData} |
+%% {reply, Reply, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData} |
+%% {stop, Reason, Reply, NewStateData}
+%%----------------------------------------------------------------------
+handle_sync_event({moderate_room_history, Nick}, _From, StateName, #state{history = History} = StateData) ->
+ NewHistory = lqueue_filter(fun({FromNick, _TSPacket, _HaveSubject, _Timestamp, _Size}) ->
+ FromNick /= Nick
+ end, History),
+ Moderated = History#lqueue.len - NewHistory#lqueue.len,
+ {reply, {ok, integer_to_list(Moderated)}, StateName, StateData#state{history = NewHistory}};
+
+handle_sync_event(persist_recent_messages, _From, StateName, StateData) ->
+ {reply, persist_muc_history(StateData), StateName, StateData};
+
+handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
+ Reply = get_roomdesc_reply(JID, StateData,
+ get_roomdesc_tail(StateData, Lang)),
+ {reply, Reply, StateName, StateData};
+handle_sync_event(get_config, _From, StateName, StateData) ->
+ {reply, {ok, StateData#state.config}, StateName, StateData};
+handle_sync_event(get_state, _From, StateName, StateData) ->
+ {reply, {ok, StateData}, 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}.
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+ {ok, StateName, StateData}.
+
+print_state(StateData) ->
+ StateData.
+
+%%----------------------------------------------------------------------
+%% Func: handle_info/3
+%% Returns: {next_state, NextStateName, NextStateData} |
+%% {next_state, NextStateName, NextStateData, Timeout} |
+%% {stop, Reason, NewStateData}
+%%----------------------------------------------------------------------
+handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
+ 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) ->
+ 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} ->
+ Activity = get_user_activity(From, StateData),
+ Packet = Activity#activity.message,
+ NewActivity = Activity#activity{message = undefined},
+ StateData1 =
+ store_user_activity(
+ From, NewActivity, StateData),
+ StateData2 =
+ StateData1#state{
+ room_queue = RoomQueue},
+ 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 =
+ store_user_activity(
+ From, NewActivity, StateData),
+ StateData2 =
+ StateData1#state{
+ room_queue = RoomQueue},
+ 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),
+ route_stanza( % 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({migrate, Node}, StateName, StateData) ->
+ if Node /= node() ->
+ {migrate, StateData,
+ {Node, ?MODULE, start, [StateName, StateData]}, 0};
+ true ->
+ {next_state, StateName, StateData}
+ end;
+handle_info('shutdown', _StateName, StateData) ->
+ {stop, 'shutdown', StateData};
+handle_info(_Info, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+%%----------------------------------------------------------------------
+%% Func: terminate/3
+%% Purpose: Shutdown the fsm
+%% Returns: any
+%%----------------------------------------------------------------------
+terminate({migrated, Clone}, _StateName, StateData) ->
+ ?INFO_MSG("Migrating room ~s@~s to ~p on node ~p",
+ [StateData#state.room, StateData#state.host,
+ Clone, node(Clone)]),
+ mod_muc:room_destroyed(StateData#state.host, StateData#state.room,
+ self(), StateData#state.server_host),
+ ok;
+terminate(Reason, _StateName, StateData) ->
+ ?INFO_MSG("Stopping MUC room ~s@~s",
+ [StateData#state.room, StateData#state.host]),
+ ReasonT = case Reason of
+ shutdown -> "You are being removed from the room because"
+ " of a system shutdown";
+ _ -> "Room terminates"
+ end,
+ ItemAttrs = [{"affiliation", "none"}, {"role", "none"}],
+ ReasonEl = {xmlelement, "reason", [], [{xmlcdata, ReasonT}]},
+ Packet = {xmlelement, "presence", [{"type", "unavailable"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs, [ReasonEl]},
+ {xmlelement, "status", [{"code", "332"}], []}
+ ]}]},
+ ?DICT:fold(
+ fun(LJID, Info, _) ->
+ Nick = Info#user.nick,
+ case Reason of
+ shutdown ->
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ Info#user.jid,
+ Packet);
+ _ -> ok
+ end,
+ tab_remove_online_user(LJID, StateData)
+ end, [], StateData#state.users),
+ add_to_log(room_existence, stopped, StateData),
+ if
+ Reason == 'shutdown' ->
+ persist_muc_history(StateData);
+ true ->
+ ok
+ end,
+ (StateData#state.mod):room_destroyed(
+ StateData#state.host, StateData#state.room, self(),
+ StateData#state.server_host),
+ ok.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+load_history(_Host, _Room, false, Queue) ->
+ Queue;
+load_history(Host, Room, true, Queue) ->
+ ?INFO_MSG("Loading history for room ~s on host ~s", [Room, Host]),
+ case odbc_queries:load_roomhistory(Host, ejabberd_odbc:escape(Room)) of
+ {selected, ["nick", "packet", "have_subject", "timestamp", "size"], Items} ->
+ ?DEBUG("Found ~p messages on history for ~s", [length(Items), Room]),
+ lists:foldl(fun(I, Q) ->
+ {Nick, XML, HS, Ts, Size} = I,
+ Item = {Nick,
+ xml_stream:parse_element(XML),
+ HS /= "0",
+ calendar:gregorian_seconds_to_datetime(list_to_integer(Ts)),
+ list_to_integer(Size)},
+ lqueue_in(Item, Q)
+ end, Queue, Items);
+ _ ->
+ Queue
+ end.
+
+
+persist_muc_history(#state{room = Room, server_host = Server, config = #config{persistent = true} ,persist_history = true, history = Q}) ->
+ ?INFO_MSG("Persisting history for room ~s on host ~s", [Room, Server]),
+ Queries = lists:map(fun({FromNick, Packet, HaveSubject, Timestamp, Size}) ->
+ odbc_queries:add_roomhistory_sql(
+ ejabberd_odbc:escape(Room),
+ ejabberd_odbc:escape(FromNick),
+ ejabberd_odbc:escape(xml:element_to_binary(Packet)),
+ atom_to_list(HaveSubject),
+ integer_to_list(calendar:datetime_to_gregorian_seconds(Timestamp)),
+ integer_to_list(Size))
+ end, lqueue_to_list(Q)),
+ odbc_queries:clear_and_add_roomhistory(Server,ejabberd_odbc:escape(Room), Queries),
+ {ok, {persisted, length(Queries)}};
+ %% en mod_muc, cuando se levantan los muc persistentes, si se crea, y el flag persist_history esta en true,
+ %% se levantan los mensajes persistentes tb.
+
+persist_muc_history(_) ->
+ {ok, not_persistent}.
+
+route(Pid, From, ToNick, Packet) ->
+ ?GEN_FSM:send_event(Pid, {route, 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) orelse
+ is_user_allowed_message_nonparticipant(From, StateData) of
+ true ->
+ {FromNick, Role} = get_participant_data(From, StateData),
+ if
+ (Role == moderator) or (Role == participant)
+ or ((StateData#state.config)#config.moderated == false) ->
+ {NewStateData1, IsAllowed} =
+ case check_subject(Packet) of
+ false ->
+ {StateData, true};
+ Subject ->
+ case can_change_subject(Role,
+ StateData) of
+ true ->
+ NSD =
+ StateData#state{
+ subject = Subject,
+ subject_author =
+ FromNick},
+ case (NSD#state.config)#config.persistent of
+ true ->
+ (NSD#state.mod):store_room(
+ NSD#state.server_host,
+ NSD#state.host,
+ NSD#state.room,
+ make_opts(NSD));
+ _ ->
+ ok
+ end,
+ {NSD, true};
+ _ ->
+ {StateData, false}
+ end
+ end,
+ case IsAllowed of
+ true ->
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ FromNick),
+ Info#user.jid,
+ Packet)
+ end,
+ ?DICT:to_list(StateData#state.users)),
+ NewStateData2 =
+ add_message_to_history(FromNick,
+ From,
+ Packet,
+ NewStateData1),
+ {next_state, normal_state, NewStateData2};
+ _ ->
+ Err =
+ case (StateData#state.config)#config.allow_change_subj of
+ true ->
+ ?ERRT_FORBIDDEN(
+ Lang,
+ "Only moderators and participants "
+ "are allowed to change the subject in this room");
+ _ ->
+ ?ERRT_FORBIDDEN(
+ Lang,
+ "Only moderators "
+ "are allowed to change the subject in this room")
+ end,
+ route_stanza(
+ StateData#state.jid,
+ From,
+ jlib:make_error_reply(Packet, Err)),
+ {next_state, normal_state, StateData}
+ end;
+ true ->
+ ErrText = "Visitors are not allowed to send messages to all occupants",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_FORBIDDEN(Lang, ErrText)),
+ route_stanza(
+ StateData#state.jid,
+ From, Err),
+ {next_state, normal_state, StateData}
+ end;
+ false ->
+ ErrText = "Only occupants are allowed to send messages to the conference",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
+ route_stanza(StateData#state.jid, From, Err),
+ {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.
+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),
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ StateData1 =
+ case Type of
+ "unavailable" ->
+ case is_user_online(From, StateData) of
+ true ->
+ NewPacket = case {(StateData#state.config)#config.allow_visitor_status,
+ is_visitor(From, StateData)} of
+ {false, true} ->
+ strip_status(Packet);
+ _ ->
+ Packet
+ end,
+ NewState =
+ add_user_presence_un(From, NewPacket, StateData),
+ case ?DICT:find(Nick, StateData#state.nicks) of
+ {ok, [_, _ | _]} -> ok;
+ _ -> send_new_presence(From, NewState)
+ end,
+ Reason = case xml:get_subtag(NewPacket, "status") of
+ false -> "";
+ Status_el -> xml:get_tag_cdata(Status_el)
+ end,
+ remove_online_user(From, NewState, Reason);
+ _ ->
+ StateData
+ end;
+ "error" ->
+ case is_user_online(From, StateData) of
+ true ->
+ ErrorText = "This participant is kicked from the room because "
+ "he sent an error presence",
+ expulse_participant(Packet, From, StateData,
+ translate:translate(Lang, ErrorText));
+ _ ->
+ StateData
+ end;
+ "" ->
+ case is_user_online(From, StateData) of
+ true ->
+ case is_nick_change(From, Nick, StateData) of
+ true ->
+ case {nick_collision(From, Nick, StateData),
+ (StateData#state.mod):can_use_nick(
+ StateData#state.server_host,
+ 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)),
+ route_stanza(
+ % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ Nick),
+ From, Err),
+ StateData;
+ {true, _, _} ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ ErrText = "That nickname is already in use by another occupant",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_CONFLICT(Lang, ErrText)),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ Nick), % TODO: s/Nick/""/
+ From, Err),
+ StateData;
+ {_, false, _} ->
+ ErrText = "That nickname is registered by another person",
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERRT_CONFLICT(Lang, ErrText)),
+ route_stanza(
+ % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid,
+ Nick),
+ From, Err),
+ StateData;
+ _ ->
+ change_nick(From, Nick, StateData)
+ end;
+ _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)
+ end;
+ _ ->
+ StateData
+ end,
+ case (not (StateData1#state.config)#config.persistent) andalso
+ (?DICT:to_list(StateData1#state.users) == []) of
+ true ->
+ ?INFO_MSG("Destroyed MUC room ~s because it's temporary and empty",
+ [jlib:jid_to_string(StateData#state.jid)]),
+ add_to_log(room_existence, destroyed, StateData),
+ {stop, normal, StateData1};
+ _ ->
+ {next_state, normal_state, StateData1}
+ end.
+
+is_user_online(JID, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ ?DICT:is_key(LJID, StateData#state.users).
+
+%% Check if the user is occupant of the room, or at least is an admin or owner.
+is_occupant_or_admin(JID, StateData) ->
+ FAffiliation = get_affiliation(JID, StateData),
+ FRole = get_role(JID, StateData),
+ case (FRole /= none) orelse
+ (FAffiliation == admin) orelse
+ (FAffiliation == owner) of
+ true ->
+ true;
+ _ ->
+ false
+ end.
+
+%%%
+%%% Handle IQ queries of vCard
+%%%
+is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource /= "" ->
+ {is_user_online(JID, StateData), StanzaId, JID};
+is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == "" ->
+ try stanzaid_unpack(StanzaId) of
+ {OriginalId, Resource} ->
+ JIDWithResource = jlib:jid_replace_resource(JID, Resource),
+ {is_user_online(JIDWithResource, StateData),
+ OriginalId, JIDWithResource}
+ catch
+ _:_ ->
+ {is_user_online(JID, StateData), StanzaId, JID}
+ end.
+
+handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) ->
+ ToBareJID = jlib:jid_remove_resource(ToJID),
+ IQ = jlib:iq_query_info(Packet),
+ handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, NewId, IQ, Packet).
+handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, _NewId,
+ #iq{type = get, xmlns = ?NS_VCARD}, Packet)
+ when ToBareJID /= ToJID ->
+ {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)};
+handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, _StanzaId, NewId, _IQ, Packet) ->
+ {ToJID, change_stanzaid(NewId, Packet)}.
+
+stanzaid_pack(OriginalId, Resource) ->
+ "berd"++base64:encode_to_string("ejab\0" ++ OriginalId ++ "\0" ++ Resource).
+stanzaid_unpack("berd"++StanzaIdBase64) ->
+ StanzaId = base64:decode_to_string(StanzaIdBase64),
+ ["ejab", OriginalId, Resource] = string:tokens(StanzaId, "\0"),
+ {OriginalId, Resource}.
+
+change_stanzaid(NewId, Packet) ->
+ {xmlelement, Name, Attrs, Els} = jlib:remove_attr("id", Packet),
+ {xmlelement, Name, [{"id", NewId} | Attrs], Els}.
+change_stanzaid(PreviousId, ToJID, Packet) ->
+ NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource),
+ change_stanzaid(NewId, Packet).
+%%%
+%%%
+
+role_to_list(Role) ->
+ case Role of
+ moderator -> "moderator";
+ participant -> "participant";
+ visitor -> "visitor";
+ none -> "none"
+ end.
+
+affiliation_to_list(Affiliation) ->
+ case Affiliation of
+ owner -> "owner";
+ admin -> "admin";
+ member -> "member";
+ outcast -> "outcast";
+ none -> "none"
+ end.
+
+list_to_role(Role) ->
+ case Role of
+ "moderator" -> moderator;
+ "participant" -> participant;
+ "visitor" -> visitor;
+ "none" -> none
+ end.
+
+list_to_affiliation(Affiliation) ->
+ case Affiliation of
+ "owner" -> owner;
+ "admin" -> admin;
+ "member" -> member;
+ "outcast" -> outcast;
+ "none" -> none
+ end.
+
+%% Decide the fate of the message and its sender
+%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
+decide_fate_message("error", Packet, From, StateData) ->
+ %% Make a preliminary decision
+ PD = case check_error_kick(Packet) of
+ %% If this is an error stanza and its condition matches a criteria
+ true ->
+ Reason = io_lib:format("This participant is considered a ghost and is expulsed: ~s",
+ [jlib:jid_to_string(From)]),
+ {expulse_sender, Reason};
+ false ->
+ continue_delivery
+ end,
+ case PD of
+ {expulse_sender, R} ->
+ case is_user_online(From, StateData) of
+ true ->
+ {expulse_sender, R};
+ false ->
+ forget_message
+ end;
+ Other ->
+ Other
+ end;
+
+decide_fate_message(_, _, _, _) ->
+ continue_delivery.
+
+%% Check if the elements of this error stanza indicate
+%% that the sender is a dead participant.
+%% If so, return true to kick the participant.
+check_error_kick(Packet) ->
+ case get_error_condition(Packet) of
+ "gone" -> true;
+ "internal-server-error" -> true;
+ "item-not-found" -> true;
+ "jid-malformed" -> true;
+ "recipient-unavailable" -> true;
+ "redirect" -> true;
+ "remote-server-not-found" -> true;
+ "remote-server-timeout" -> true;
+ "service-unavailable" -> true;
+ _ -> false
+ end.
+
+get_error_condition(Packet) ->
+ case catch get_error_condition2(Packet) of
+ {condition, ErrorCondition} ->
+ ErrorCondition;
+ {'EXIT', _} ->
+ "badformed error stanza"
+ end.
+get_error_condition2(Packet) ->
+ {xmlelement, _, _, EEls} = xml:get_subtag(Packet, "error"),
+ [Condition] = [Name || {xmlelement, Name, [{"xmlns", ?NS_STANZAS}], []} <- EEls],
+ {condition, Condition}.
+
+expulse_participant(Packet, From, StateData, Reason1) ->
+ ErrorCondition = get_error_condition(Packet),
+ Reason2 = io_lib:format(Reason1 ++ ": " ++ "~s", [ErrorCondition]),
+ NewState = add_user_presence_un(
+ From,
+ {xmlelement, "presence",
+ [{"type", "unavailable"}],
+ [{xmlelement, "status", [],
+ [{xmlcdata, Reason2}]
+ }]},
+ StateData),
+ send_new_presence(From, NewState),
+ remove_online_user(From, NewState).
+
+
+set_affiliation(JID, Affiliation, StateData) ->
+ set_affiliation(JID, Affiliation, StateData, "").
+
+set_affiliation(JID, Affiliation, StateData, Reason) ->
+ LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)),
+ Affiliations = case Affiliation of
+ none ->
+ ?DICT:erase(LJID,
+ StateData#state.affiliations);
+ _ ->
+ ?DICT:store(LJID,
+ {Affiliation, Reason},
+ StateData#state.affiliations)
+ end,
+ StateData#state{affiliations = Affiliations}.
+
+get_affiliation(JID, StateData) ->
+ {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = StateData#state.access,
+ Res =
+ case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of
+ allow ->
+ owner;
+ _ ->
+ LJID = jlib:jid_tolower(JID),
+ case ?DICT:find(LJID, StateData#state.affiliations) of
+ {ok, Affiliation} ->
+ Affiliation;
+ _ ->
+ LJID1 = jlib:jid_remove_resource(LJID),
+ case ?DICT:find(LJID1, StateData#state.affiliations) of
+ {ok, Affiliation} ->
+ Affiliation;
+ _ ->
+ LJID2 = setelement(1, LJID, ""),
+ case ?DICT:find(LJID2, StateData#state.affiliations) of
+ {ok, Affiliation} ->
+ Affiliation;
+ _ ->
+ LJID3 = jlib:jid_remove_resource(LJID2),
+ case ?DICT:find(LJID3, StateData#state.affiliations) of
+ {ok, Affiliation} ->
+ Affiliation;
+ _ ->
+ none
+ end
+ end
+ end
+ end
+ end,
+ case Res of
+ {A, _Reason} ->
+ A;
+ _ ->
+ Res
+ end.
+
+get_service_affiliation(JID, StateData) ->
+ {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} =
+ StateData#state.access,
+ case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of
+ allow ->
+ owner;
+ _ ->
+ none
+ end.
+
+set_role(JID, Role, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ LJIDs = case LJID of
+ {U, S, ""} ->
+ ?DICT:fold(
+ fun(J, _, Js) ->
+ case J of
+ {U, S, _} ->
+ [J | Js];
+ _ ->
+ Js
+ end
+ end, [], StateData#state.users);
+ _ ->
+ case ?DICT:is_key(LJID, StateData#state.users) of
+ true ->
+ [LJID];
+ _ ->
+ []
+ end
+ end,
+ {Users, Nicks}
+ = case Role of
+ none ->
+ lists:foldl(fun(J, {Us, Ns}) ->
+ NewNs =
+ case ?DICT:find(J, Us) of
+ {ok, #user{nick = Nick}} ->
+ ?DICT:erase(Nick, Ns);
+ _ ->
+ Ns
+ end,
+ {?DICT:erase(J, Us), NewNs}
+ end,
+ {StateData#state.users, StateData#state.nicks},
+ LJIDs);
+ _ ->
+ {lists:foldl(fun(J, Us) ->
+ {ok, User} = ?DICT:find(J, Us),
+ ?DICT:store(J,
+ User#user{role = Role},
+ Us)
+ end, StateData#state.users, LJIDs),
+ StateData#state.nicks}
+ end,
+ StateData#state{users = Users, nicks = Nicks}.
+
+get_role(JID, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ case ?DICT:find(LJID, StateData#state.users) of
+ {ok, #user{role = Role}} ->
+ Role;
+ _ ->
+ none
+ end.
+
+get_default_role(Affiliation, StateData) ->
+ case Affiliation of
+ owner -> moderator;
+ admin -> moderator;
+ member -> participant;
+ outcast -> none;
+ none ->
+ case (StateData#state.config)#config.members_only of
+ true ->
+ none;
+ _ ->
+ case (StateData#state.config)#config.members_by_default of
+ true ->
+ participant;
+ _ ->
+ visitor
+ end
+ end
+ end.
+
+is_visitor(Jid, StateData) ->
+ get_role(Jid, StateData) =:= visitor.
+
+is_moderator(Jid, StateData) ->
+ get_role(Jid, StateData) =:= moderator.
+
+get_max_users(StateData) ->
+ MaxUsers = (StateData#state.config)#config.max_users,
+ ServiceMaxUsers = get_service_max_users(StateData),
+ if
+ MaxUsers =< ServiceMaxUsers -> MaxUsers;
+ true -> ServiceMaxUsers
+ end.
+
+get_service_max_users(StateData) ->
+ gen_mod:get_module_opt(StateData#state.server_host,
+ StateData#state.mod, max_users, ?MAX_USERS_DEFAULT).
+
+get_max_users_admin_threshold(StateData) ->
+ gen_mod:get_module_opt(StateData#state.server_host,
+ StateData#state.mod, max_users_admin_threshold, 5).
+
+get_user_activity(JID, StateData) ->
+ case treap:lookup(jlib:jid_tolower(JID),
+ StateData#state.activity) of
+ {ok, _P, A} -> A;
+ error ->
+ MessageShaper =
+ shaper:new(gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, user_message_shaper, none)),
+ PresenceShaper =
+ shaper:new(gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, user_presence_shaper, none)),
+ #activity{message_shaper = MessageShaper,
+ presence_shaper = PresenceShaper}
+ end.
+
+store_user_activity(JID, UserActivity, StateData) ->
+ MinMessageInterval =
+ gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, min_message_interval, 0),
+ MinPresenceInterval =
+ gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, 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} ->
+ Activity = get_user_activity(From, StateData),
+ Packet = Activity#activity.message,
+ Size = element_size(Packet),
+ {RoomShaper, RoomShaperInterval} =
+ shaper:update(StateData#state.room_shaper, Size),
+ erlang:send_after(
+ RoomShaperInterval, self(),
+ process_room_queue),
+ StateData#state{
+ room_shaper = RoomShaper};
+ {{value, {presence, From}}, _RoomQueue} ->
+ Activity = get_user_activity(From, StateData),
+ {_Nick, Packet} = Activity#activity.presence,
+ Size = element_size(Packet),
+ {RoomShaper, RoomShaperInterval} =
+ shaper:update(StateData#state.room_shaper, Size),
+ erlang:send_after(
+ RoomShaperInterval, self(),
+ process_room_queue),
+ StateData#state{
+ room_shaper = RoomShaper};
+ {empty, _} ->
+ StateData
+ end.
+
+
+add_online_user(JID, Nick, Role, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ Users = ?DICT:store(LJID,
+ #user{jid = JID,
+ nick = Nick,
+ role = Role},
+ StateData#state.users),
+ add_to_log(join, Nick, StateData),
+ Nicks = ?DICT:update(Nick,
+ fun(Entry) ->
+ case lists:member(LJID, Entry) of
+ true ->
+ Entry;
+ false ->
+ [LJID|Entry]
+ end
+ end,
+ [LJID],
+ StateData#state.nicks),
+ tab_add_online_user(JID, StateData),
+ StateData#state{users = Users, nicks = Nicks}.
+
+remove_online_user(JID, StateData) ->
+ remove_online_user(JID, StateData, "").
+
+remove_online_user(JID, StateData, Reason) ->
+ LJID = jlib:jid_tolower(JID),
+ {ok, #user{nick = Nick}} =
+ ?DICT:find(LJID, StateData#state.users),
+ add_to_log(leave, {Nick, Reason}, StateData),
+ tab_remove_online_user(JID, StateData),
+ Users = ?DICT:erase(LJID, StateData#state.users),
+ Nicks = case ?DICT:find(Nick, StateData#state.nicks) of
+ {ok, [LJID]} ->
+ ?DICT:erase(Nick, StateData#state.nicks);
+ {ok, U} ->
+ ?DICT:store(Nick, U -- [LJID], StateData#state.nicks);
+ error ->
+ StateData#state.nicks
+ end,
+ StateData#state{users = Users, nicks = Nicks}.
+
+
+filter_presence({xmlelement, "presence", Attrs, Els}) ->
+ FEls = lists:filter(
+ fun(El) ->
+ case El of
+ {xmlcdata, _} ->
+ false;
+ {xmlelement, _Name1, Attrs1, _Els1} ->
+ XMLNS = xml:get_attr_s("xmlns", Attrs1),
+ case XMLNS of
+ ?NS_MUC ++ _ ->
+ false;
+ _ ->
+ true
+ end
+ end
+ 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),
+ FPresence = filter_presence(Presence),
+ Users =
+ ?DICT:update(
+ LJID,
+ fun(#user{} = User) ->
+ User#user{last_presence = FPresence}
+ end, StateData#state.users),
+ StateData#state{users = Users}.
+
+add_user_presence_un(JID, Presence, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ FPresence = filter_presence(Presence),
+ Users =
+ ?DICT:update(
+ LJID,
+ fun(#user{} = User) ->
+ User#user{last_presence = FPresence,
+ role = none}
+ end, StateData#state.users),
+ StateData#state{users = Users}.
+
+
+%% Find and return a list of the full JIDs of the users of Nick.
+%% Return jid record.
+find_jids_by_nick(Nick, StateData) ->
+ case ?DICT:find(Nick, StateData#state.nicks) of
+ {ok, [User]} ->
+ [jlib:make_jid(User)];
+ {ok, Users} ->
+ [jlib:make_jid(LJID) || LJID <- Users];
+ error ->
+ false
+ end.
+
+%% Find and return the full JID of the user of Nick with
+%% highest-priority presence. Return jid record.
+find_jid_by_nick(Nick, StateData) ->
+ case ?DICT:find(Nick, StateData#state.nicks) of
+ {ok, [User]} ->
+ jlib:make_jid(User);
+ {ok, [FirstUser|Users]} ->
+ #user{last_presence = FirstPresence} =
+ ?DICT:fetch(FirstUser, StateData#state.users),
+ {LJID, _} =
+ lists:foldl(fun(Compare, {HighestUser, HighestPresence}) ->
+ #user{last_presence = P1} =
+ ?DICT:fetch(Compare, StateData#state.users),
+ case higher_presence(P1, HighestPresence) of
+ true ->
+ {Compare, P1};
+ false ->
+ {HighestUser, HighestPresence}
+ end
+ end, {FirstUser, FirstPresence}, Users),
+ jlib:make_jid(LJID);
+ error ->
+ false
+ end.
+
+higher_presence(Pres1, Pres2) ->
+ Pri1 = get_priority_from_presence(Pres1),
+ Pri2 = get_priority_from_presence(Pres2),
+ Pri1 > Pri2.
+
+get_priority_from_presence(PresencePacket) ->
+ case xml:get_subtag(PresencePacket, "priority") of
+ false ->
+ 0;
+ SubEl ->
+ case catch list_to_integer(xml:get_tag_cdata(SubEl)) of
+ P when is_integer(P) ->
+ P;
+ _ ->
+ 0
+ end
+ end.
+
+find_nick_by_jid(Jid, StateData) ->
+ [{_, #user{nick = Nick}}] = lists:filter(
+ fun({_, #user{jid = FJid}}) -> FJid == Jid end,
+ ?DICT:to_list(StateData#state.users)),
+ Nick.
+
+is_nick_change(JID, Nick, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ case Nick of
+ "" ->
+ false;
+ _ ->
+ {ok, #user{nick = OldNick}} =
+ ?DICT:find(LJID, StateData#state.users),
+ Nick /= OldNick
+ end.
+
+nick_collision(User, Nick, StateData) ->
+ UserOfNick = find_jid_by_nick(Nick, StateData),
+ %% if nick is not used, or is used by another resource of the same
+ %% user, it's ok.
+ UserOfNick /= false andalso
+ jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick)) /=
+ jlib:jid_remove_resource(jlib:jid_tolower(User)).
+
+add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
+ Lang = xml:get_attr_s("xml:lang", Attrs),
+ MaxUsers = get_max_users(StateData),
+ MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData),
+ NUsers = dict:fold(fun(_, _, Acc) -> Acc + 1 end, 0,
+ StateData#state.users),
+ Affiliation = get_affiliation(From, StateData),
+ ServiceAffiliation = get_service_affiliation(From, StateData),
+ NConferences = tab_count_user(From),
+ MaxConferences = gen_mod:get_module_opt(
+ StateData#state.server_host,
+ StateData#state.mod, max_user_conferences, 10),
+ Collision = nick_collision(From, Nick, StateData),
+ case {(ServiceAffiliation == owner orelse
+ ((Affiliation == admin orelse Affiliation == owner) andalso
+ NUsers < MaxAdminUsers) orelse
+ NUsers < MaxUsers) andalso
+ NConferences < MaxConferences,
+ Collision,
+ (StateData#state.mod):can_use_nick(StateData#state.server_host,
+ StateData#state.host, From, Nick),
+ get_default_role(Affiliation, StateData)} of
+ {false, _, _, _} ->
+ % max user reached and user is not admin or owner
+ Err = jlib:make_error_reply(
+ Packet,
+ ?ERR_SERVICE_UNAVAILABLE),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ {_, _, _, none} ->
+ Err = jlib:make_error_reply(
+ Packet,
+ case Affiliation of
+ outcast ->
+ ErrText = "You have been banned from this room",
+ ?ERRT_FORBIDDEN(Lang, ErrText);
+ _ ->
+ ErrText = "Membership is required to enter this room",
+ ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText)
+ end),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ {_, true, _, _} ->
+ ErrText = "That nickname is already in use by another occupant",
+ Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
+ route_stanza(
+ % TODO: s/Nick/""/
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ {_, _, false, _} ->
+ ErrText = "That nickname is registered by another person",
+ Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)),
+ route_stanza(
+ % TODO: s/Nick/""/
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ {_, _, _, Role} ->
+ case check_password(ServiceAffiliation, Affiliation,
+ Els, From, StateData) of
+ true ->
+ NewState =
+ add_user_presence(
+ From, Packet,
+ add_online_user(From, Nick, Role, StateData)),
+ if not (NewState#state.config)#config.anonymous ->
+ WPacket = {xmlelement, "message", [{"type", "groupchat"}],
+ [{xmlelement, "body", [],
+ [{xmlcdata, translate:translate(
+ Lang,
+ "This room is not anonymous")}]},
+ {xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "status", [{"code", "100"}], []}]}]},
+ route_stanza(
+ StateData#state.jid,
+ From, WPacket);
+ true ->
+ ok
+ end,
+ send_existing_presences(From, NewState),
+ send_new_presence(From, NewState),
+ Shift = count_stanza_shift(Nick, Els, NewState),
+ case send_history(From, Shift, NewState) of
+ true ->
+ ok;
+ _ ->
+ send_subject(From, Lang, StateData)
+ end,
+ case NewState#state.just_created of
+ true ->
+ NewState#state{just_created = false};
+ false ->
+ Robots = ?DICT:erase(From, StateData#state.robots),
+ NewState#state{robots = Robots}
+ end;
+ nopass ->
+ ErrText = "A password is required to enter this room",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ captcha_required ->
+ SID = xml:get_attr_s("id", Attrs),
+ RoomJID = StateData#state.jid,
+ To = jlib:jid_replace_resource(RoomJID, Nick),
+ Limiter = {From#jid.luser, From#jid.lserver},
+ case ejabberd_captcha:create_captcha(
+ SID, RoomJID, To, Lang, Limiter, From) of
+ {ok, ID, CaptchaEls} ->
+ MsgPkt = {xmlelement, "message", [{"id", ID}], CaptchaEls},
+ Robots = ?DICT:store(From,
+ {Nick, Packet}, StateData#state.robots),
+ route_stanza(RoomJID, From, MsgPkt),
+ StateData#state{robots = Robots};
+ {error, limit} ->
+ ErrText = "Too many CAPTCHA requests",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid, Nick),
+ From, Err),
+ StateData;
+ _ ->
+ ErrText = "Unable to generate a CAPTCHA",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid, Nick),
+ From, Err),
+ StateData
+ end;
+ _ ->
+ ErrText = "Incorrect password",
+ Err = jlib:make_error_reply(
+ Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)),
+ route_stanza( % TODO: s/Nick/""/
+ jlib:jid_replace_resource(
+ StateData#state.jid, Nick),
+ From, Err),
+ StateData
+ end
+ end.
+
+check_password(owner, _Affiliation, _Els, _From, _StateData) ->
+ %% Don't check pass if user is owner in MUC service (access_admin option)
+ true;
+check_password(_ServiceAffiliation, Affiliation, Els, From, StateData) ->
+ case (StateData#state.config)#config.password_protected of
+ false ->
+ check_captcha(Affiliation, From, StateData);
+ true ->
+ Pass = extract_password(Els),
+ case Pass of
+ false ->
+ nopass;
+ _ ->
+ case (StateData#state.config)#config.password of
+ Pass ->
+ true;
+ _ ->
+ 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;
+ _ ->
+ WList = (StateData#state.config)#config.captcha_whitelist,
+ #jid{luser = U, lserver = S, lresource = R} = From,
+ case ?SETS:is_element({U, S, R}, WList) of
+ true ->
+ true;
+ false ->
+ case ?SETS:is_element({U, S, ""}, WList) of
+ true ->
+ true;
+ false ->
+ case ?SETS:is_element({"", S, ""}, WList) of
+ true ->
+ true;
+ false ->
+ captcha_required
+ end
+ end
+ end
+ end;
+ _ ->
+ true
+ end.
+
+extract_password([]) ->
+ false;
+extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_MUC ->
+ case xml:get_subtag(El, "password") of
+ false ->
+ false;
+ SubEl ->
+ xml:get_tag_cdata(SubEl)
+ end;
+ _ ->
+ extract_password(Els)
+ end;
+extract_password([_ | Els]) ->
+ extract_password(Els).
+
+count_stanza_shift(Nick, Els, StateData) ->
+ HL = lqueue_to_list(StateData#state.history),
+ Since = extract_history(Els, "since"),
+ Shift0 = case Since of
+ false ->
+ 0;
+ _ ->
+ Sin = calendar:datetime_to_gregorian_seconds(Since),
+ count_seconds_shift(Sin, HL)
+ end,
+ Seconds = extract_history(Els, "seconds"),
+ Shift1 = case Seconds of
+ false ->
+ 0;
+ _ ->
+ Sec = calendar:datetime_to_gregorian_seconds(
+ calendar:now_to_universal_time(now())) - Seconds,
+ count_seconds_shift(Sec, HL)
+ end,
+ MaxStanzas = extract_history(Els, "maxstanzas"),
+ Shift2 = case MaxStanzas of
+ false ->
+ 0;
+ _ ->
+ count_maxstanzas_shift(MaxStanzas, HL)
+ end,
+ MaxChars = extract_history(Els, "maxchars"),
+ Shift3 = case MaxChars of
+ false ->
+ 0;
+ _ ->
+ count_maxchars_shift(Nick, MaxChars, HL)
+ end,
+ lists:max([Shift0, Shift1, Shift2, Shift3]).
+
+count_seconds_shift(Seconds, HistoryList) ->
+ lists:sum(
+ lists:map(
+ fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) ->
+ T = calendar:datetime_to_gregorian_seconds(TimeStamp),
+ if
+ T < Seconds ->
+ 1;
+ true ->
+ 0
+ end
+ end, HistoryList)).
+
+count_maxstanzas_shift(MaxStanzas, HistoryList) ->
+ S = length(HistoryList) - MaxStanzas,
+ if
+ S =< 0 ->
+ 0;
+ true ->
+ S
+ end.
+
+count_maxchars_shift(Nick, MaxSize, HistoryList) ->
+ NLen = string:len(Nick) + 1,
+ Sizes = lists:map(
+ fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) ->
+ Size + NLen
+ end, HistoryList),
+ calc_shift(MaxSize, Sizes).
+
+calc_shift(MaxSize, Sizes) ->
+ Total = lists:sum(Sizes),
+ calc_shift(MaxSize, Total, 0, Sizes).
+
+calc_shift(_MaxSize, _Size, Shift, []) ->
+ Shift;
+calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
+ if
+ MaxSize >= Size ->
+ Shift;
+ true ->
+ calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
+ end.
+
+extract_history([], _Type) ->
+ false;
+extract_history([{xmlelement, _Name, Attrs, _SubEls} = El | Els], Type) ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_MUC ->
+ AttrVal = xml:get_path_s(El,
+ [{elem, "history"}, {attr, Type}]),
+ case Type of
+ "since" ->
+ case jlib:datetime_string_to_timestamp(AttrVal) of
+ undefined ->
+ false;
+ TS ->
+ calendar:now_to_universal_time(TS)
+ end;
+ _ ->
+ case catch list_to_integer(AttrVal) of
+ IntVal when is_integer(IntVal) and (IntVal >= 0) ->
+ IntVal;
+ _ ->
+ false
+ end
+ end;
+ _ ->
+ extract_history(Els, Type)
+ end;
+extract_history([_ | Els], Type) ->
+ 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, ""} ->
+ ?DICT:fold(
+ fun(J, _, Js) ->
+ case J of
+ {U, S, _} ->
+ [J | Js];
+ _ ->
+ Js
+ end
+ end, [], StateData#state.users);
+ _ ->
+ case ?DICT:is_key(LJID, StateData#state.users) of
+ true ->
+ [LJID];
+ _ ->
+ []
+ end
+ end,
+ lists:foreach(fun(J) ->
+ send_new_presence(J, Reason, StateData)
+ end, LJIDs).
+
+send_new_presence(NJID, StateData) ->
+ send_new_presence(NJID, "", StateData).
+
+send_new_presence(NJID, Reason, StateData) ->
+ %% First, find the nick associated with this JID.
+ #user{nick = Nick} = ?DICT:fetch(jlib:jid_tolower(NJID), StateData#state.users),
+ %% Then find the JID using this nick with highest priority.
+ LJID = find_jid_by_nick(Nick, StateData),
+ %% Then we get the presence data we're supposed to send.
+ {ok, #user{jid = RealJID,
+ role = Role,
+ last_presence = Presence}} =
+ ?DICT:find(jlib:jid_tolower(LJID), StateData#state.users),
+ Affiliation = get_affiliation(LJID, StateData),
+ SAffiliation = affiliation_to_list(Affiliation),
+ SRole = role_to_list(Role),
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ ItemAttrs =
+ case (Info#user.role == moderator) orelse
+ ((StateData#state.config)#config.anonymous == false) of
+ true ->
+ [{"jid", jlib:jid_to_string(RealJID)},
+ {"affiliation", SAffiliation},
+ {"role", SRole}];
+ _ ->
+ [{"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,
+ Status2 = case ((StateData#state.config)#config.anonymous==false)
+ andalso (NJID == Info#user.jid) of
+ true ->
+ [{xmlelement, "status", [{"code", "100"}], []}
+ | Status];
+ false ->
+ Status
+ end,
+ Status3 = case NJID == Info#user.jid of
+ true ->
+ [{xmlelement, "status", [{"code", "110"}], []}
+ | Status2];
+ false ->
+ Status2
+ end,
+ Packet = xml:append_subtags(
+ Presence,
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs, ItemEls} | Status3]}]),
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ Info#user.jid,
+ Packet)
+ end, ?DICT:to_list(StateData#state.users)).
+
+
+send_existing_presences(ToJID, StateData) ->
+ LToJID = jlib:jid_tolower(ToJID),
+ {ok, #user{jid = RealToJID,
+ role = Role}} =
+ ?DICT:find(LToJID, StateData#state.users),
+ lists:foreach(
+ fun({FromNick, _Users}) ->
+ LJID = find_jid_by_nick(FromNick, StateData),
+ #user{jid = FromJID,
+ role = FromRole,
+ last_presence = Presence
+ } = ?DICT:fetch(jlib:jid_tolower(LJID), StateData#state.users),
+ case RealToJID of
+ FromJID ->
+ ok;
+ _ ->
+ FromAffiliation = get_affiliation(LJID, StateData),
+ ItemAttrs =
+ case (Role == moderator) orelse
+ ((StateData#state.config)#config.anonymous ==
+ false) of
+ true ->
+ [{"jid", jlib:jid_to_string(FromJID)},
+ {"affiliation",
+ affiliation_to_list(FromAffiliation)},
+ {"role", role_to_list(FromRole)}];
+ _ ->
+ [{"affiliation",
+ affiliation_to_list(FromAffiliation)},
+ {"role", role_to_list(FromRole)}]
+ end,
+ Packet = xml:append_subtags(
+ Presence,
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs, []}]}]),
+ route_stanza(
+ jlib:jid_replace_resource(
+ StateData#state.jid, FromNick),
+ RealToJID,
+ Packet)
+ end
+ end, ?DICT:to_list(StateData#state.nicks)).
+
+
+now_to_usec({MSec, Sec, USec}) ->
+ (MSec*1000000 + Sec)*1000000 + USec.
+
+
+change_nick(JID, Nick, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ {ok, #user{nick = OldNick}} =
+ ?DICT:find(LJID, StateData#state.users),
+ Users =
+ ?DICT:update(
+ LJID,
+ fun(#user{} = User) ->
+ User#user{nick = Nick}
+ end, StateData#state.users),
+ OldNickUsers = ?DICT:fetch(OldNick, StateData#state.nicks),
+ NewNickUsers = case ?DICT:find(Nick, StateData#state.nicks) of
+ {ok, U} -> U;
+ error -> []
+ end,
+ %% Send unavailable presence from the old nick if it's no longer
+ %% used.
+ SendOldUnavailable = length(OldNickUsers) == 1,
+ %% If we send unavailable presence from the old nick, we should
+ %% probably send presence from the new nick, in order not to
+ %% confuse clients. Otherwise, do it only if the new nick was
+ %% unused.
+ SendNewAvailable = SendOldUnavailable orelse
+ NewNickUsers == [],
+ Nicks =
+ case OldNickUsers of
+ [LJID] ->
+ ?DICT:store(Nick, [LJID|NewNickUsers],
+ ?DICT:erase(OldNick, StateData#state.nicks));
+ [_|_] ->
+ ?DICT:store(Nick, [LJID|NewNickUsers],
+ ?DICT:store(OldNick, OldNickUsers -- [LJID],
+ StateData#state.nicks))
+ end,
+ NewStateData = StateData#state{users = Users, nicks = Nicks},
+ send_nick_changing(JID, OldNick, NewStateData, SendOldUnavailable, SendNewAvailable),
+ add_to_log(nickchange, {OldNick, Nick}, StateData),
+ NewStateData.
+
+send_nick_changing(JID, OldNick, StateData,
+ SendOldUnavailable, SendNewAvailable) ->
+ {ok, #user{jid = RealJID,
+ nick = Nick,
+ role = Role,
+ last_presence = Presence}} =
+ ?DICT:find(jlib:jid_tolower(JID), StateData#state.users),
+ Affiliation = get_affiliation(JID, StateData),
+ SAffiliation = affiliation_to_list(Affiliation),
+ SRole = role_to_list(Role),
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ ItemAttrs1 =
+ case (Info#user.role == moderator) orelse
+ ((StateData#state.config)#config.anonymous == false) of
+ true ->
+ [{"jid", jlib:jid_to_string(RealJID)},
+ {"affiliation", SAffiliation},
+ {"role", SRole},
+ {"nick", Nick}];
+ _ ->
+ [{"affiliation", SAffiliation},
+ {"role", SRole},
+ {"nick", Nick}]
+ end,
+ ItemAttrs2 =
+ case (Info#user.role == moderator) orelse
+ ((StateData#state.config)#config.anonymous == false) of
+ true ->
+ [{"jid", jlib:jid_to_string(RealJID)},
+ {"affiliation", SAffiliation},
+ {"role", SRole}];
+ _ ->
+ [{"affiliation", SAffiliation},
+ {"role", SRole}]
+ end,
+ Packet1 =
+ {xmlelement, "presence", [{"type", "unavailable"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs1, []},
+ {xmlelement, "status", [{"code", "303"}], []}]}]},
+ Packet2 = xml:append_subtags(
+ Presence,
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs2, []}]}]),
+ if SendOldUnavailable ->
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, OldNick),
+ Info#user.jid,
+ Packet1);
+ true ->
+ ok
+ end,
+ if SendNewAvailable ->
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ Info#user.jid,
+ Packet2);
+ true ->
+ ok
+ end
+ end, ?DICT:to_list(StateData#state.users)).
+
+
+lqueue_new(Max) ->
+ #lqueue{queue = queue:new(),
+ len = 0,
+ max = Max}.
+
+%% If the message queue limit is set to 0, do not store messages.
+lqueue_in(_Item, LQ = #lqueue{max = 0}) ->
+ LQ;
+%% Otherwise, rotate messages in the queue store.
+lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) ->
+ Q2 = queue:in(Item, Q1),
+ if
+ Len >= Max ->
+ Q3 = lqueue_cut(Q2, Len - Max + 1),
+ #lqueue{queue = Q3, len = Max, max = Max};
+ true ->
+ #lqueue{queue = Q2, len = Len + 1, max = Max}
+ end.
+
+lqueue_cut(Q, 0) ->
+ Q;
+lqueue_cut(Q, N) ->
+ {_, Q1} = queue:out(Q),
+ lqueue_cut(Q1, N - 1).
+
+lqueue_to_list(#lqueue{queue = Q1}) ->
+ queue:to_list(Q1).
+
+lqueue_filter(F, #lqueue{queue = Q1} = LQ) ->
+ Q2 = queue:filter(F, Q1),
+ LQ#lqueue{queue = Q2, len = queue:len(Q2)}.
+
+add_message_to_history(FromNick, FromJID, Packet, StateData) ->
+ HaveSubject = case xml:get_subtag(Packet, "subject") of
+ false ->
+ false;
+ _ ->
+ true
+ end,
+ TimeStamp = calendar:now_to_universal_time(now()),
+ %% 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,
+ TSPacket),
+ Size = element_size(SPacket),
+ Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size},
+ StateData#state.history),
+ add_to_log(text, {FromNick, Packet}, StateData),
+ StateData#state{history = Q1}.
+
+send_history(JID, Shift, StateData) ->
+ lists:foldl(
+ fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) ->
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ JID,
+ Packet),
+ B or HaveSubject
+ end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))).
+
+
+send_subject(JID, Lang, StateData) ->
+ case StateData#state.subject_author of
+ "" ->
+ ok;
+ Nick ->
+ Subject = StateData#state.subject,
+ Packet = {xmlelement, "message", [{"type", "groupchat"}],
+ [{xmlelement, "subject", [], [{xmlcdata, Subject}]},
+ {xmlelement, "body", [],
+ [{xmlcdata,
+ Nick ++
+ translate:translate(Lang,
+ " has set the subject to: ") ++
+ Subject}]}]},
+ route_stanza(
+ StateData#state.jid,
+ JID,
+ Packet)
+ end.
+
+check_subject(Packet) ->
+ case xml:get_subtag(Packet, "subject") of
+ false ->
+ false;
+ SubjEl ->
+ xml:get_tag_cdata(SubjEl)
+ end.
+
+can_change_subject(Role, StateData) ->
+ case (StateData#state.config)#config.allow_change_subj of
+ true ->
+ (Role == moderator) orelse (Role == participant);
+ _ ->
+ Role == moderator
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Admin stuff
+
+process_iq_admin(From, set, Lang, SubEl, StateData) ->
+ {xmlelement, _, _, Items} = SubEl,
+ process_admin_items_set(From, Items, Lang, StateData);
+
+process_iq_admin(From, get, Lang, SubEl, StateData) ->
+ case xml:get_subtag(SubEl, "item") of
+ false ->
+ {error, ?ERR_BAD_REQUEST};
+ Item ->
+ FAffiliation = get_affiliation(From, StateData),
+ FRole = get_role(From, StateData),
+ case xml:get_tag_attr("role", Item) of
+ false ->
+ case xml:get_tag_attr("affiliation", Item) of
+ false ->
+ {error, ?ERR_BAD_REQUEST};
+ {value, StrAffiliation} ->
+ case catch list_to_affiliation(StrAffiliation) of
+ {'EXIT', _} ->
+ {error, ?ERR_BAD_REQUEST};
+ SAffiliation ->
+ if
+ (FAffiliation == owner) or
+ (FAffiliation == admin) ->
+ Items = items_with_affiliation(
+ SAffiliation, StateData),
+ {result, Items, StateData};
+ true ->
+ ErrText = "Administrator privileges required",
+ {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
+ end
+ end
+ end;
+ {value, StrRole} ->
+ case catch list_to_role(StrRole) of
+ {'EXIT', _} ->
+ {error, ?ERR_BAD_REQUEST};
+ SRole ->
+ if
+ FRole == moderator ->
+ Items = items_with_role(SRole, StateData),
+ {result, Items, StateData};
+ true ->
+ ErrText = "Moderator privileges required",
+ {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
+ end
+ end
+ end
+ end.
+
+
+items_with_role(SRole, StateData) ->
+ lists:map(
+ fun({_, U}) ->
+ user_to_item(U, StateData)
+ end, search_role(SRole, StateData)).
+
+items_with_affiliation(SAffiliation, StateData) ->
+ lists:map(
+ fun({JID, {Affiliation, Reason}}) ->
+ {xmlelement, "item",
+ [{"affiliation", affiliation_to_list(Affiliation)},
+ {"jid", jlib:jid_to_string(JID)}],
+ [{xmlelement, "reason", [], [{xmlcdata, Reason}]}]};
+ ({JID, Affiliation}) ->
+ {xmlelement, "item",
+ [{"affiliation", affiliation_to_list(Affiliation)},
+ {"jid", jlib:jid_to_string(JID)}],
+ []}
+ end, search_affiliation(SAffiliation, StateData)).
+
+user_to_item(#user{role = Role,
+ nick = Nick,
+ jid = JID
+ }, StateData) ->
+ Affiliation = get_affiliation(JID, StateData),
+ {xmlelement, "item",
+ [{"role", role_to_list(Role)},
+ {"affiliation", affiliation_to_list(Affiliation)},
+ {"nick", Nick},
+ {"jid", jlib:jid_to_string(JID)}],
+ []}.
+
+search_role(Role, StateData) ->
+ lists:filter(
+ fun({_, #user{role = R}}) ->
+ Role == R
+ end, ?DICT:to_list(StateData#state.users)).
+
+search_affiliation(Affiliation, StateData) ->
+ lists:filter(
+ fun({_, A}) ->
+ case A of
+ {A1, _Reason} ->
+ Affiliation == A1;
+ _ ->
+ Affiliation == A
+ end
+ end, ?DICT:to_list(StateData#state.affiliations)).
+
+
+process_admin_items_set(UJID, Items, Lang, StateData) ->
+ UAffiliation = get_affiliation(UJID, StateData),
+ URole = get_role(UJID, StateData),
+ case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of
+ {result, Res} ->
+ ?INFO_MSG("Processing MUC admin query from ~s in room ~s:~n ~p",
+ [jlib:jid_to_string(UJID), jlib:jid_to_string(StateData#state.jid), Res]),
+ NSD =
+ lists:foldl(
+ fun(E, SD) ->
+ case catch (
+ case E of
+ {JID, affiliation, owner, _}
+ when (JID#jid.luser == "") ->
+ %% If the provided JID does not have username,
+ %% forget the affiliation completely
+ SD;
+ {JID, role, none, Reason} ->
+ catch send_kickban_presence(
+ JID, Reason, "307", SD),
+ set_role(JID, none, SD);
+ {JID, affiliation, none, Reason} ->
+ case (SD#state.config)#config.members_only of
+ true ->
+ catch send_kickban_presence(
+ JID, Reason, "321", none, SD),
+ SD1 = set_affiliation(JID, none, SD),
+ set_role(JID, none, SD1);
+ _ ->
+ SD1 = set_affiliation(JID, none, SD),
+ send_update_presence(JID, SD1),
+ SD1
+ end;
+ {JID, affiliation, outcast, Reason} ->
+ catch send_kickban_presence(
+ JID, Reason, "301", outcast, SD),
+ set_affiliation(
+ JID, outcast,
+ set_role(JID, none, SD), Reason);
+ {JID, affiliation, A, Reason} when
+ (A == admin) or (A == owner) ->
+ SD1 = set_affiliation(JID, A, SD, Reason),
+ SD2 = set_role(JID, moderator, SD1),
+ send_update_presence(JID, Reason, SD2),
+ SD2;
+ {JID, affiliation, member, Reason} ->
+ SD1 = set_affiliation(
+ JID, member, SD, Reason),
+ SD2 = set_role(JID, participant, SD1),
+ send_update_presence(JID, Reason, SD2),
+ SD2;
+ {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),
+ send_update_presence(JID, SD1),
+ SD1
+ end
+ ) of
+ {'EXIT', ErrReason} ->
+ ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n",
+ [ErrReason]),
+ SD;
+ NSD ->
+ NSD
+ end
+ end, StateData, lists:flatten(Res)),
+ case (NSD#state.config)#config.persistent of
+ true ->
+ (NSD#state.mod):store_room(NSD#state.server_host,
+ NSD#state.host, NSD#state.room,
+ make_opts(NSD));
+ _ ->
+ ok
+ end,
+ {result, [], NSD};
+ Err ->
+ Err
+ end.
+
+
+find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) ->
+ {result, Res};
+find_changed_items(UJID, UAffiliation, URole, [{xmlcdata, _} | Items],
+ Lang, StateData, Res) ->
+ find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res);
+find_changed_items(UJID, UAffiliation, URole,
+ [{xmlelement, "item", Attrs, _Els} = Item | Items],
+ Lang, StateData, Res) ->
+ TJID = case xml:get_attr("jid", Attrs) of
+ {value, S} ->
+ case jlib:string_to_jid(S) of
+ error ->
+ ErrText = io_lib:format(
+ translate:translate(
+ Lang,
+ "Jabber ID ~s is invalid"), [S]),
+ {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
+ J ->
+ {value, [J]}
+ end;
+ _ ->
+ case xml:get_attr("nick", Attrs) of
+ {value, N} ->
+ case find_jids_by_nick(N, StateData) of
+ false ->
+ ErrText =
+ io_lib:format(
+ translate:translate(
+ Lang,
+ "Nickname ~s does not exist in the room"),
+ [N]),
+ {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
+ J ->
+ {value, J}
+ end;
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end
+ end,
+ case TJID of
+ {value, [JID|_]=JIDs} ->
+ TAffiliation = get_affiliation(JID, StateData),
+ TRole = get_role(JID, StateData),
+ case xml:get_attr("role", Attrs) of
+ false ->
+ case xml:get_attr("affiliation", Attrs) of
+ false ->
+ {error, ?ERR_BAD_REQUEST};
+ {value, StrAffiliation} ->
+ case catch list_to_affiliation(StrAffiliation) of
+ {'EXIT', _} ->
+ ErrText1 =
+ io_lib:format(
+ translate:translate(
+ Lang,
+ "Invalid affiliation: ~s"),
+ [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,
+ ServiceAf) of
+ nothing ->
+ nothing;
+ true ->
+ true;
+ check_owner ->
+ case search_affiliation(
+ owner, StateData) of
+ [{OJID, _}] ->
+ jlib:jid_remove_resource(OJID) /=
+ jlib:jid_tolower(jlib:jid_remove_resource(UJID));
+ _ ->
+ true
+ end;
+ _ ->
+ false
+ end,
+ case CanChangeRA of
+ nothing ->
+ find_changed_items(
+ UJID,
+ UAffiliation, URole,
+ Items, Lang, StateData,
+ Res);
+ true ->
+ Reason = xml:get_path_s(Item, [{elem, "reason"}, cdata]),
+ MoreRes = [{jlib:jid_remove_resource(Jidx), affiliation, SAffiliation, Reason} || Jidx <- JIDs],
+ find_changed_items(
+ UJID,
+ UAffiliation, URole,
+ Items, Lang, StateData,
+ [MoreRes | Res]);
+ false ->
+ {error, ?ERR_NOT_ALLOWED}
+ end
+ end
+ end;
+ {value, StrRole} ->
+ case catch list_to_role(StrRole) of
+ {'EXIT', _} ->
+ ErrText1 =
+ io_lib:format(
+ translate:translate(
+ Lang,
+ "Invalid role: ~s"),
+ [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,
+ ServiceAf) of
+ nothing ->
+ nothing;
+ true ->
+ true;
+ check_owner ->
+ case search_affiliation(
+ owner, StateData) of
+ [{OJID, _}] ->
+ jlib:jid_remove_resource(OJID) /=
+ jlib:jid_tolower(jlib:jid_remove_resource(UJID));
+ _ ->
+ true
+ end;
+ _ ->
+ false
+ end,
+ case CanChangeRA of
+ nothing ->
+ find_changed_items(
+ UJID,
+ UAffiliation, URole,
+ Items, Lang, StateData,
+ Res);
+ true ->
+ Reason = xml:get_path_s(Item, [{elem, "reason"}, cdata]),
+ MoreRes = [{Jidx, role, SRole, Reason} || Jidx <- JIDs],
+ find_changed_items(
+ UJID,
+ UAffiliation, URole,
+ Items, Lang, StateData,
+ [MoreRes | Res]);
+ _ ->
+ {error, ?ERR_NOT_ALLOWED}
+ end
+ end
+ end;
+ Err ->
+ Err
+ end;
+find_changed_items(_UJID, _UAffiliation, _URole, _Items,
+ _Lang, _StateData, _Res) ->
+ {error, ?ERR_BAD_REQUEST}.
+
+
+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,
+ _RoleorAffiliation, _Value, owner) ->
+ %% Nobody can decrease MUC admin's role/affiliation
+ false;
+can_change_ra(_FAffiliation, _FRole,
+ TAffiliation, _TRole,
+ affiliation, Value, _ServiceAf)
+ when (TAffiliation == Value) ->
+ nothing;
+can_change_ra(_FAffiliation, _FRole,
+ _TAffiliation, TRole,
+ role, Value, _ServiceAf)
+ when (TRole == Value) ->
+ nothing;
+can_change_ra(FAffiliation, _FRole,
+ outcast, _TRole,
+ affiliation, none, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ outcast, _TRole,
+ affiliation, member, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(owner, _FRole,
+ outcast, _TRole,
+ affiliation, admin, _ServiceAf) ->
+ true;
+can_change_ra(owner, _FRole,
+ outcast, _TRole,
+ affiliation, owner, _ServiceAf) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ none, _TRole,
+ affiliation, outcast, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ none, _TRole,
+ affiliation, member, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(owner, _FRole,
+ none, _TRole,
+ affiliation, admin, _ServiceAf) ->
+ true;
+can_change_ra(owner, _FRole,
+ none, _TRole,
+ affiliation, owner, _ServiceAf) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ member, _TRole,
+ affiliation, outcast, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ member, _TRole,
+ affiliation, none, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(owner, _FRole,
+ member, _TRole,
+ affiliation, admin, _ServiceAf) ->
+ true;
+can_change_ra(owner, _FRole,
+ member, _TRole,
+ affiliation, owner, _ServiceAf) ->
+ true;
+can_change_ra(owner, _FRole,
+ admin, _TRole,
+ affiliation, _Affiliation, _ServiceAf) ->
+ true;
+can_change_ra(owner, _FRole,
+ owner, _TRole,
+ affiliation, _Affiliation, _ServiceAf) ->
+ check_owner;
+can_change_ra(_FAffiliation, _FRole,
+ _TAffiliation, _TRole,
+ affiliation, _Value, _ServiceAf) ->
+ false;
+can_change_ra(_FAffiliation, moderator,
+ _TAffiliation, visitor,
+ role, none, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, moderator,
+ _TAffiliation, visitor,
+ role, participant, _ServiceAf) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ _TAffiliation, visitor,
+ role, moderator, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(_FAffiliation, moderator,
+ _TAffiliation, participant,
+ role, none, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, moderator,
+ _TAffiliation, participant,
+ role, visitor, _ServiceAf) ->
+ true;
+can_change_ra(FAffiliation, _FRole,
+ _TAffiliation, participant,
+ role, moderator, _ServiceAf)
+ when (FAffiliation == owner) or (FAffiliation == admin) ->
+ true;
+can_change_ra(_FAffiliation, _FRole,
+ owner, moderator,
+ role, visitor, _ServiceAf) ->
+ false;
+can_change_ra(owner, _FRole,
+ _TAffiliation, moderator,
+ role, visitor, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, _FRole,
+ admin, moderator,
+ role, visitor, _ServiceAf) ->
+ false;
+can_change_ra(admin, _FRole,
+ _TAffiliation, moderator,
+ role, visitor, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, _FRole,
+ owner, moderator,
+ role, participant, _ServiceAf) ->
+ false;
+can_change_ra(owner, _FRole,
+ _TAffiliation, moderator,
+ role, participant, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, _FRole,
+ admin, moderator,
+ role, participant, _ServiceAf) ->
+ false;
+can_change_ra(admin, _FRole,
+ _TAffiliation, moderator,
+ role, participant, _ServiceAf) ->
+ true;
+can_change_ra(_FAffiliation, _FRole,
+ _TAffiliation, _TRole,
+ role, _Value, _ServiceAf) ->
+ false.
+
+
+send_kickban_presence(JID, Reason, Code, StateData) ->
+ NewAffiliation = get_affiliation(JID, StateData),
+ send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData).
+
+send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) ->
+ LJID = jlib:jid_tolower(JID),
+ LJIDs = case LJID of
+ {U, S, ""} ->
+ ?DICT:fold(
+ fun(J, _, Js) ->
+ case J of
+ {U, S, _} ->
+ [J | Js];
+ _ ->
+ Js
+ end
+ end, [], StateData#state.users);
+ _ ->
+ case ?DICT:is_key(LJID, StateData#state.users) of
+ true ->
+ [LJID];
+ _ ->
+ []
+ end
+ end,
+ lists:foreach(fun(J) ->
+ {ok, #user{nick = Nick}} =
+ ?DICT:find(J, StateData#state.users),
+ add_to_log(kickban, {Nick, Reason, Code}, StateData),
+ tab_remove_online_user(J, StateData),
+ send_kickban_presence1(J, Reason, Code, NewAffiliation, StateData)
+ end, LJIDs).
+
+send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) ->
+ {ok, #user{jid = RealJID,
+ nick = Nick}} =
+ ?DICT:find(jlib:jid_tolower(UJID), StateData#state.users),
+ SAffiliation = affiliation_to_list(Affiliation),
+ BannedJIDString = jlib:jid_to_string(RealJID),
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ JidAttrList = case (Info#user.role == moderator) orelse
+ ((StateData#state.config)#config.anonymous
+ == false) of
+ true -> [{"jid", BannedJIDString}];
+ false -> []
+ end,
+ ItemAttrs = [{"affiliation", SAffiliation},
+ {"role", "none"}] ++ JidAttrList,
+ ItemEls = case Reason of
+ "" ->
+ [];
+ _ ->
+ [{xmlelement, "reason", [],
+ [{xmlcdata, Reason}]}]
+ end,
+ Packet = {xmlelement, "presence", [{"type", "unavailable"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs, ItemEls},
+ {xmlelement, "status", [{"code", Code}], []}]}]},
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ Info#user.jid,
+ Packet)
+ end, ?DICT:to_list(StateData#state.users)).
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Owner stuff
+
+process_iq_owner(From, set, Lang, SubEl, StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ case FAffiliation of
+ owner ->
+ {xmlelement, _Name, _Attrs, Els} = SubEl,
+ case xml:remove_cdata(Els) of
+ [{xmlelement, "x", _Attrs1, _Els1} = XEl] ->
+ case {xml:get_tag_attr_s("xmlns", XEl),
+ xml:get_tag_attr_s("type", XEl)} of
+ {?NS_XDATA, "cancel"} ->
+ {result, [], StateData};
+ {?NS_XDATA, "submit"} ->
+ 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;
+ [{xmlelement, "destroy", _Attrs1, _Els1} = SubEl1] ->
+ ?INFO_MSG("Destroyed MUC room ~s by the owner ~s",
+ [jlib:jid_to_string(StateData#state.jid), jlib:jid_to_string(From)]),
+ add_to_log(room_existence, destroyed, StateData),
+ destroy_room(SubEl1, StateData);
+ Items ->
+ process_admin_items_set(From, Items, Lang, StateData)
+ end;
+ _ ->
+ ErrText = "Owner privileges required",
+ {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
+ end;
+
+process_iq_owner(From, get, Lang, SubEl, StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ case FAffiliation of
+ owner ->
+ {xmlelement, _Name, _Attrs, Els} = SubEl,
+ case xml:remove_cdata(Els) of
+ [] ->
+ get_config(Lang, StateData, From);
+ [Item] ->
+ case xml:get_tag_attr("affiliation", Item) of
+ false ->
+ {error, ?ERR_BAD_REQUEST};
+ {value, StrAffiliation} ->
+ case catch list_to_affiliation(StrAffiliation) of
+ {'EXIT', _} ->
+ ErrText =
+ io_lib:format(
+ translate:translate(
+ Lang,
+ "Invalid affiliation: ~s"),
+ [StrAffiliation]),
+ {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
+ SAffiliation ->
+ Items = items_with_affiliation(
+ SAffiliation, StateData),
+ {result, Items, StateData}
+ end
+ end;
+ _ ->
+ {error, ?ERR_FEATURE_NOT_IMPLEMENTED}
+ end;
+ _ ->
+ ErrText = "Owner privileges required",
+ {error, ?ERRT_FORBIDDEN(Lang, ErrText)}
+ end.
+
+is_allowed_log_change(XEl, StateData, From) ->
+ case lists:keymember("muc#roomconfig_enablelogging", 1,
+ jlib:parse_xdata_submit(XEl)) of
+ false ->
+ true;
+ true ->
+ (allow == mod_muc_log:check_access_log(
+ StateData#state.server_host, From))
+ end.
+
+is_allowed_persistent_change(XEl, StateData, From) ->
+ case lists:keymember("muc#roomconfig_persistentroom", 1,
+ jlib:parse_xdata_submit(XEl)) of
+ false ->
+ true;
+ true ->
+ {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access,
+ (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,
+ StateData#state.mod,
+ 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,
+ StateData#state.mod,
+ 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)},
+ {"var", Var}],
+ [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
+
+-define(BOOLXFIELD(Label, Var, Val),
+ ?XFIELD("boolean", Label, Var,
+ case Val of
+ true -> "1";
+ _ -> "0"
+ end)).
+
+-define(STRINGXFIELD(Label, Var, Val),
+ ?XFIELD("text-single", Label, Var, Val)).
+
+-define(PRIVATEXFIELD(Label, Var, Val),
+ ?XFIELD("text-private", Label, Var, Val)).
+
+-define(JIDMULTIXFIELD(Label, Var, JIDList),
+ {xmlelement, "field", [{"type", "jid-multi"},
+ {"label", translate:translate(Lang, Label)},
+ {"var", Var}],
+ [{xmlelement, "value", [], [{xmlcdata, jlib:jid_to_string(JID)}]}
+ || JID <- JIDList]}).
+
+get_default_room_maxusers(RoomState) ->
+ DefRoomOpts = gen_mod:get_module_opt(
+ RoomState#state.server_host,
+ RoomState#state.mod, 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, 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),
+ ?STRINGXFIELD("Room description",
+ "muc#roomconfig_roomdesc",
+ Config#config.description)
+ ] ++
+ case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
+ allow ->
+ [?BOOLXFIELD(
+ "Make room persistent",
+ "muc#roomconfig_persistentroom",
+ Config#config.persistent)];
+ _ -> []
+ end ++ [
+ ?BOOLXFIELD("Make room public searchable",
+ "muc#roomconfig_publicroom",
+ Config#config.public),
+ ?BOOLXFIELD("Make participants list public",
+ "public_list",
+ Config#config.public_list),
+ ?BOOLXFIELD("Make room password protected",
+ "muc#roomconfig_passwordprotectedroom",
+ Config#config.password_protected),
+ ?PRIVATEXFIELD("Password",
+ "muc#roomconfig_roomsecret",
+ case Config#config.password_protected of
+ true -> Config#config.password;
+ false -> ""
+ end),
+ {xmlelement, "field",
+ [{"type", "list-single"},
+ {"label", translate:translate(Lang, "Maximum Number of Occupants")},
+ {"var", "muc#roomconfig_maxusers"}],
+ [{xmlelement, "value", [], [{xmlcdata, MaxUsersRoomString}]}] ++
+ if
+ is_integer(ServiceMaxUsers) -> [];
+ true ->
+ [{xmlelement, "option",
+ [{"label", translate:translate(Lang, "No limit")}],
+ [{xmlelement, "value", [], [{xmlcdata, "none"}]}]}]
+ end ++
+ [{xmlelement, "option", [{"label", erlang:integer_to_list(N)}],
+ [{xmlelement, "value", [],
+ [{xmlcdata, erlang:integer_to_list(N)}]}]} ||
+ N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger |
+ ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers]
+ },
+ {xmlelement, "field",
+ [{"type", "list-single"},
+ {"label", translate:translate(Lang, "Present real Jabber IDs to")},
+ {"var", "muc#roomconfig_whois"}],
+ [{xmlelement, "value", [], [{xmlcdata,
+ if Config#config.anonymous ->
+ "moderators";
+ true ->
+ "anyone"
+ end}]},
+ {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}],
+ [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]},
+ {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}],
+ [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]},
+ ?BOOLXFIELD("Make room members-only",
+ "muc#roomconfig_membersonly",
+ Config#config.members_only),
+ ?BOOLXFIELD("Make room moderated",
+ "muc#roomconfig_moderatedroom",
+ Config#config.moderated),
+ ?BOOLXFIELD("Default users as participants",
+ "members_by_default",
+ Config#config.members_by_default),
+ ?BOOLXFIELD("Allow users to change the subject",
+ "muc#roomconfig_changesubject",
+ Config#config.allow_change_subj),
+ ?BOOLXFIELD("Allow users to send private messages",
+ "allow_private_messages",
+ Config#config.allow_private_messages),
+ {xmlelement, "field",
+ [{"type", "list-single"},
+ {"label", translate:translate(Lang, "Allow visitors to send private messages to")},
+ {"var", "allow_private_messages_from_visitors"}],
+ [{xmlelement, "value", [], [{xmlcdata,
+ case Config#config.allow_private_messages_from_visitors of
+ anyone ->
+ "anyone";
+ moderators ->
+ "moderators";
+ nobody ->
+ "nobody"
+ end}]},
+ {xmlelement, "option", [{"label", translate:translate(Lang, "nobody")}],
+ [{xmlelement, "value", [], [{xmlcdata, "nobody"}]}]},
+ {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}],
+ [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]},
+ {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}],
+ [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]},
+ ?BOOLXFIELD("Allow users to query other users",
+ "allow_query_users",
+ Config#config.allow_query_users),
+ ?BOOLXFIELD("Allow users to send invites",
+ "muc#roomconfig_allowinvites",
+ 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),
+ ?BOOLXFIELD("Allow visitors to send voice requests",
+ "muc#roomconfig_allowvoicerequests",
+ Config#config.allow_voice_requests),
+ ?STRINGXFIELD("Minimum interval between voice requests (in seconds)",
+ "muc#roomconfig_voicerequestmininterval",
+ erlang:integer_to_list(Config#config.voice_request_min_interval))
+ ] ++
+ case ejabberd_captcha:is_feature_available() of
+ true ->
+ [?BOOLXFIELD("Make room CAPTCHA protected",
+ "captcha_protected",
+ Config#config.captcha_protected)];
+ false -> []
+ end ++
+ [?JIDMULTIXFIELD("Exclude Jabber IDs from CAPTCHA challenge",
+ "muc#roomconfig_captcha_whitelist",
+ ?SETS:to_list(Config#config.captcha_whitelist))] ++
+ case mod_muc_log:check_access_log(
+ StateData#state.server_host, From) of
+ allow ->
+ [?BOOLXFIELD(
+ "Enable logging",
+ "muc#roomconfig_enablelogging",
+ Config#config.logging)];
+ _ -> []
+ end,
+ {result, [{xmlelement, "instructions", [],
+ [{xmlcdata,
+ translate:translate(
+ Lang, "You need an x:data capable client to configure room")}]},
+ {xmlelement, "x", [{"xmlns", ?NS_XDATA},
+ {"type", "form"}],
+ Res}],
+ StateData}.
+
+
+
+set_config(XEl, StateData) ->
+ XData = jlib:parse_xdata_submit(XEl),
+ case XData of
+ invalid ->
+ {error, ?ERR_BAD_REQUEST};
+ _ ->
+ case set_xoption(XData, StateData#state.config) of
+ #config{} = Config ->
+ Res = change_config(Config, StateData),
+ {result, _, NSD} = Res,
+ 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
+ end
+ end.
+
+-define(SET_BOOL_XOPT(Opt, Val),
+ case Val of
+ "0" -> set_xoption(Opts, Config#config{Opt = false});
+ "false" -> set_xoption(Opts, Config#config{Opt = false});
+ "1" -> set_xoption(Opts, Config#config{Opt = true});
+ "true" -> set_xoption(Opts, Config#config{Opt = true});
+ _ -> {error, ?ERR_BAD_REQUEST}
+ end).
+
+-define(SET_NAT_XOPT(Opt, Val),
+ case catch list_to_integer(Val) of
+ I when is_integer(I),
+ I > 0 ->
+ set_xoption(Opts, Config#config{Opt = I});
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end).
+
+-define(SET_STRING_XOPT(Opt, Val),
+ set_xoption(Opts, Config#config{Opt = Val})).
+
+-define(SET_JIDMULTI_XOPT(Opt, Vals),
+ begin
+ Set = lists:foldl(
+ fun({U, S, R}, Set1) ->
+ ?SETS:add_element({U, S, R}, Set1);
+ (#jid{luser = U, lserver = S, lresource = R}, Set1) ->
+ ?SETS:add_element({U, S, R}, Set1);
+ (_, Set1) ->
+ Set1
+ end, ?SETS:empty(), Vals),
+ set_xoption(Opts, Config#config{Opt = Set})
+ end).
+
+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([{"allow_private_messages_from_visitors", [Val]} | Opts], Config) ->
+ case Val of
+ "anyone" ->
+ ?SET_STRING_XOPT(allow_private_messages_from_visitors, anyone);
+ "moderators" ->
+ ?SET_STRING_XOPT(allow_private_messages_from_visitors, moderators);
+ "nobody" ->
+ ?SET_STRING_XOPT(allow_private_messages_from_visitors, nobody);
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end;
+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) ->
+ ?SET_BOOL_XOPT(public_list, Val);
+set_xoption([{"muc#roomconfig_persistentroom", [Val]} | Opts], Config) ->
+ ?SET_BOOL_XOPT(persistent, Val);
+set_xoption([{"muc#roomconfig_moderatedroom", [Val]} | Opts], Config) ->
+ ?SET_BOOL_XOPT(moderated, Val);
+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) ->
+ ?SET_BOOL_XOPT(password_protected, Val);
+set_xoption([{"muc#roomconfig_roomsecret", [Val]} | Opts], Config) ->
+ ?SET_STRING_XOPT(password, Val);
+set_xoption([{"anonymous", [Val]} | Opts], Config) ->
+ ?SET_BOOL_XOPT(anonymous, Val);
+set_xoption([{"muc#roomconfig_allowvoicerequests", [Val]} | Opts], Config) ->
+ ?SET_BOOL_XOPT(allow_voice_requests, Val);
+set_xoption([{"muc#roomconfig_voicerequestmininterval", [Val]} | Opts], Config) ->
+ ?SET_NAT_XOPT(voice_request_min_interval, Val);
+set_xoption([{"muc#roomconfig_whois", [Val]} | Opts], Config) ->
+ case Val of
+ "moderators" ->
+ ?SET_BOOL_XOPT(anonymous, integer_to_list(1));
+ "anyone" ->
+ ?SET_BOOL_XOPT(anonymous, integer_to_list(0));
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end;
+set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) ->
+ case Val of
+ "none" ->
+ ?SET_STRING_XOPT(max_users, none);
+ _ ->
+ ?SET_NAT_XOPT(max_users, Val)
+ end;
+set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) ->
+ ?SET_BOOL_XOPT(logging, Val);
+set_xoption([{"muc#roomconfig_captcha_whitelist", Vals} | Opts], Config) ->
+ JIDs = [jlib:string_to_jid(Val) || Val <- Vals],
+ ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
+set_xoption([{"FORM_TYPE", _} | Opts], Config) ->
+ %% Ignore our FORM_TYPE
+ set_xoption(Opts, Config);
+set_xoption([_ | _Opts], _Config) ->
+ {error, ?ERR_BAD_REQUEST}.
+
+
+change_config(Config, StateData) ->
+ NSD = StateData#state{config = Config},
+ Mod = StateData#state.mod,
+ case {(StateData#state.config)#config.persistent,
+ Config#config.persistent} of
+ {_, true} ->
+ Mod:store_room(NSD#state.server_host, NSD#state.host,
+ NSD#state.room, make_opts(NSD));
+ {true, false} ->
+ Mod:forget_room(NSD#state.server_host, NSD#state.host, NSD#state.room);
+ {false, false} ->
+ ok
+ end,
+ case {(StateData#state.config)#config.members_only,
+ Config#config.members_only} of
+ {false, true} ->
+ NSD1 = remove_nonmembers(NSD),
+ {result, [], NSD1};
+ _ ->
+ {result, [], NSD}
+ end.
+
+remove_nonmembers(StateData) ->
+ lists:foldl(
+ fun({_LJID, #user{jid = JID}}, SD) ->
+ Affiliation = get_affiliation(JID, SD),
+ case Affiliation of
+ none ->
+ catch send_kickban_presence(
+ JID, "", "322", SD),
+ set_role(JID, none, SD);
+ _ ->
+ SD
+ end
+ end, StateData, ?DICT:to_list(StateData#state.users)).
+
+
+-define(CASE_CONFIG_OPT(Opt),
+ Opt -> StateData#state{
+ config = (StateData#state.config)#config{Opt = Val}}).
+
+set_opts([], StateData) ->
+ 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_private_messages_from_visitors -> StateData#state{config = (StateData#state.config)#config{allow_private_messages_from_visitors = 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}};
+ moderated -> StateData#state{config = (StateData#state.config)#config{moderated = Val}};
+ members_by_default -> StateData#state{config = (StateData#state.config)#config{members_by_default = Val}};
+ 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}};
+ captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = ?SETS:from_list(Val)}};
+ allow_voice_requests -> StateData#state{config = (StateData#state.config)#config{allow_voice_requests = Val}};
+ voice_request_min_interval -> StateData#state{config = (StateData#state.config)#config{voice_request_min_interval = Val}};
+ max_users ->
+ ServiceMaxUsers = get_service_max_users(StateData),
+ MaxUsers = if
+ Val =< ServiceMaxUsers -> Val;
+ true -> ServiceMaxUsers
+ end,
+ StateData#state{
+ config = (StateData#state.config)#config{
+ max_users = MaxUsers}};
+ affiliations ->
+ StateData#state{affiliations = ?DICT:from_list(Val)};
+ subject ->
+ StateData#state{subject = Val};
+ subject_author ->
+ StateData#state{subject_author = Val};
+ _ -> StateData
+ end,
+ set_opts(Opts, NSD).
+
+-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
+
+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_private_messages_from_visitors),
+ ?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),
+ ?MAKE_CONFIG_OPT(moderated),
+ ?MAKE_CONFIG_OPT(members_by_default),
+ ?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),
+ ?MAKE_CONFIG_OPT(max_users),
+ ?MAKE_CONFIG_OPT(allow_voice_requests),
+ ?MAKE_CONFIG_OPT(voice_request_min_interval),
+ {captcha_whitelist,
+ ?SETS:to_list((StateData#state.config)#config.captcha_whitelist)},
+ {affiliations, ?DICT:to_list(StateData#state.affiliations)},
+ {subject, StateData#state.subject},
+ {subject_author, StateData#state.subject_author}
+ ].
+
+
+
+destroy_room(DEl, StateData) ->
+ lists:foreach(
+ fun({_LJID, Info}) ->
+ Nick = Info#user.nick,
+ ItemAttrs = [{"affiliation", "none"},
+ {"role", "none"}],
+ Packet = {xmlelement, "presence", [{"type", "unavailable"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}],
+ [{xmlelement, "item", ItemAttrs, []}, DEl]}]},
+ route_stanza(
+ jlib:jid_replace_resource(StateData#state.jid, Nick),
+ Info#user.jid,
+ Packet)
+ end, ?DICT:to_list(StateData#state.users)),
+ case (StateData#state.config)#config.persistent of
+ true ->
+ (StateData#state.mod):forget_room(
+ StateData#state.server_host,
+ StateData#state.host, StateData#state.room);
+ false ->
+ ok
+ end,
+ {result, [], stop}.
+
+
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Disco
+
+-define(FEATURE(Var), {xmlelement, "feature", [{"var", Var}], []}).
+
+-define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse),
+ case Opt of
+ true ->
+ ?FEATURE(Fiftrue);
+ false ->
+ ?FEATURE(Fiffalse)
+ end).
+
+process_iq_disco_info(_From, set, _Lang, _StateData) ->
+ {error, ?ERR_NOT_ALLOWED};
+
+process_iq_disco_info(_From, get, Lang, StateData) ->
+ Config = StateData#state.config,
+ {result, [{xmlelement, "identity",
+ [{"category", "conference"},
+ {"type", "text"},
+ {"name", get_title(StateData)}], []},
+ {xmlelement, "feature",
+ [{"var", ?NS_MUC}], []},
+ ?CONFIG_OPT_TO_FEATURE(Config#config.public,
+ "muc_public", "muc_hidden"),
+ ?CONFIG_OPT_TO_FEATURE(Config#config.persistent,
+ "muc_persistent", "muc_temporary"),
+ ?CONFIG_OPT_TO_FEATURE(Config#config.members_only,
+ "muc_membersonly", "muc_open"),
+ ?CONFIG_OPT_TO_FEATURE(Config#config.anonymous,
+ "muc_semianonymous", "muc_nonanonymous"),
+ ?CONFIG_OPT_TO_FEATURE(Config#config.moderated,
+ "muc_moderated", "muc_unmoderated"),
+ ?CONFIG_OPT_TO_FEATURE(Config#config.password_protected,
+ "muc_passwordprotected", "muc_unsecured")
+ ] ++ iq_disco_info_extras(Lang, StateData), StateData}.
+
+-define(RFIELDT(Type, Var, Val),
+ {xmlelement, "field", [{"type", Type}, {"var", Var}],
+ [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
+
+-define(RFIELD(Label, Var, Val),
+ {xmlelement, "field", [{"label", translate:translate(Lang, Label)},
+ {"var", Var}],
+ [{xmlelement, "value", [], [{xmlcdata, Val}]}]}).
+
+iq_disco_info_extras(Lang, StateData) ->
+ Len = ?DICT:size(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))
+ ]}].
+
+process_iq_disco_items(_From, set, _Lang, _StateData) ->
+ {error, ?ERR_NOT_ALLOWED};
+
+process_iq_disco_items(From, get, _Lang, StateData) ->
+ case (StateData#state.config)#config.public_list of
+ true ->
+ {result, get_mucroom_disco_items(StateData), StateData};
+ _ ->
+ case is_occupant_or_admin(From, StateData) of
+ true ->
+ {result, get_mucroom_disco_items(StateData), StateData};
+ _ ->
+ {error, ?ERR_FORBIDDEN}
+ end
+ 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
+ "" ->
+ StateData#state.room;
+ Name ->
+ Name
+ end.
+
+get_roomdesc_reply(JID, StateData, Tail) ->
+ IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData),
+ if (StateData#state.config)#config.public or IsOccupantOrAdmin ->
+ if (StateData#state.config)#config.public_list or IsOccupantOrAdmin ->
+ {item, get_title(StateData) ++ Tail};
+ true ->
+ {item, get_title(StateData)}
+ end;
+ true ->
+ false
+ end.
+
+get_roomdesc_tail(StateData, Lang) ->
+ Desc = case (StateData#state.config)#config.public of
+ true ->
+ "";
+ _ ->
+ translate:translate(Lang, "private, ")
+ end,
+ Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, StateData#state.users),
+ " (" ++ Desc ++ integer_to_list(Len) ++ ")".
+
+get_mucroom_disco_items(StateData) ->
+ lists:map(
+ fun({_LJID, Info}) ->
+ Nick = Info#user.nick,
+ {xmlelement, "item",
+ [{"jid", jlib:jid_to_string({StateData#state.room,
+ StateData#state.host, Nick})},
+ {"name", Nick}], []}
+ end,
+ ?DICT:to_list(StateData#state.users)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Voice request support
+
+is_voice_request(Els) ->
+ lists:foldl(
+ fun({xmlelement, "x", Attrs, _} = El, false) ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_XDATA ->
+ case jlib:parse_xdata_submit(El) of
+ [_|_] = Fields ->
+ case {lists:keysearch("FORM_TYPE", 1, Fields),
+ lists:keysearch("muc#role", 1, Fields)} of
+ {{value,
+ {_, ["http://jabber.org/protocol/muc#request"]}},
+ {value, {_, ["participant"]}}} ->
+ true;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end;
+ (_, Acc) ->
+ Acc
+ end, false, Els).
+
+prepare_request_form(Requester, Nick, Lang) ->
+ {xmlelement, "message", [{"type", "normal"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}],
+ [{xmlelement, "title", [],
+ [{xmlcdata, translate:translate(Lang, "Voice request")}]},
+ {xmlelement, "instructions", [],
+ [{xmlcdata,
+ translate:translate(
+ Lang, "Either approve or decline the voice request.")}]},
+ {xmlelement, "field", [{"var", "FORM_TYPE"}, {"type", "hidden"}],
+ [{xmlelement, "value", [],
+ [{xmlcdata, "http://jabber.org/protocol/muc#request"}]}]},
+ {xmlelement, "field", [{"var", "muc#role"}, {"type", "hidden"}],
+ [{xmlelement, "value", [], [{xmlcdata, "participant"}]}]},
+ ?STRINGXFIELD("User JID", "muc#jid", jlib:jid_to_string(Requester)),
+ ?STRINGXFIELD("Nickname", "muc#roomnick", Nick),
+ ?BOOLXFIELD("Grant voice to this person?", "muc#request_allow",
+ list_to_atom("false"))
+ ]}]}.
+
+send_voice_request(From, StateData) ->
+ Moderators = search_role(moderator, StateData),
+ FromNick = find_nick_by_jid(From, StateData),
+ lists:foreach(
+ fun({_, User}) ->
+ route_stanza(
+ StateData#state.jid,
+ User#user.jid,
+ prepare_request_form(From, FromNick, ""))
+ end, Moderators).
+
+is_voice_approvement(Els) ->
+ lists:foldl(
+ fun({xmlelement, "x", Attrs, _} = El, false) ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_XDATA ->
+ case jlib:parse_xdata_submit(El) of
+ [_|_] = Fs ->
+ case {lists:keysearch("FORM_TYPE", 1, Fs),
+ lists:keysearch("muc#role", 1, Fs),
+ lists:keysearch("muc#request_allow", 1, Fs)} of
+ {{value,
+ {_, ["http://jabber.org/protocol/muc#request"]}},
+ {value, {_, ["participant"]}},
+ {value, {_, [Flag]}}}
+ when Flag == "true"; Flag == "1" ->
+ true;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end;
+ (_, Acc) ->
+ Acc
+ end, false, Els).
+
+extract_jid_from_voice_approvement(Els) ->
+ lists:foldl(
+ fun({xmlelement, "x", _, _} = El, error) ->
+ Fields = case jlib:parse_xdata_submit(El) of
+ invalid -> [];
+ Res -> Res
+ end,
+ lists:foldl(
+ fun({"muc#jid", [JIDStr]}, error) ->
+ case jlib:string_to_jid(JIDStr) of
+ error -> error;
+ J -> {ok, J}
+ end;
+ (_, Acc) ->
+ Acc
+ end, error, Fields);
+ (_, Acc) ->
+ Acc
+ end, error, Els).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Invitation support
+
+is_invitation(Els) ->
+ lists:foldl(
+ fun({xmlelement, "x", Attrs, _} = El, false) ->
+ case xml:get_attr_s("xmlns", Attrs) of
+ ?NS_MUC_USER ->
+ case xml:get_subtag(El, "invite") of
+ false ->
+ false;
+ _ ->
+ true
+ end;
+ _ ->
+ false
+ end;
+ (_, Acc) ->
+ Acc
+ end, false, Els).
+
+check_invitation(From, Els, Lang, StateData) ->
+ FAffiliation = get_affiliation(From, StateData),
+ CanInvite = (StateData#state.config)#config.allow_user_invites
+ orelse (FAffiliation == admin) orelse (FAffiliation == owner),
+ InviteEl = case xml:remove_cdata(Els) of
+ [{xmlelement, "x", _Attrs1, Els1} = XEl] ->
+ case xml:get_tag_attr_s("xmlns", XEl) of
+ ?NS_MUC_USER ->
+ ok;
+ _ ->
+ throw({error, ?ERR_BAD_REQUEST})
+ end,
+ case xml:remove_cdata(Els1) of
+ [{xmlelement, "invite", _Attrs2, _Els2} = InviteEl1] ->
+ InviteEl1;
+ _ ->
+ throw({error, ?ERR_BAD_REQUEST})
+ end;
+ _ ->
+ throw({error, ?ERR_BAD_REQUEST})
+ end,
+ JID = case jlib:string_to_jid(
+ xml:get_tag_attr_s("to", InviteEl)) of
+ error ->
+ throw({error, ?ERR_JID_MALFORMED});
+ JID1 ->
+ JID1
+ end,
+ case CanInvite of
+ false ->
+ throw({error, ?ERR_NOT_ALLOWED});
+ true ->
+ Reason =
+ xml:get_path_s(
+ InviteEl,
+ [{elem, "reason"}, cdata]),
+ ContinueEl =
+ case xml:get_path_s(
+ InviteEl,
+ [{elem, "continue"}]) of
+ [] -> [];
+ Continue1 -> [Continue1]
+ end,
+ IEl =
+ [{xmlelement, "invite",
+ [{"from",
+ jlib:jid_to_string(From)}],
+ [{xmlelement, "reason", [],
+ [{xmlcdata, Reason}]}] ++ ContinueEl}],
+ PasswdEl =
+ case (StateData#state.config)#config.password_protected of
+ true ->
+ [{xmlelement, "password", [],
+ [{xmlcdata, (StateData#state.config)#config.password}]}];
+ _ ->
+ []
+ end,
+ Body =
+ {xmlelement, "body", [],
+ [{xmlcdata,
+ lists:flatten(
+ io_lib:format(
+ translate:translate(
+ Lang,
+ "~s invites you to the room ~s"),
+ [jlib:jid_to_string(From),
+ jlib:jid_to_string({StateData#state.room,
+ StateData#state.host,
+ ""})
+ ])) ++
+ case (StateData#state.config)#config.password_protected of
+ true ->
+ ", " ++
+ translate:translate(Lang, "the password is") ++
+ " '" ++
+ (StateData#state.config)#config.password ++ "'";
+ _ ->
+ ""
+ end ++
+ case Reason of
+ "" -> "";
+ _ -> " (" ++ Reason ++ ") "
+ end
+ }]},
+ Msg =
+ {xmlelement, "message",
+ [{"type", "normal"}],
+ [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], IEl ++ PasswdEl},
+ {xmlelement, "x",
+ [{"xmlns", ?NS_XCONFERENCE},
+ {"jid", jlib:jid_to_string(
+ {StateData#state.room,
+ StateData#state.host,
+ ""})}],
+ [{xmlcdata, Reason}]},
+ Body]},
+ route_stanza(StateData#state.jid, JID, Msg),
+ JID
+ end.
+
+%% Handle a message sent to the room by a non-participant.
+%% If it is a decline, send to the inviter.
+%% Otherwise, an error message is sent to the sender.
+handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From) ->
+ case catch check_decline_invitation(Packet) of
+ {true, Decline_data} ->
+ send_decline_invitation(Decline_data, StateData#state.jid, From);
+ _ ->
+ send_error_only_occupants(Packet, Lang, StateData#state.jid, From)
+ end.
+
+%% Check in the packet is a decline.
+%% If so, also returns the splitted packet.
+%% This function must be catched,
+%% because it crashes when the packet is not a decline message.
+check_decline_invitation(Packet) ->
+ {xmlelement, "message", _, _} = Packet,
+ XEl = xml:get_subtag(Packet, "x"),
+ ?NS_MUC_USER = xml:get_tag_attr_s("xmlns", XEl),
+ DEl = xml:get_subtag(XEl, "decline"),
+ ToString = xml:get_tag_attr_s("to", DEl),
+ ToJID = jlib:string_to_jid(ToString),
+ {true, {Packet, XEl, DEl, ToJID}}.
+
+%% Send the decline to the inviter user.
+%% The original stanza must be slightly modified.
+send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) ->
+ FromString = jlib:jid_to_string(jlib:jid_remove_resource(FromJID)),
+ {xmlelement, "decline", DAttrs, DEls} = DEl,
+ DAttrs2 = lists:keydelete("to", 1, DAttrs),
+ DAttrs3 = [{"from", FromString} | DAttrs2],
+ DEl2 = {xmlelement, "decline", DAttrs3, DEls},
+ XEl2 = replace_subelement(XEl, DEl2),
+ Packet2 = replace_subelement(Packet, XEl2),
+ route_stanza(RoomJID, ToJID, Packet2).
+
+%% Given an element and a new subelement,
+%% replace the instance of the subelement in element with the new subelement.
+replace_subelement({xmlelement, Name, Attrs, SubEls}, NewSubEl) ->
+ {_, NameNewSubEl, _, _} = NewSubEl,
+ SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl),
+ {xmlelement, Name, Attrs, SubEls2}.
+
+send_error_only_occupants(Packet, Lang, RoomJID, From) ->
+ ErrText = "Only occupants are allowed to send messages to the conference",
+ Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
+ route_stanza(RoomJID, From, Err).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% 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 ->
+ mod_muc_log:add_to_log(
+ StateData#state.server_host, Type, Data,
+ StateData#state.jid, make_opts(StateData));
+ false ->
+ ok
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Users number checking
+
+tab_add_online_user(JID, StateData) ->
+ {LUser, LServer, LResource} = jlib:jid_tolower(JID),
+ US = {LUser, LServer},
+ Room = StateData#state.room,
+ Host = StateData#state.host,
+ catch ets:insert(
+ muc_online_users,
+ #muc_online_users{us = US, resource = LResource, room = Room, host = Host}).
+
+
+tab_remove_online_user(JID, StateData) ->
+ {LUser, LServer, LResource} = jlib:jid_tolower(JID),
+ US = {LUser, LServer},
+ Room = StateData#state.room,
+ Host = StateData#state.host,
+ catch ets:delete_object(
+ muc_online_users,
+ #muc_online_users{us = US, resource = LResource, room = Room, host = Host}).
+
+tab_count_user(JID) ->
+ {LUser, LServer, _} = jlib:jid_tolower(JID),
+ US = {LUser, LServer},
+ case catch ets:select(
+ muc_online_users,
+ [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) of
+ Res when is_list(Res) ->
+ length(Res);
+ _ ->
+ 0
+ end.
+
+element_size(El) ->
+ size(xml:element_to_binary(El)).
+
+route_stanza(From, To, El) ->
+ case mod_muc:is_broadcasted(From#jid.lserver) of
+ true ->
+ #jid{luser = LUser, lserver = LServer} = To,
+ case ejabberd_cluster:get_node({LUser, LServer}) of
+ Node when Node == node() ->
+ ejabberd_router:route(From, To, El);
+ _ ->
+ ok
+ end;
+ false ->
+ ejabberd_router:route(From, To, El)
+ end.