diff options
author | Evgeniy Khramtsov <ekhramtsov@process-one.net> | 2012-05-03 18:34:53 +1000 |
---|---|---|
committer | Evgeniy Khramtsov <ekhramtsov@process-one.net> | 2012-05-03 18:34:53 +1000 |
commit | db8bd0126b31ec5ffc4363ae7b8a08d504231081 (patch) | |
tree | f75074c87129cde71648c82dafe23d17a4d2abed /src | |
parent | Do 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.erl | 2332 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.erl | 8452 |
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. |