diff options
Diffstat (limited to 'src/mod_muc')
-rw-r--r-- | src/mod_muc/Makefile.in | 2 | ||||
-rw-r--r-- | src/mod_muc/mod_muc.erl | 1985 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_log.erl | 1661 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.erl | 7043 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.hrl | 146 |
5 files changed, 5667 insertions, 5170 deletions
diff --git a/src/mod_muc/Makefile.in b/src/mod_muc/Makefile.in index 41315ad29..5ede5e521 100644 --- a/src/mod_muc/Makefile.in +++ b/src/mod_muc/Makefile.in @@ -14,7 +14,7 @@ EFLAGS += -pz .. # make debug=true to compile Erlang module with debug informations. ifdef debug - EFLAGS+=+debug_info +export_all + EFLAGS+=+debug_info endif ifeq (@transient_supervisors@, false) diff --git a/src/mod_muc/mod_muc.erl b/src/mod_muc/mod_muc.erl index 1f40ca0a4..09fe95bee 100644 --- a/src/mod_muc/mod_muc.erl +++ b/src/mod_muc/mod_muc.erl @@ -25,147 +25,131 @@ %%%---------------------------------------------------------------------- -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]). +-export([start_link/2, start/2, stop/1, export/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]). +-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 = {<<"">>, <<"">>} :: {binary(), binary()} | + {'_', binary()}, + opts = [] :: list() | '_'}). + +-record(muc_online_room, + {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', + pid = self() :: pid() | '$2' | '_'}). --record(muc_room, {name_host, opts}). --record(muc_online_room, {name_host, pid}). --record(muc_registered, {us_host, nick}). +-record(muc_registered, + {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', + nick = <<"">> :: binary()}). --record(state, {host, - server_host, - access, - history_size, - persist_history, - default_room_opts, - room_shaper}). +-record(state, + {host = <<"">> :: binary(), + server_host = <<"">> :: binary(), + access = {none, none, none, none} :: {atom(), atom(), atom(), atom()}, + history_size = 20 :: non_neg_integer(), + persist_history = false :: boolean(), + default_room_opts = [] :: list(), + room_shaper = none :: shaper: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], []). + 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]}, + 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), + 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 + {wait, Rooms}. shutdown_rooms(Host) -> - MyHost = gen_mod:get_module_opt_host(Host, mod_muc, "conference.@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], + [{#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@"), + 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). + [{#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 = 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. + room_destroyed(Host, Room, Pid, ServerHost) -> catch gen_mod:get_module_proc(ServerHost, ?PROCNAME) ! - {room_destroyed, {Room, Host}, Pid}, + {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@"), + 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}). + gen_server:call({Proc, Node}, + {create, Name, From, Nick, Opts}). store_room(ServerHost, Host, Name, Opts) -> LServer = jlib:nameprep(ServerHost), - store_room(LServer, Host, Name, Opts, gen_mod:db_type(LServer, ?MODULE)). + store_room(LServer, Host, Name, Opts, + gen_mod:db_type(LServer, ?MODULE)). store_room(_LServer, Host, Name, Opts, mnesia) -> - F = fun() -> + F = fun () -> mnesia:write(#muc_room{name_host = {Name, Host}, opts = Opts}) end, @@ -174,143 +158,132 @@ store_room(LServer, Host, Name, Opts, odbc) -> SName = ejabberd_odbc:escape(Name), SHost = ejabberd_odbc:escape(Host), SOpts = ejabberd_odbc:encode_term(Opts), - F = fun() -> - odbc_queries:update_t( - "muc_room", - ["name", "host", "opts"], - [SName, SHost, SOpts], - ["name='", SName, "' and host='", SHost, "'"]) + F = fun () -> + odbc_queries:update_t(<<"muc_room">>, + [<<"name">>, <<"host">>, <<"opts">>], + [SName, SHost, SOpts], + [<<"name='">>, SName, <<"' and host='">>, + SHost, <<"'">>]) end, ejabberd_odbc:sql_transaction(LServer, F). restore_room(ServerHost, Host, Name) -> LServer = jlib:nameprep(ServerHost), - restore_room(LServer, Host, Name, gen_mod:db_type(LServer, ?MODULE)). + restore_room(LServer, Host, Name, + gen_mod:db_type(LServer, ?MODULE)). restore_room(_LServer, Host, Name, mnesia) -> case catch mnesia:dirty_read(muc_room, {Name, Host}) of - [#muc_room{opts = Opts}] -> - Opts; - _ -> - error + [#muc_room{opts = Opts}] -> Opts; + _ -> error end; restore_room(LServer, Host, Name, odbc) -> SName = ejabberd_odbc:escape(Name), SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query( - LServer, ["select opts from muc_room where name='", - SName, "' and host='", SHost, "';"]) of - {selected, ["opts"], [{Opts}]} -> - ejabberd_odbc:decode_term(Opts); - _ -> - error + case catch ejabberd_odbc:sql_query(LServer, + [<<"select opts from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) + of + {selected, [<<"opts">>], [[Opts]]} -> + opts_to_binary(ejabberd_odbc:decode_term(Opts)); + _ -> error end. forget_room(ServerHost, Host, Name) -> LServer = jlib:nameprep(ServerHost), - forget_room(LServer, Host, Name, gen_mod:db_type(LServer, ?MODULE)). + forget_room(LServer, Host, Name, + gen_mod:db_type(LServer, ?MODULE)). forget_room(_LServer, Host, Name, mnesia) -> - F = fun() -> - mnesia:delete({muc_room, {Name, Host}}) + F = fun () -> mnesia:delete({muc_room, {Name, Host}}) end, mnesia:transaction(F); forget_room(LServer, Host, Name, odbc) -> SName = ejabberd_odbc:escape(Name), SHost = ejabberd_odbc:escape(Host), - F = fun() -> - ejabberd_odbc:sql_query_t( - ["delete from muc_room where name='", - SName, "' and host='", SHost, "';"]) + F = fun () -> + ejabberd_odbc:sql_query_t([<<"delete from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) end, ejabberd_odbc:sql_transaction(LServer, F). -process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> +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; + sub_el = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], + children = 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) -> LServer = jlib:nameprep(ServerHost), - can_use_nick(LServer, Host, JID, Nick, gen_mod:db_type(LServer, ?MODULE)). + can_use_nick(LServer, Host, JID, Nick, + gen_mod:db_type(LServer, ?MODULE)). can_use_nick(_LServer, Host, JID, Nick, mnesia) -> {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 + 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; can_use_nick(LServer, Host, JID, Nick, odbc) -> - SJID = jlib:jid_to_string( - jlib:jid_tolower( - jlib:jid_remove_resource(JID))), + SJID = + jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(JID))), SNick = ejabberd_odbc:escape(Nick), SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query( - LServer, ["select jid from muc_registered ", - "where nick='", SNick, "' and host='", - SHost, "';"]) of - {selected, ["jid"], [{SJID1}]} -> - SJID == SJID1; - _ -> - true + case catch ejabberd_odbc:sql_query(LServer, + [<<"select jid from muc_registered ">>, + <<"where nick='">>, SNick, + <<"' and host='">>, SHost, <<"';">>]) + of + {selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1; + _ -> true 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). + 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. +node_down(_) -> ok. -copy_rooms('$end_of_table') -> - 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 + [#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)). @@ -318,1015 +291,1031 @@ copy_rooms(Key) -> %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Host, Opts]) -> - MyHost = gen_mod:get_opt_host(Host, Opts, "conference.@HOST@"), + MyHost = gen_mod:get_opt_host(Host, Opts, + <<"conference.@HOST@">>), case gen_mod:db_type(Opts) of - mnesia -> - update_muc_online_table(), - update_tables(MyHost), - 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:add_table_index(muc_registered, nick); - _ -> - ok + mnesia -> + 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)}]), + update_tables(), + mnesia:add_table_index(muc_registered, nick); + _ -> ok end, mnesia:create_table(muc_online_room, - [{ram_copies, [node()]}, - {local_content, true}, + [{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}]), + mnesia:add_table_copy(muc_online_room, node(), + ram_copies), + catch ets:new(muc_online_users, + [bag, named_table, public, {keypos, 2}]), mnesia:subscribe(system), - 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), + Access = gen_mod:get_opt(access, Opts, + fun(A) when is_atom(A) -> A end, all), + AccessCreate = gen_mod:get_opt(access_create, Opts, + fun(A) when is_atom(A) -> A end, all), + AccessAdmin = gen_mod:get_opt(access_admin, Opts, + fun(A) when is_atom(A) -> A end, + none), + AccessPersistent = gen_mod:get_opt(access_persistent, Opts, + fun(A) when is_atom(A) -> A end, + all), + HistorySize = gen_mod:get_opt(history_size, Opts, + fun(I) when is_integer(I), I>=0 -> I end, + 20), + PersistHistory = gen_mod:get_opt(persist_history, Opts, + fun(B) when is_boolean(B) -> B end, + false), + DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, + fun(L) when is_list(L) -> L end, + []), + RoomShaper = gen_mod:get_opt(room_shaper, Opts, + fun(A) when is_atom(A) -> A end, + 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), + 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 -%%-------------------------------------------------------------------- + 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}}. + 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, +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) -> + room_shaper = RoomShaper} = + State) -> ?DEBUG("MUC: create new room '~s'~n", [Room]), NewOpts = case Opts of - default -> DefOpts; - _ -> Opts + default -> DefOpts; + _ -> Opts end, - {ok, Pid} = mod_muc_room:start( - Host, ServerHost, Access, - Room, HistorySize, PersistHistory, - RoomShaper, From, - Nick, NewOpts), + {ok, Pid} = mod_muc_room:start(Host, ServerHost, Access, + Room, HistorySize, PersistHistory, + RoomShaper, From, Nick, NewOpts), 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_cast(_Msg, State) -> {noreply, State}. + handle_info({route, From, To, Packet}, - #state{host = Host, - server_host = ServerHost, - access = Access, - default_room_opts = DefRoomOpts, + #state{host = Host, server_host = ServerHost, + access = Access, default_room_opts = DefRoomOpts, history_size = HistorySize, persist_history = PersistHistory, - room_shaper = RoomShaper} = State) -> + 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} + 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, + 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 + 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. -%%-------------------------------------------------------------------- +handle_info(_Info, State) -> {noreply, State}. + 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_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}. +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]}, + 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), + 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, +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) + allow -> + do_route1(Host, ServerHost, Access, HistorySize, + PersistHistory, RoomShaper, From, To, Packet, + DefRoomOpts); + _ -> + #xmlel{attrs = Attrs} = 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, +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, + #xmlel{name = Name, attrs = Attrs} = 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( - ServerHost, 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( - ServerHost, 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 + <<"">> -> + 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 = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + 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 = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + iq_get_register_info(ServerHost, + 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(ServerHost, Host, From, + SubEl, Lang) + of + {result, IQRes} -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, + XMLNS}], + children = 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; - "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; + #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, + lang = Lang, sub_el = _SubEl} = + IQ -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"vCard">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + 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 = + [#xmlel{name = <<"unique">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_UNIQUE}], + children = + [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; _ -> - 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) + 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; - _ -> - 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)), + 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; - [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; + _ -> + 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) -> +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 + allow -> + byte_size(RoomID) =< + gen_mod:get_module_opt(ServerHost, ?MODULE, max_room_id, + fun(infinity) -> infinity; + (I) when is_integer(I), I>0 -> I + end, infinity); + _ -> false end. get_rooms(ServerHost, Host) -> LServer = jlib:nameprep(ServerHost), - get_rooms(LServer, Host, gen_mod:db_type(LServer, ?MODULE)). + get_rooms(LServer, Host, + gen_mod:db_type(LServer, ?MODULE)). get_rooms(_LServer, Host, mnesia) -> - case catch mnesia:dirty_select( - muc_room, [{#muc_room{name_host = {'_', Host}, _ = '_'}, - [], - ['$_']}]) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]), - []; - Rs -> - Rs + case catch mnesia:dirty_select(muc_room, + [{#muc_room{name_host = {'_', Host}, + _ = '_'}, + [], ['$_']}]) + of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; + Rs -> Rs end; get_rooms(LServer, Host, odbc) -> SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query( - LServer, ["select name, opts from muc_room ", - "where host='", SHost, "';"]) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]), - []; - {selected, ["name", "opts"], RoomOpts} -> - lists:map( - fun({Room, Opts}) -> - #muc_room{name_host = {Room, Host}, - opts = ejabberd_odbc:decode_term(Opts)} - end, RoomOpts) + case catch ejabberd_odbc:sql_query(LServer, + [<<"select name, opts from muc_room ">>, + <<"where host='">>, SHost, <<"';">>]) + of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; + {selected, [<<"name">>, <<"opts">>], RoomOpts} -> + lists:map(fun ([Room, Opts]) -> + #muc_room{name_host = {Room, Host}, + opts = opts_to_binary( + ejabberd_odbc:decode_term(Opts))} + end, + RoomOpts) end. -load_permanent_rooms(Host, ServerHost, Access, HistorySize, PersistHistory, RoomShaper) -> - 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), - register_room(Host, Room, Pid); - _ -> - ok - end; - _ -> - ok - end; - _ -> - ok - end - end, get_rooms(ServerHost, Host)). +load_permanent_rooms(Host, ServerHost, Access, + HistorySize, PersistHistory, RoomShaper) -> + 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), + register_room(Host, Room, Pid); + _ -> ok + end; + _ -> ok + end; + _ -> ok + end + end, + get_rooms(ServerHost, Host)). start_new_room(Host, ServerHost, Access, Room, - HistorySize, PersistHistory, RoomShaper, From, - Nick, DefRoomOpts) -> + 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 restore_room(ServerHost, Room, Host) of - error -> - ?DEBUG("MUC: open new room '~s'~n", [Room]), - mod_muc_room:start(Host, ServerHost, Access, - Room, HistorySize, PersistHistory, - RoomShaper, From, - Nick, DefRoomOpts); - Opts -> - ?DEBUG("MUC: restore room '~s'~n", [Room]), - mod_muc_room:start(Host, ServerHost, Access, - Room, HistorySize, PersistHistory, - RoomShaper, Opts) - end + {ok, RoomState} -> + ?DEBUG("MUC: restore room '~s' from other node~n", + [Room]), + mod_muc_room:start(normal_state, RoomState); + error -> + case restore_room(ServerHost, Room, Host) of + error -> + ?DEBUG("MUC: open new room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, Room, + HistorySize, PersistHistory, RoomShaper, + From, Nick, DefRoomOpts); + Opts -> + ?DEBUG("MUC: restore room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, Room, + HistorySize, PersistHistory, RoomShaper, + Opts) + end end. register_room(Host, Room, Pid) -> - F = fun() -> - mnesia:write(#muc_online_room{name_host = {Room, Host}, - pid = Pid}) - end, + 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 + Node when Node /= node() -> + 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 -> + 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}], []}]. - + [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"type">>, <<"text">>}, + {<<"name">>, + translate:translate(Lang, <<"Chatrooms">>)}], + children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC_UNIQUE}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_REGISTER}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_RSM}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_VCARD}], children = []}]. 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 + 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, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({Name, Host, + <<"">>})}, + {<<"name">>, Desc}], + children = []}}; + _ -> false end - end, get_vh_rooms_all_nodes(Host)); - + 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 + 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, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({Name, Host, + <<"">>})}, + {<<"name">>, Desc}], + children = []}}; + _ -> false end - end, Rooms) ++ RsmOut. + end, + Rooms) + ++ RsmOut. -get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> +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) + 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}} + 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(_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), + {_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 + [] -> []; + [#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), +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) -> +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) -> + 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. +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 + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, Type}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). + iq_get_unique(From) -> - {xmlcdata, sha:sha(term_to_binary([From, now(), randoms:get_string()]))}. + {xmlcdata, + sha:sha(term_to_binary([From, now(), + randoms:get_string()]))}. get_nick(ServerHost, Host, From) -> LServer = jlib:nameprep(ServerHost), - get_nick(LServer, Host, From, gen_mod:db_type(LServer, ?MODULE)). + get_nick(LServer, Host, From, + gen_mod:db_type(LServer, ?MODULE)). get_nick(_LServer, Host, From, mnesia) -> {LUser, LServer, _} = jlib:jid_tolower(From), LUS = {LUser, LServer}, - case catch mnesia:dirty_read(muc_registered, {LUS, Host}) of - {'EXIT', _Reason} -> - error; - [] -> - error; - [#muc_registered{nick = Nick}] -> - Nick + case catch mnesia:dirty_read(muc_registered, + {LUS, Host}) + of + {'EXIT', _Reason} -> error; + [] -> error; + [#muc_registered{nick = Nick}] -> Nick end; get_nick(LServer, Host, From, odbc) -> - SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:jid_tolower( - jlib:jid_remove_resource(From)))), + SJID = + ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query( - LServer, ["select nick from muc_registered where " - "jid='", SJID, "' and host='", SHost, "';"]) of - {selected, ["nick"], [{Nick}]} -> - Nick; - _ -> - error + case catch ejabberd_odbc:sql_query(LServer, + [<<"select nick from muc_registered where " + "jid='">>, + SJID, <<"' and host='">>, SHost, + <<"';">>]) + of + {selected, [<<"nick">>], [[Nick]]} -> Nick; + _ -> error end. iq_get_register_info(ServerHost, Host, From, Lang) -> - {Nick, Registered} = - case get_nick(ServerHost, Host, From) of - error -> - {"", []}; - N -> - {N, [{xmlelement, "registered", [], []}]} - end, + {Nick, Registered} = case get_nick(ServerHost, Host, + From) + of + error -> {<<"">>, []}; + N -> + {N, + [#xmlel{name = <<"registered">>, attrs = [], + children = []}]} + 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)]}]. + [#xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"You need a client that supports x:data " + "to register the nickname">>)}]}, + #xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + <<(translate:translate(Lang, + <<"Nickname Registration at ">>))/binary, + Host/binary>>}]}, + #xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Enter nickname you want to register">>)}]}, + ?XFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>, + Nick)]}]. set_nick(ServerHost, Host, From, Nick) -> LServer = jlib:nameprep(ServerHost), - set_nick(LServer, Host, From, Nick, gen_mod:db_type(LServer, ?MODULE)). + set_nick(LServer, Host, From, Nick, + gen_mod:db_type(LServer, ?MODULE)). set_nick(_LServer, Host, From, Nick, mnesia) -> {LUser, LServer, _} = jlib:jid_tolower(From), LUS = {LUser, LServer}, - F = fun() -> + 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; + <<"">> -> + 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, + if Allow -> + mnesia:write(#muc_registered{us_host = {LUS, Host}, + nick = Nick}), + ok; + true -> false + end end end, mnesia:transaction(F); set_nick(LServer, Host, From, Nick, odbc) -> - JID = jlib:jid_to_string( - jlib:jid_tolower( - jlib:jid_remove_resource(From))), + JID = + jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From))), SJID = ejabberd_odbc:escape(JID), SNick = ejabberd_odbc:escape(Nick), SHost = ejabberd_odbc:escape(Host), - F = fun() -> - case Nick of - "" -> - ejabberd_odbc:sql_query_t( - ["delete from muc_registered where ", - "jid='", SJID, "' and host='", Host, "';"]), - ok; - _ -> - Allow = - case ejabberd_odbc:sql_query_t( - ["select jid from muc_registered ", - "where nick='", SNick, "' and host='", - SHost, "';"]) of - {selected, ["jid"], [{J}]} -> - J == JID; - _ -> - true - end, - if Allow -> - odbc_queries:update_t( - "muc_registered", - ["jid", "host", "nick"], - [SJID, SHost, SNick], - ["jid='", SJID, "' and host='", SHost, "'"]), - ok; - true -> - false - end - end - end, + F = fun () -> + case Nick of + <<"">> -> + ejabberd_odbc:sql_query_t([<<"delete from muc_registered where ">>, + <<"jid='">>, SJID, + <<"' and host='">>, Host, + <<"';">>]), + ok; + _ -> + Allow = case + ejabberd_odbc:sql_query_t([<<"select jid from muc_registered ">>, + <<"where nick='">>, + SNick, + <<"' and host='">>, + SHost, <<"';">>]) + of + {selected, [<<"jid">>], [[J]]} -> J == JID; + _ -> true + end, + if Allow -> + odbc_queries:update_t(<<"muc_registered">>, + [<<"jid">>, <<"host">>, + <<"nick">>], + [SJID, SHost, SNick], + [<<"jid='">>, SJID, + <<"' and host='">>, SHost, + <<"'">>]), + ok; + true -> false + end + end + end, ejabberd_odbc:sql_transaction(LServer, F). -iq_set_register_info(ServerHost, Host, From, Nick, Lang) -> +iq_set_register_info(ServerHost, Host, From, Nick, + Lang) -> case set_nick(ServerHost, Host, From, Nick) of - {atomic, ok} -> - {result, []}; - {atomic, false} -> - ErrText = "That nickname is registered by another person", - {error, ?ERRT_CONFLICT(Lang, ErrText)}; - _ -> - {error, ?ERR_INTERNAL_SERVER_ERROR} + {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(ServerHost, 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(ServerHost, Host, - From, Nick, Lang); - _ -> - ErrText = "You must fill in field \"Nickname\" in the form", - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)} - end - end; +process_iq_register_set(ServerHost, Host, From, SubEl, + Lang) -> + #xmlel{children = Els} = SubEl, + case xml:get_subtag(SubEl, <<"remove">>) of + false -> + case xml:remove_cdata(Els) of + [#xmlel{name = <<"x">>} = 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}; _ -> - {error, ?ERR_BAD_REQUEST} - end; - _ -> - {error, ?ERR_BAD_REQUEST} - end; - _ -> - iq_set_register_info(ServerHost, Host, From, "", Lang) + case lists:keysearch(<<"nick">>, 1, XData) of + {value, {_, [Nick]}} when Nick /= <<"">> -> + iq_set_register_info(ServerHost, 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(ServerHost, 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"}]}]. - + [#xmlel{name = <<"FN">>, attrs = [], + children = [{xmlcdata, <<"ejabberd/mod_muc">>}]}, + #xmlel{name = <<"URL">>, attrs = [], + children = [{xmlcdata, ?EJABBERD_URI}]}, + #xmlel{name = <<"DESC">>, attrs = [], + children = + [{xmlcdata, + <<(translate:translate(Lang, + <<"ejabberd MUC module">>))/binary, + "\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)). + 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)), + 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). + [{'==', {element, 2, '$1'}, Host}], ['$_']}]). + +opts_to_binary(Opts) -> + lists:map( + fun({title, Title}) -> + {title, iolist_to_binary(Title)}; + ({description, Desc}) -> + {description, iolist_to_binary(Desc)}; + ({password, Pass}) -> + {password, iolist_to_binary(Pass)}; + ({subject, Subj}) -> + {subject, iolist_to_binary(Subj)}; + ({subject_author, Author}) -> + {subject_author, iolist_to_binary(Author)}; + ({affiliations, Affs}) -> + {affiliations, lists:map( + fun({{U, S, R}, Aff}) -> + NewAff = + case Aff of + {A, Reason} -> + {A, iolist_to_binary(Reason)}; + _ -> + Aff + end, + {{iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(R)}, + NewAff} + end, Affs)}; + ({captcha_whitelist, CWList}) -> + {captcha_whitelist, lists:map( + fun({U, S, R}) -> + {iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(R)} + end, CWList)}; + (Opt) -> + Opt + end, Opts). + +update_tables() -> + update_muc_online_table(), + update_muc_room_table(), + update_muc_registered_table(). update_muc_online_table() -> - case catch mnesia:table_info(muc_online_room, local_content) of - false -> - mnesia:delete_table(muc_online_room); - _ -> - ok + 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) -> +update_muc_room_table() -> 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) + Fields -> + ejabberd_config:convert_table_to_binary( + muc_room, Fields, set, + fun(#muc_room{name_host = {N, _}}) -> N end, + fun(#muc_room{name_host = {N, H}, + opts = Opts} = R) -> + R#muc_room{name_host = {iolist_to_binary(N), + iolist_to_binary(H)}, + opts = opts_to_binary(Opts)} + end); + _ -> + ?INFO_MSG("Recreating muc_room table", []), + mnesia:transform_table(muc_room, ignore, Fields) end. - -update_muc_registered_table(Host) -> +update_muc_registered_table() -> 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) + Fields -> + ejabberd_config:convert_table_to_binary( + muc_registered, Fields, set, + fun(#muc_registered{us_host = {_, H}}) -> H end, + fun(#muc_registered{us_host = {{U, S}, H}, + nick = Nick} = R) -> + R#muc_registered{us_host = {{iolist_to_binary(U), + iolist_to_binary(S)}, + iolist_to_binary(H)}, + nick = iolist_to_binary(Nick)} + end); + _ -> + ?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 + case ejabberd_router:get_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) + true -> node(); + false -> ejabberd_cluster:get_node(Key) end; -get_node(RoomHost) -> - get_node({"", RoomHost}). +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) + true -> node(); + false -> ejabberd_cluster:get_node_new(Key) end; get_node_new(RoomHost) -> - get_node_new({"", RoomHost}). + get_node_new({<<"">>, RoomHost}). get_nodes(RoomHost) -> case is_broadcasted(RoomHost) of - true -> - [node()]; - false -> - ejabberd_cluster:get_nodes() + 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 + 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. + +export(_Server) -> + [{muc_room, + fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> + case str:suffix(Host, RoomHost) of + true -> + SName = ejabberd_odbc:escape(Name), + SRoomHost = ejabberd_odbc:escape(RoomHost), + SOpts = ejabberd_odbc:encode_term(Opts), + [[<<"delete from muc_room where name='">>, SName, + <<"' and host='">>, SRoomHost, <<"';">>], + [<<"insert into muc_room(name, host, opts) ", + "values (">>, + <<"'">>, SName, <<"', '">>, SRoomHost, + <<"', '">>, SOpts, <<"');">>]]; + false -> + [] + end + end}, + {muc_registered, + fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, + nick = Nick}) -> + case str:suffix(Host, RoomHost) of + true -> + SJID = ejabberd_odbc:escape( + jlib:jid_to_string( + jlib:make_jid(U, S, <<"">>))), + SNick = ejabberd_odbc:escape(Nick), + SRoomHost = ejabberd_odbc:escape(RoomHost), + [[<<"delete from muc_registered where jid='">>, + SJID, <<"' and host='">>, SRoomHost, <<"';">>], + [<<"insert into muc_registered(jid, host, " + "nick) values ('">>, + SJID, <<"', '">>, SRoomHost, <<"', '">>, SNick, + <<"');">>]]; + false -> + [] + end + end}]. diff --git a/src/mod_muc/mod_muc_log.erl b/src/mod_muc/mod_muc_log.erl index 47d4a3c96..e06795ee3 100644 --- a/src/mod_muc/mod_muc_log.erl +++ b/src/mod_muc/mod_muc_log.erl @@ -25,66 +25,60 @@ %%%---------------------------------------------------------------------- -module(mod_muc_log). + -author('badlop@process-one.net'). -behaviour(gen_server). + -behaviour(gen_mod). %% API --export([start_link/2, - start/2, - stop/1, - check_access_log/2, - add_to_log/5]). +-export([start_link/2, start/2, stop/1, + check_access_log/2, add_to_log/5]). %% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). -include("ejabberd.hrl"). + -include("jlib.hrl"). + -include("mod_muc_room.hrl"). -%% Copied from mod_muc/mod_muc.erl --record(muc_online_room, {name_host, pid}). +-record(muc_online_room, {name_host = {<<>>, <<>>} :: {binary(), binary()}, + pid = self() :: pid()}). -define(T(Text), translate:translate(Lang, Text)). --define(PROCNAME, ejabberd_mod_muc_log). --record(room, {jid, title, subject, subject_author, config}). +-define(PROCNAME, ejabberd_mod_muc_log). --record(logstate, {host, - out_dir, - dir_type, - dir_name, - file_format, - css_file, - access, - lang, - timezone, - spam_prevention, - top_link}). +-record(room, {jid = <<"">> :: binary(), + title = <<"">> :: binary(), + subject = <<"">> :: binary(), + subject_author = <<"">> :: binary(), + config = [] :: list()}). + +-record(logstate, {host = <<"">> :: binary(), + out_dir = <<"">> :: binary(), + dir_type = subdirs :: subdirs | plain, + dir_name = room_jid :: room_jid| room_name, + file_format = html :: html | plaintext, + css_file = false :: false | binary(), + access :: atom(), + lang = <<"">> :: binary(), + timezone = local :: local | universal, + spam_prevention = true :: boolean(), + top_link = {<<>>, <<>>} :: {binary(), binary()}}). -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link(Host, Opts) -> Proc = get_proc_name(Host), gen_server:start_link(Proc, ?MODULE, [Host, Opts], []). start(Host, Opts) -> Proc = get_proc_name(Host), - ChildSpec = - {Proc, - {?MODULE, start_link, [Host, Opts]}, - temporary, - 1000, - worker, - [?MODULE]}, + ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, + temporary, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> @@ -98,837 +92,1019 @@ add_to_log(Host, Type, Data, Room, Opts) -> check_access_log(Host, From) -> case catch gen_server:call(get_proc_name(Host), - {check_access_log, Host, From}) of - {'EXIT', _Error} -> - deny; - Res -> - Res + {check_access_log, Host, From}) + of + {'EXIT', _Error} -> deny; + Res -> Res end. %%==================================================================== %% gen_server callbacks %%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Host, Opts]) -> - OutDir = gen_mod:get_opt(outdir, Opts, "www/muc"), - DirType = gen_mod:get_opt(dirtype, Opts, subdirs), - DirName = gen_mod:get_opt(dirname, Opts, room_jid), - FileFormat = gen_mod:get_opt(file_format, Opts, html), % Allowed values: html|plaintext - CSSFile = gen_mod:get_opt(cssfile, Opts, false), - AccessLog = gen_mod:get_opt(access_log, Opts, muc_admin), - Timezone = gen_mod:get_opt(timezone, Opts, local), - Top_link = gen_mod:get_opt(top_link, Opts, {"/", "Home"}), - NoFollow = gen_mod:get_opt(spam_prevention, Opts, true), - Lang = case ejabberd_config:get_local_option({language, Host}) of - undefined -> - case ejabberd_config:get_global_option(language) of - undefined -> "en"; - L -> L - end; - L -> L - end, - {ok, #logstate{host = Host, - out_dir = OutDir, - dir_type = DirType, - dir_name = DirName, - file_format = FileFormat, - css_file = CSSFile, - access = AccessLog, - lang = Lang, - timezone = Timezone, - spam_prevention = NoFollow, - top_link = Top_link}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- + OutDir = gen_mod:get_opt(outdir, Opts, + fun iolist_to_binary/1, + <<"www/muc">>), + DirType = gen_mod:get_opt(dirtype, Opts, + fun(subdirs) -> subdirs; + (plain) -> plain + end, subdirs), + DirName = gen_mod:get_opt(dirname, Opts, + fun(room_jid) -> room_jid; + (room_name) -> room_name + end, room_jid), + FileFormat = gen_mod:get_opt(file_format, Opts, + fun(html) -> html; + (plaintext) -> plaintext + end, html), + CSSFile = gen_mod:get_opt(cssfile, Opts, + fun iolist_to_binary/1, + false), + AccessLog = gen_mod:get_opt(access_log, Opts, + fun(A) when is_atom(A) -> A end, + muc_admin), + Timezone = gen_mod:get_opt(timezone, Opts, + fun(local) -> local; + (universal) -> universal + end, local), + Top_link = gen_mod:get_opt(top_link, Opts, + fun({S1, S2}) -> + {iolist_to_binary(S1), + iolist_to_binary(S2)} + end, {<<"/">>, <<"Home">>}), + NoFollow = gen_mod:get_opt(spam_prevention, Opts, + fun(B) when is_boolean(B) -> B end, + true), + Lang = ejabberd_config:get_local_option( + {language, Host}, + fun iolist_to_binary/1, + ?MYLANG), + {ok, + #logstate{host = Host, out_dir = OutDir, + dir_type = DirType, dir_name = DirName, + file_format = FileFormat, css_file = CSSFile, + access = AccessLog, lang = Lang, timezone = Timezone, + spam_prevention = NoFollow, top_link = Top_link}}. + handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> case catch add_to_log2(Type, Data, Room, Opts, State) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]); - _ -> - ok + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); + _ -> ok end, {noreply, State}; -handle_cast(_Msg, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info(_Info, State) -> - {noreply, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. +handle_cast(_Msg, State) -> {noreply, State}. -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +handle_info(_Info, State) -> {noreply, State}. -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> - case {xml:get_subtag(Packet, "subject"), xml:get_subtag(Packet, "body")} of - {false, false} -> - ok; - {false, SubEl} -> - Message = {body, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State); - {SubEl, _} -> - Message = {subject, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State) - end; - -add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) -> - add_message_to_log("", roomconfig_change, Room, Opts, State); +terminate(_Reason, _State) -> ok. -add_to_log2(roomconfig_change_enabledlogging, Occupants, Room, Opts, State) -> - add_message_to_log("", {roomconfig_change, Occupants}, Room, Opts, State); - -add_to_log2(room_existence, NewStatus, Room, Opts, State) -> - add_message_to_log("", {room_existence, NewStatus}, Room, Opts, State); - -add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, State) -> - add_message_to_log(NewNick, {nickchange, OldNick}, Room, Opts, State); +code_change(_OldVsn, State, _Extra) -> {ok, State}. +add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> + case {xml:get_subtag(Packet, <<"subject">>), + xml:get_subtag(Packet, <<"body">>)} + of + {false, false} -> ok; + {false, SubEl} -> + Message = {body, xml:get_tag_cdata(SubEl)}, + add_message_to_log(Nick, Message, Room, Opts, State); + {SubEl, _} -> + Message = {subject, xml:get_tag_cdata(SubEl)}, + add_message_to_log(Nick, Message, Room, Opts, State) + end; +add_to_log2(roomconfig_change, _Occupants, Room, Opts, + State) -> + add_message_to_log(<<"">>, roomconfig_change, Room, + Opts, State); +add_to_log2(roomconfig_change_enabledlogging, Occupants, + Room, Opts, State) -> + add_message_to_log(<<"">>, + {roomconfig_change, Occupants}, Room, Opts, State); +add_to_log2(room_existence, NewStatus, Room, Opts, + State) -> + add_message_to_log(<<"">>, {room_existence, NewStatus}, + Room, Opts, State); +add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, + State) -> + add_message_to_log(NewNick, {nickchange, OldNick}, Room, + Opts, State); add_to_log2(join, Nick, Room, Opts, State) -> add_message_to_log(Nick, join, Room, Opts, State); - add_to_log2(leave, {Nick, Reason}, Room, Opts, State) -> case Reason of - "" -> add_message_to_log(Nick, leave, Room, Opts, State); - _ -> add_message_to_log(Nick, {leave, Reason}, Room, Opts, State) + <<"">> -> + add_message_to_log(Nick, leave, Room, Opts, State); + _ -> + add_message_to_log(Nick, {leave, Reason}, Room, Opts, + State) end; - -add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, State) -> - add_message_to_log(Nick, {kickban, Code, Reason}, Room, Opts, State). - +add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, + State) -> + add_message_to_log(Nick, {kickban, Code, Reason}, Room, + Opts, State). %%---------------------------------------------------------------------- %% Core -build_filename_string(TimeStamp, OutDir, RoomJID, DirType, DirName, FileFormat) -> +build_filename_string(TimeStamp, OutDir, RoomJID, + DirType, DirName, FileFormat) -> {{Year, Month, Day}, _Time} = TimeStamp, - - %% Directory and file names - {Dir, Filename, Rel} = - case DirType of - subdirs -> - SYear = lists:flatten(io_lib:format("~4..0w", [Year])), - SMonth = lists:flatten(io_lib:format("~2..0w", [Month])), - SDay = lists:flatten(io_lib:format("~2..0w", [Day])), - {filename:join(SYear, SMonth), SDay, "../.."}; - plain -> - Date = lists:flatten( - io_lib:format("~4..0w-~2..0w-~2..0w", - [Year, Month, Day])), - {"", Date, "."} - end, - + {Dir, Filename, Rel} = case DirType of + subdirs -> + SYear = + iolist_to_binary(io_lib:format("~4..0w", + [Year])), + SMonth = + iolist_to_binary(io_lib:format("~2..0w", + [Month])), + SDay = iolist_to_binary(io_lib:format("~2..0w", + [Day])), + {fjoin([SYear, SMonth]), SDay, + <<"../..">>}; + plain -> + Date = + iolist_to_binary(io_lib:format("~4..0w-~2..0w-~2..0w", + [Year, + Month, + Day])), + {<<"">>, Date, <<".">>} + end, RoomString = case DirName of - room_jid -> RoomJID; - room_name -> get_room_name(RoomJID) + room_jid -> RoomJID; + room_name -> get_room_name(RoomJID) end, Extension = case FileFormat of - html -> ".html"; - plaintext -> ".txt" + html -> <<".html">>; + plaintext -> <<".txt">> end, - Fd = filename:join([OutDir, RoomString, Dir]), - Fn = filename:join([Fd, Filename ++ Extension]), - Fnrel = filename:join([Rel, Dir, Filename ++ Extension]), + Fd = fjoin([OutDir, RoomString, Dir]), + Fn = fjoin([Fd, <<Filename/binary, Extension/binary>>]), + Fnrel = fjoin([Rel, Dir, <<Filename/binary, Extension/binary>>]), {Fd, Fn, Fnrel}. get_room_name(RoomJID) -> - JID = jlib:string_to_jid(RoomJID), - JID#jid.user. + JID = jlib:string_to_jid(RoomJID), JID#jid.user. -%% calculate day before get_timestamp_daydiff(TimeStamp, Daydiff) -> {Date1, HMS} = TimeStamp, - Date2 = calendar:gregorian_days_to_date( - calendar:date_to_gregorian_days(Date1) + Daydiff), + Date2 = + calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date1) + + Daydiff), {Date2, HMS}. -%% Try to close the previous day log, if it exists close_previous_log(Fn, Images_dir, FileFormat) -> case file:read_file_info(Fn) of - {ok, _} -> - {ok, F} = file:open(Fn, [append]), - write_last_lines(F, Images_dir, FileFormat), - file:close(F); - _ -> ok + {ok, _} -> + {ok, F} = file:open(Fn, [append]), + write_last_lines(F, Images_dir, FileFormat), + file:close(F); + _ -> ok end. -write_last_lines(_, _, plaintext) -> - ok; +write_last_lines(_, _, plaintext) -> ok; write_last_lines(F, Images_dir, _FileFormat) -> - %%fw(F, "<div class=\"legend\">ejabberd/mod_muc log<span class=\"w3c\">"), - fw(F, "<div class=\"legend\">"), - fw(F, " <a href=\"http://www.ejabberd.im\"><img style=\"border:0\" src=\"~s/powered-by-ejabberd.png\" alt=\"Powered by ejabberd\"/></a>", [Images_dir]), - fw(F, " <a href=\"http://www.erlang.org/\"><img style=\"border:0\" src=\"~s/powered-by-erlang.png\" alt=\"Powered by Erlang\"/></a>", [Images_dir]), - fw(F, "<span class=\"w3c\">"), - fw(F, " <a href=\"http://validator.w3.org/check?uri=referer\"><img style=\"border:0;width:88px;height:31px\" src=\"~s/valid-xhtml10.png\" alt=\"Valid XHTML 1.0 Transitional\" /></a>", [Images_dir]), - fw(F, " <a href=\"http://jigsaw.w3.org/css-validator/\"><img style=\"border:0;width:88px;height:31px\" src=\"~s/vcss.png\" alt=\"Valid CSS!\"/></a>", [Images_dir]), - fw(F, "</span></div></body></html>"). - -add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> - #logstate{out_dir = OutDir, - dir_type = DirType, - dir_name = DirName, - file_format = FileFormat, - css_file = CSSFile, - lang = Lang, - timezone = Timezone, - spam_prevention = NoFollow, - top_link = TopLink} = State, + fw(F, <<"<div class=\"legend\">">>), + fw(F, + <<" <a href=\"http://www.ejabberd.im\"><img " + "style=\"border:0\" src=\"~s/powered-by-ejabbe" + "rd.png\" alt=\"Powered by ejabberd\"/></a>">>, + [Images_dir]), + fw(F, + <<" <a href=\"http://www.erlang.org/\"><img " + "style=\"border:0\" src=\"~s/powered-by-erlang" + ".png\" alt=\"Powered by Erlang\"/></a>">>, + [Images_dir]), + fw(F, <<"<span class=\"w3c\">">>), + fw(F, + <<" <a href=\"http://validator.w3.org/check?uri" + "=referer\"><img style=\"border:0;width:88px;h" + "eight:31px\" src=\"~s/valid-xhtml10.png\" " + "alt=\"Valid XHTML 1.0 Transitional\" " + "/></a>">>, + [Images_dir]), + fw(F, + <<" <a href=\"http://jigsaw.w3.org/css-validato" + "r/\"><img style=\"border:0;width:88px;height:" + "31px\" src=\"~s/vcss.png\" alt=\"Valid " + "CSS!\"/></a>">>, + [Images_dir]), + fw(F, <<"</span></div></body></html>">>). + +add_message_to_log(Nick1, Message, RoomJID, Opts, + State) -> + #logstate{out_dir = OutDir, dir_type = DirType, + dir_name = DirName, file_format = FileFormat, + css_file = CSSFile, lang = Lang, timezone = Timezone, + spam_prevention = NoFollow, top_link = TopLink} = + State, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), - Nick2 = htmlize("<"++Nick1++">", FileFormat), + Nick2 = htmlize(<<"<", Nick1/binary, ">">>, FileFormat), Now = now(), TimeStamp = case Timezone of - local -> calendar:now_to_local_time(Now); - universal -> calendar:now_to_universal_time(Now) + local -> calendar:now_to_local_time(Now); + universal -> calendar:now_to_universal_time(Now) end, - {Fd, Fn, _Dir} = build_filename_string(TimeStamp, OutDir, Room#room.jid, DirType, DirName, FileFormat), + {Fd, Fn, _Dir} = build_filename_string(TimeStamp, + OutDir, Room#room.jid, DirType, + DirName, FileFormat), {Date, Time} = TimeStamp, - - %% Open file, create if it does not exist, create parent dirs if needed case file:read_file_info(Fn) of - {ok, _} -> - {ok, F} = file:open(Fn, [append]); - {error, enoent} -> - make_dir_rec(Fd), - {ok, F} = file:open(Fn, [append]), - Datestring = get_dateweek(Date, Lang), - - TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1), - {_FdYesterday, FnYesterday, DatePrev} = - build_filename_string( - TimeStampYesterday, OutDir, Room#room.jid, DirType, DirName, FileFormat), - - TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), - {_FdTomorrow, _FnTomorrow, DateNext} = - build_filename_string( - TimeStampTomorrow, OutDir, Room#room.jid, DirType, DirName, FileFormat), - - HourOffset = calc_hour_offset(TimeStamp), - put_header(F, Room, Datestring, CSSFile, Lang, - HourOffset, DatePrev, DateNext, TopLink, FileFormat), - - Images_dir = filename:join([OutDir, "images"]), - file:make_dir(Images_dir), - create_image_files(Images_dir), - Images_url = case DirType of - subdirs -> "../../../images"; - plain -> "../images" - end, - close_previous_log(FnYesterday, Images_url, FileFormat) + {ok, _} -> {ok, F} = file:open(Fn, [append]); + {error, enoent} -> + make_dir_rec(Fd), + {ok, F} = file:open(Fn, [append]), + Datestring = get_dateweek(Date, Lang), + TimeStampYesterday = get_timestamp_daydiff(TimeStamp, + -1), + {_FdYesterday, FnYesterday, DatePrev} = + build_filename_string(TimeStampYesterday, OutDir, + Room#room.jid, DirType, DirName, + FileFormat), + TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), + {_FdTomorrow, _FnTomorrow, DateNext} = + build_filename_string(TimeStampTomorrow, OutDir, + Room#room.jid, DirType, DirName, + FileFormat), + HourOffset = calc_hour_offset(TimeStamp), + put_header(F, Room, Datestring, CSSFile, Lang, + HourOffset, DatePrev, DateNext, TopLink, FileFormat), + Images_dir = fjoin([OutDir, <<"images">>]), + file:make_dir(Images_dir), + create_image_files(Images_dir), + Images_url = case DirType of + subdirs -> <<"../../../images">>; + plain -> <<"../images">> + end, + close_previous_log(FnYesterday, Images_url, FileFormat) end, - - %% Build message Text = case Message of - roomconfig_change -> - RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), - put_room_config(F, RoomConfig, Lang, FileFormat), - io_lib:format("<font class=\"mrcm\">~s</font><br/>", - [?T("Chatroom configuration modified")]); - {roomconfig_change, Occupants} -> - RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), - put_room_config(F, RoomConfig, Lang, FileFormat), - RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), - put_room_occupants(F, RoomOccupants, Lang, FileFormat), - io_lib:format("<font class=\"mrcm\">~s</font><br/>", - [?T("Chatroom configuration modified")]); - join -> - io_lib:format("<font class=\"mj\">~s ~s</font><br/>", - [Nick, ?T("joins the room")]); - leave -> - io_lib:format("<font class=\"ml\">~s ~s</font><br/>", - [Nick, ?T("leaves the room")]); - {leave, Reason} -> - io_lib:format("<font class=\"ml\">~s ~s: ~s</font><br/>", - [Nick, ?T("leaves the room"), htmlize(Reason,NoFollow,FileFormat)]); - {kickban, "301", ""} -> - io_lib:format("<font class=\"mb\">~s ~s</font><br/>", - [Nick, ?T("has been banned")]); - {kickban, "301", Reason} -> - io_lib:format("<font class=\"mb\">~s ~s: ~s</font><br/>", - [Nick, ?T("has been banned"), htmlize(Reason,FileFormat)]); - {kickban, "307", ""} -> - io_lib:format("<font class=\"mk\">~s ~s</font><br/>", - [Nick, ?T("has been kicked")]); - {kickban, "307", Reason} -> - io_lib:format("<font class=\"mk\">~s ~s: ~s</font><br/>", - [Nick, ?T("has been kicked"), htmlize(Reason,FileFormat)]); - {kickban, "321", ""} -> - io_lib:format("<font class=\"mk\">~s ~s</font><br/>", - [Nick, ?T("has been kicked because of an affiliation change")]); - {kickban, "322", ""} -> - io_lib:format("<font class=\"mk\">~s ~s</font><br/>", - [Nick, ?T("has been kicked because the room has been changed to members-only")]); - {kickban, "332", ""} -> - io_lib:format("<font class=\"mk\">~s ~s</font><br/>", - [Nick, ?T("has been kicked because of a system shutdown")]); - {nickchange, OldNick} -> - io_lib:format("<font class=\"mnc\">~s ~s ~s</font><br/>", - [htmlize(OldNick,FileFormat), ?T("is now known as"), Nick]); - {subject, T} -> - io_lib:format("<font class=\"msc\">~s~s~s</font><br/>", - [Nick, ?T(" has set the subject to: "), htmlize(T,NoFollow,FileFormat)]); - {body, T} -> - case {ejabberd_regexp:run(T, "^/me\s"), Nick} of - {_, ""} -> - io_lib:format("<font class=\"msm\">~s</font><br/>", - [htmlize(T,NoFollow,FileFormat)]); - {match, _} -> - io_lib:format("<font class=\"mne\">~s ~s</font><br/>", - [Nick, string:substr(htmlize(T,FileFormat), 5)]); - {nomatch, _} -> - io_lib:format("<font class=\"mn\">~s</font> ~s<br/>", - [Nick2, htmlize(T,NoFollow,FileFormat)]) - end; - {room_existence, RoomNewExistence} -> - io_lib:format("<font class=\"mrcm\">~s</font><br/>", - [get_room_existence_string(RoomNewExistence, Lang)]) + roomconfig_change -> + RoomConfig = roomconfig_to_string(Room#room.config, + Lang, FileFormat), + put_room_config(F, RoomConfig, Lang, FileFormat), + io_lib:format("<font class=\"mrcm\">~s</font><br/>", + [?T(<<"Chatroom configuration modified">>)]); + {roomconfig_change, Occupants} -> + RoomConfig = roomconfig_to_string(Room#room.config, + Lang, FileFormat), + put_room_config(F, RoomConfig, Lang, FileFormat), + RoomOccupants = roomoccupants_to_string(Occupants, + FileFormat), + put_room_occupants(F, RoomOccupants, Lang, FileFormat), + io_lib:format("<font class=\"mrcm\">~s</font><br/>", + [?T(<<"Chatroom configuration modified">>)]); + join -> + io_lib:format("<font class=\"mj\">~s ~s</font><br/>", + [Nick, ?T(<<"joins the room">>)]); + leave -> + io_lib:format("<font class=\"ml\">~s ~s</font><br/>", + [Nick, ?T(<<"leaves the room">>)]); + {leave, Reason} -> + io_lib:format("<font class=\"ml\">~s ~s: ~s</font><br/>", + [Nick, ?T(<<"leaves the room">>), + htmlize(Reason, NoFollow, FileFormat)]); + {kickban, <<"301">>, <<"">>} -> + io_lib:format("<font class=\"mb\">~s ~s</font><br/>", + [Nick, ?T(<<"has been banned">>)]); + {kickban, <<"301">>, Reason} -> + io_lib:format("<font class=\"mb\">~s ~s: ~s</font><br/>", + [Nick, ?T(<<"has been banned">>), + htmlize(Reason, FileFormat)]); + {kickban, <<"307">>, <<"">>} -> + io_lib:format("<font class=\"mk\">~s ~s</font><br/>", + [Nick, ?T(<<"has been kicked">>)]); + {kickban, <<"307">>, Reason} -> + io_lib:format("<font class=\"mk\">~s ~s: ~s</font><br/>", + [Nick, ?T(<<"has been kicked">>), + htmlize(Reason, FileFormat)]); + {kickban, <<"321">>, <<"">>} -> + io_lib:format("<font class=\"mk\">~s ~s</font><br/>", + [Nick, + ?T(<<"has been kicked because of an affiliation " + "change">>)]); + {kickban, <<"322">>, <<"">>} -> + io_lib:format("<font class=\"mk\">~s ~s</font><br/>", + [Nick, + ?T(<<"has been kicked because the room has " + "been changed to members-only">>)]); + {kickban, <<"332">>, <<"">>} -> + io_lib:format("<font class=\"mk\">~s ~s</font><br/>", + [Nick, + ?T(<<"has been kicked because of a system " + "shutdown">>)]); + {nickchange, OldNick} -> + io_lib:format("<font class=\"mnc\">~s ~s ~s</font><br/>", + [htmlize(OldNick, FileFormat), + ?T(<<"is now known as">>), Nick]); + {subject, T} -> + io_lib:format("<font class=\"msc\">~s~s~s</font><br/>", + [Nick, ?T(<<" has set the subject to: ">>), + htmlize(T, NoFollow, FileFormat)]); + {body, T} -> + case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of + {_, <<"">>} -> + io_lib:format("<font class=\"msm\">~s</font><br/>", + [htmlize(T, NoFollow, FileFormat)]); + {match, _} -> + io_lib:format("<font class=\"mne\">~s ~s</font><br/>", + [Nick, + str:substr(htmlize(T, FileFormat), 5)]); + {nomatch, _} -> + io_lib:format("<font class=\"mn\">~s</font> ~s<br/>", + [Nick2, htmlize(T, NoFollow, FileFormat)]) + end; + {room_existence, RoomNewExistence} -> + io_lib:format("<font class=\"mrcm\">~s</font><br/>", + [get_room_existence_string(RoomNewExistence, + Lang)]) end, {Hour, Minute, Second} = Time, - STime = lists:flatten( - io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second])), + STime = io_lib:format("~2..0w:~2..0w:~2..0w", + [Hour, Minute, Second]), {_, _, Microsecs} = Now, - STimeUnique = io_lib:format("~s.~w", [STime, Microsecs]), - - %% Write message - fw(F, io_lib:format("<a id=\"~s\" name=\"~s\" href=\"#~s\" class=\"ts\">[~s]</a> ", - [STimeUnique, STimeUnique, STimeUnique, STime]) ++ Text, FileFormat), - - %% Close file + STimeUnique = io_lib:format("~s.~w", + [STime, Microsecs]), + fw(F, + list_to_binary( + io_lib:format("<a id=\"~s\" name=\"~s\" href=\"#~s\" " + "class=\"ts\">[~s]</a> ", + [STimeUnique, STimeUnique, STimeUnique, STime]) + ++ Text), + FileFormat), file:close(F), ok. - %%---------------------------------------------------------------------- %% Utilities -get_room_existence_string(created, Lang) -> ?T("Chatroom is created"); -get_room_existence_string(destroyed, Lang) -> ?T("Chatroom is destroyed"); -get_room_existence_string(started, Lang) -> ?T("Chatroom is started"); -get_room_existence_string(stopped, Lang) -> ?T("Chatroom is stopped"). +get_room_existence_string(created, Lang) -> + ?T(<<"Chatroom is created">>); +get_room_existence_string(destroyed, Lang) -> + ?T(<<"Chatroom is destroyed">>); +get_room_existence_string(started, Lang) -> + ?T(<<"Chatroom is started">>); +get_room_existence_string(stopped, Lang) -> + ?T(<<"Chatroom is stopped">>). get_dateweek(Date, Lang) -> Weekday = case calendar:day_of_the_week(Date) of - 1 -> ?T("Monday"); - 2 -> ?T("Tuesday"); - 3 -> ?T("Wednesday"); - 4 -> ?T("Thursday"); - 5 -> ?T("Friday"); - 6 -> ?T("Saturday"); - 7 -> ?T("Sunday") + 1 -> ?T(<<"Monday">>); + 2 -> ?T(<<"Tuesday">>); + 3 -> ?T(<<"Wednesday">>); + 4 -> ?T(<<"Thursday">>); + 5 -> ?T(<<"Friday">>); + 6 -> ?T(<<"Saturday">>); + 7 -> ?T(<<"Sunday">>) end, {Y, M, D} = Date, Month = case M of - 1 -> ?T("January"); - 2 -> ?T("February"); - 3 -> ?T("March"); - 4 -> ?T("April"); - 5 -> ?T("May"); - 6 -> ?T("June"); - 7 -> ?T("July"); - 8 -> ?T("August"); - 9 -> ?T("September"); - 10 -> ?T("October"); - 11 -> ?T("November"); - 12 -> ?T("December") + 1 -> ?T(<<"January">>); + 2 -> ?T(<<"February">>); + 3 -> ?T(<<"March">>); + 4 -> ?T(<<"April">>); + 5 -> ?T(<<"May">>); + 6 -> ?T(<<"June">>); + 7 -> ?T(<<"July">>); + 8 -> ?T(<<"August">>); + 9 -> ?T(<<"September">>); + 10 -> ?T(<<"October">>); + 11 -> ?T(<<"November">>); + 12 -> ?T(<<"December">>) end, - case Lang of - "en" -> io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y]); - "es" -> io_lib:format("~s ~w de ~s de ~w", [Weekday, D, Month, Y]); - _ -> io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y]) - end. + list_to_binary( + case Lang of + <<"en">> -> + io_lib:format("~s, ~s ~w, ~w", [Weekday, Month, D, Y]); + <<"es">> -> + io_lib:format("~s ~w de ~s de ~w", + [Weekday, D, Month, Y]); + _ -> + io_lib:format("~s, ~w ~s ~w", [Weekday, D, Month, Y]) + end). make_dir_rec(Dir) -> - case file:read_file_info(Dir) of - {ok, _} -> - ok; - {error, enoent} -> - DirS = filename:split(Dir), - DirR = lists:sublist(DirS, length(DirS)-1), - make_dir_rec(filename:join(DirR)), - file:make_dir(Dir) + DirS = binary_to_list(Dir), + case file:read_file_info(DirS) of + {ok, _} -> ok; + {error, enoent} -> + DirL = [list_to_binary(F) || F <- filename:split(DirS)], + DirR = lists:sublist(DirL, length(DirL) - 1), + make_dir_rec(fjoin(DirR)), + file:make_dir(DirS) end. - %% {ok, F1}=file:open("valid-xhtml10.png", [read]). %% {ok, F1b}=file:read(F1, 1000000). %% c("../../ejabberd/src/jlib.erl"). %% jlib:encode_base64(F1b). -image_base64("powered-by-erlang.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoAAADN0lEQVRo3u1a" - "P0waURz+rjGRRQ+nUyRCYmJyDPTapDARaSIbTUjt1gVSh8ZW69aBAR0cWLSx" - "CXWp59LR1jbdqKnGxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu" - "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOHo8HkiQxDBXEOjg9" - "PcHc3BxuUSqsI8jR0REAUFGsCCoKFYWCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJ" - "ut9hMQlvdNbUhKWshLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm" - "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1MdG8rSKKXlJ7lHr" - "fjouCut2IrC82BDPbe/gc+xlXez7KxEz63H4lmIN473Rh8Si1BKhRY6aEJI8" - "pLmbjSPN0xOnBBILmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG" - "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFOTYznzpPO+IQi+IK" - "+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN3Olvk4ge/IU98spne+FfGtlslm16nn" - "a8fduntfDscoVjGJqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5" - "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizgwwu5SLK8jllu1t9" - "ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y1m+hSj13WjqOiJKK0qpXKhSFxJAYBv" - "KYaZ9TjWRu4SiWi2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g" - "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJvHsdL+utNYrm2uf" - "ZDVZSkzPKaQkW8kthpyS297BvRdRzR6DdTurJbPy9Ov1K6xr3HBPQuIMowR3" - "asegUyDuU9SuUG+dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q" - "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafIYrizbfESoB+dd9/" - "KF/d/wX3cJvREzl1vAAAAABJRU5ErkJggg=="; - -image_base64("valid-xhtml10.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEAAACiFBMVEUAAADe" - "5+fOezmtra3ejEKlhELvvWO9WlrehELOe3vepaWclHvetVLGc3PerVKcCAj3" - "vVqUjHOUe1JjlL0xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY" - "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoICACEazEhGAgIAACE" - "YzFra2utjELWcznGnEr/7+9jY2POazHOYzGta2NShLVrlL05OUqctdacCADG" - "a2ucAADGpVqUtc61ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe" - "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7/fGxsa9pVqEOSkp" - "Y5xznL29tZxahLXOpVr/99ZrY1L/79ZjUiljSikAOYTvxmMAMYScezmchFqU" - "czGtlFp7c2utjFqUlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC" - "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1mtCMRhzY1L/zmvn" - "vVpSQiHOpVJrUinntVr3zmOEc1L3xmNaWlq1nFo5QkrGWim1lFoISpRSUlK1" - "zt4hWpwASoz///////8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3" - "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUlHtKORh7a1IxIRCU" - "jHtaSiHWrVIpIQhzWinvvVpaQiH/1mPWpVKMe1L/zmP/xmNrUiGErc4YGBj/" - "73PG1ucQWpT/53O9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS" - "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////iP9sSf//dP//////" - "//////////////////////////////////////////8M////////////ef//" - "////////////////////////////////////////////////////////////" - "//////////////////////9d////////////////////////////////////" - "AP//////////////CP//RP//////////////////////////////////////" - "//////////////////////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba" - "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpIvGQGzokvTTA85VH" - "KTpbRoeJnPno/p1+M7t3txj20e/Nzu7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSA" - "NeBDxVmxZZSwEUYkGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb" - "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6VbdtBblesOs+MjmEmz" - "JKNIJWFEfEQTCWNPFKvcKEymjLO1b8bwYQd1hCiiDCl5KsrDCIlhj4fSuvcp" - "fSpgJmyv6dzeZv+nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk" - "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDNE9uzA1YD/5MhnzW" - "1KRqF7mYG8jFYXLcfLpjOe2LA0fuGqQrQHl10sdK0sFcFSOSlzF0BgXQH9h3" - "QZDBI0ccNEhftjXuippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7" - "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woWSRJ0m2ewhvAOUiY" - "YtZtaZL0CZZmtmVOQttLfr/dbveLZodrfrL7W75wG/JjqkQxoNTtNsTKELQp" - "QL6/D5loaSmyTT8TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+" - "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qqmj5bHqVZlJKZIRk" - "1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p1V76DeIs0MX+s3GxRlA1vtw83KhgNp" - "hc1nyErLO5zcvbOsrq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N" - "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4Fsvjmuv662jsWxPZ" - "x2xg890bYMYnTgya7bjmCiEY0qgJ0vMF3c+NoFdPyzxz6V3Uxs3AOWCDchRv" - "OsQtBrbFsrT2fhHEc7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1" - "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3e4HFmYPDl/thYcR" - "Vi8qPEoXVUWBl6FTBFTtnqmKKg5wnlF4wZ1yeLv7TiwXKektE+iDBNicWEyL" - "pnFhfDkpJc3q2khSPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx" - "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdiMOlEOJHw1jOQ4sW" - "t5rQopRkXZhMEi7pmeDCVWBlfUKwhMZ7rsF6elKsvbwiKxgxIdewa3ErsaYo" - "mCVZFYJb0GUu3JqGUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+" - "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx17XhBuDCaOerbt5" - "38erta7TFktLvdHghZcCbcPQO33zIJG9kxF5hoVXnzTzRz0r5js8oTj6uyPk" - "GRf346HOLcasgFexueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl" - "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1juvBSWNFmNWGZy8Lv" - "zUl5PN4JCwyNDzbQ0aAj4Zrjz0FatGJJYhvq4j7mGSpvytGFlZtHf2C4o/28" - "Zu8z7wo7eYPfXysnF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO" - "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+faCSJPJ1bmy6Thyvp" - "ivBmYWgjxPDPAp7JTemY/yGKFEiRt/jG/2P79s8KCwoLCgoLC/khUBA5F0Sf" - "QZ+RYfpNE/4Xosmq7jsZAJsAAAAASUVORK5CYII="; - -image_base64("vcss.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSAAABKVBMVEUAAAAj" - "Ix8MR51ZVUqAdlmdnZ3ejEWLDAuNjY1kiMG0n2d9fX19Ghfrp1FtbW3y39+3" - "Ph6lIRNdXV2qJBFcVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd" - "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T09RRDwsJBG+vr73" - "wV6fkG6eCQRFcLSurq6/X1+ht9nXfz5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5t" - "WCsmWajsz8+Sq9NMPh4hVaY8MRj///////////////////////9MTEyOp9Lu" - "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn82dfz7/zGafDw+f" - "Dw+zRSlzlMcMDAyNcji1tbXf5vIcFgvATJOjAAAAY3RSTlP/8///////////" - "//////8A//////P/////ov//8//////////////z///T//////////+i////" - "//////////8w/////6IA/xAgMP//////////8/////////8w0/////////+z" - "ehebAAACkUlEQVR42u2VfVPTQBDG19VqC6LY+lKrRIxFQaFSBPuSvhBPF8SI" - "UZK2J5Yav/+HcO8uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6" - "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE24pFtBtzG1Q4AoD5" - "zvw5pEDcJvIQV/TE3/l+H9GnNJwcdABS5wAbFQLMqI98/UReoAaOTlaJsp0z" - "aHx7LwZvY0BUR2xpWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7" - "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45OeuPgm4+Xjtv7xm4N" - "7PMV4C61+Mrz3H2WImm3ATiWrAiwZRWcUA5Ej4dgIEMxDv6yxHHcNuAutnjv" - "2HZ1NeuycoVPh0mwC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW" - "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyGqidQSw1eqtJcA8c" - "Zq4zXqhPuCBYE1fKJjh/5X6MwRm9c2xf7WVdLf5oSdt64esVIwVAKC1HJ2ol" - "i8vj3L0YzC4zjkMagt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD" - "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz54uUyet+hBM7BXX" - "DS8Xc5+8Gmmbu1xwSoGIokA3oTptQecQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo" - "+W9xC8oWpugVCXLuwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua" - "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/AV5htgaQ9SeRAAAA" - "AElFTkSuQmCC"; - -image_base64("powered-by-ejabberd.png") -> - "iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaAAAAw1BMVEUAAAAj" - "BgYtBAM5AwFCAAAYGAJNAABcAABIDQ5qAAAoJRV7AACFAAAoKSdJHByLAAAw" - "Lwk1NQA1MzFJKyo4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb" - "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEgAB3dnd4d2+OjACD" - "hYKcmACJi4iQkpWspgCYmJm5swCmqazEwACwsbS4ub3X0QLExsPLyszW1Nnc" - "3ODm5ugMBwAWAwPHm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ" - "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VBkCMAAACcElEQVRI" - "x72WjXKiMBSFQalIFbNiy1pdrJZaRVYR5deGwPs/VRNBSBB2OjvQO0oYjPfj" - "5J6bCcdx8i2UldxKcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv" - "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJlRpsUei0JF6Qi+O" - "HyGK7ijXxPklHe/umIllim3iUBMJDIEULxxPP0TVWhhKJoN9fUpdmQLteV8a" - "DgEAg9gIcTjL4F4L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF" - "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7rovDjMpIHHGYeid" - "Xn4TQcaxMYqP3RV3C8oCl2WvrlSPaNpGZadRnmPGCk8ylM2okAJ4i9TEe1Ke" - "rsXxSl6jUt5uayiIodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs" - "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzvBo1I2tLmuL69kSi" - "tAweyHKN9b3leDfQMnu3nIIKWfmXnqGVKedJT6QpICbJvf2f8aOsvn68v+k7" - "/cwUQdPoxaMoRTnKFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG" - "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCTLav3VrqbPvP3vdd" - "uGEhQJzilncbpSA4F3vsihErO+dayv/sY5/yRE0GDEXCu2VoNiMlo5i+P2Kl" - "gMEvTNk2eYa5XEyh12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA" - "AElFTkSuQmCC". +image_base64(<<"powered-by-erlang.png">>) -> + <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoA" + "AADN0lEQVRo3u1aP0waURz+rjGRRQ+nUyRCYmJyDPTapD" + "ARaSIbTUjt1gVSh8ZW69aBAR0cWLSxCXWp59LR1jbdqKn" + "GxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu" + "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOH" + "o8HkiQxDBXEOjg9PcHc3BxuUSqsI8jR0REAUFGsCCoKFY" + "WCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJut9hMQlvdNbUhKW" + "shLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm" + "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1" + "MdG8rSKKXlJ7lHrfjouCut2IrC82BDPbe/gc+xlXez7Kx" + "Ez63H4lmIN473Rh8Si1BKhRY6aEJI8pLmbjSPN0xOnBBI" + "Lmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG" + "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFO" + "TYznzpPO+IQi+IK+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN" + "3Olvk4ge/IU98spne+FfGtlslm16nna8fduntfDscoVjG" + "JqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5" + "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizg" + "wwu5SLK8jllu1t9ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y" + "1m+hSj13WjqOiJKK0qpXKhSFxJAYBvKYaZ9TjWRu4SiWi" + "2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g" + "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJ" + "vHsdL+utNYrm2ufZDVZSkzPKaQkW8kthpyS297BvRdRzR" + "6DdTurJbPy9Ov1K6xr3HBPQuIMowR3asegUyDuU9SuUG+" + "dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q" + "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafI" + "YrizbfESoB+dd9/KF/d/wX3cJvREzl1vAAAAABJRU5Erk" + "Jggg==">>; +image_base64(<<"valid-xhtml10.png">>) -> + <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEA" + "AACiFBMVEUAAADe5+fOezmtra3ejEKlhELvvWO9WlrehE" + "LOe3vepaWclHvetVLGc3PerVKcCAj3vVqUjHOUe1JjlL0" + "xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY" + "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoIC" + "ACEazEhGAgIAACEYzFra2utjELWcznGnEr/7+9jY2POaz" + "HOYzGta2NShLVrlL05OUqctdacCADGa2ucAADGpVqUtc6" + "1ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe" + "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7" + "/fGxsa9pVqEOSkpY5xznL29tZxahLXOpVr/99ZrY1L/79" + "ZjUiljSikAOYTvxmMAMYScezmchFqUczGtlFp7c2utjFq" + "UlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC" + "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1" + "mtCMRhzY1L/zmvnvVpSQiHOpVJrUinntVr3zmOEc1L3xm" + "NaWlq1nFo5QkrGWim1lFoISpRSUlK1zt4hWpwASoz////" + "///8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3" + "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUl" + "HtKORh7a1IxIRCUjHtaSiHWrVIpIQhzWinvvVpaQiH/1m" + "PWpVKMe1L/zmP/xmNrUiGErc4YGBj/73PG1ucQWpT/53O" + "9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS" + "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////i" + "P9sSf//dP////////////////////////////////////" + "////////////8M////////////ef/////////////////" + "/////////////////////////////////////////////" + "//////////////////////9d/////////////////////" + "///////////////AP//////////////CP//RP////////" + "/////////////////////////////////////////////" + "///////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba" + "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpI" + "vGQGzokvTTA85VHKTpbRoeJnPno/p1+M7t3txj20e/Nzu" + "7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSANeBDxVmxZZSwEUY" + "kGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb" + "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6Vbd" + "tBblesOs+MjmEmzJKNIJWFEfEQTCWNPFKvcKEymjLO1b8" + "bwYQd1hCiiDCl5KsrDCIlhj4fSuvcpfSpgJmyv6dzeZv+" + "nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk" + "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDN" + "E9uzA1YD/5MhnzW1KRqF7mYG8jFYXLcfLpjOe2LA0fuGq" + "QrQHl10sdK0sFcFSOSlzF0BgXQH9h3QZDBI0ccNEhftjX" + "uippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7" + "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woW" + "SRJ0m2ewhvAOUiYYtZtaZL0CZZmtmVOQttLfr/dbveLZo" + "drfrL7W75wG/JjqkQxoNTtNsTKELQpQL6/D5loaSmyTT8" + "TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+" + "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qq" + "mj5bHqVZlJKZIRk1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p" + "1V76DeIs0MX+s3GxRlA1vtw83KhgNphc1nyErLO5zcvbO" + "srq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N" + "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4F" + "svjmuv662jsWxPZx2xg890bYMYnTgya7bjmCiEY0qgJ0v" + "MF3c+NoFdPyzxz6V3Uxs3AOWCDchRvOsQtBrbFsrT2fhH" + "Ec7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1" + "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3" + "e4HFmYPDl/thYcRVi8qPEoXVUWBl6FTBFTtnqmKKg5wnl" + "F4wZ1yeLv7TiwXKektE+iDBNicWEyLpnFhfDkpJc3q2kh" + "SPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx" + "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdi" + "MOlEOJHw1jOQ4sWt5rQopRkXZhMEi7pmeDCVWBlfUKwhM" + "Z7rsF6elKsvbwiKxgxIdewa3ErsaYomCVZFYJb0GUu3Jq" + "GUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+" + "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx" + "17XhBuDCaOerbt538erta7TFktLvdHghZcCbcPQO33zIJ" + "G9kxF5hoVXnzTzRz0r5js8oTj6uyPkGRf346HOLcasgFe" + "xueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl" + "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1ju" + "vBSWNFmNWGZy8LvzUl5PN4JCwyNDzbQ0aAj4Zrjz0FatG" + "JJYhvq4j7mGSpvytGFlZtHf2C4o/28Zu8z7wo7eYPfXys" + "nF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO" + "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+fa" + "CSJPJ1bmy6ThyvpivBmYWgjxPDPAp7JTemY/yGKFEiRt/" + "jG/2P79s8KCwoLCgoLC/khUBA5F0SfQZ+RYfpNE/4Xosm" + "q7jsZAJsAAAAASUVORK5CYII=">>; +image_base64(<<"vcss.png">>) -> + <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSA" + "AABKVBMVEUAAAAjIx8MR51ZVUqAdlmdnZ3ejEWLDAuNjY" + "1kiMG0n2d9fX19Ghfrp1FtbW3y39+3Ph6lIRNdXV2qJBF" + "cVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd" + "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T" + "09RRDwsJBG+vr73wV6fkG6eCQRFcLSurq6/X1+ht9nXfz" + "5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5tWCsmWajsz8+Sq9N" + "MPh4hVaY8MRj///////////////////////9MTEyOp9Lu" + "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn" + "82dfz7/zGafDw+fDw+zRSlzlMcMDAyNcji1tbXf5vIcFg" + "vATJOjAAAAY3RSTlP/8/////////////////8A//////P" + "/////ov//8//////////////z///T//////////+i////" + "//////////8w/////6IA/xAgMP//////////8////////" + "/8w0/////////+zehebAAACkUlEQVR42u2VfVPTQBDG19" + "VqC6LY+lKrRIxFQaFSBPuSvhBPF8SIUZK2J5Yav/+HcO8" + "uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6" + "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE2" + "4pFtBtzG1Q4AoD5zvw5pEDcJvIQV/TE3/l+H9GnNJwcdA" + "BS5wAbFQLMqI98/UReoAaOTlaJsp0zaHx7LwZvY0BUR2x" + "pWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7" + "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45Oe" + "uPgm4+Xjtv7xm4N7PMV4C61+Mrz3H2WImm3ATiWrAiwZR" + "WcUA5Ej4dgIEMxDv6yxHHcNuAutnjv2HZ1NeuycoVPh0m" + "wC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW" + "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyG" + "qidQSw1eqtJcA8cZq4zXqhPuCBYE1fKJjh/5X6MwRm9c2" + "xf7WVdLf5oSdt64esVIwVAKC1HJ2oli8vj3L0YzC4zjkM" + "agt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD" + "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz" + "54uUyet+hBM7BXXDS8Xc5+8Gmmbu1xwSoGIokA3oTptQe" + "cQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo+W9xC8oWpugVCXL" + "uwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua" + "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/A" + "V5htgaQ9SeRAAAAAElFTkSuQmCC">>; +image_base64(<<"powered-by-ejabberd.png">>) -> + <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaA" + "AAAw1BMVEUAAAAjBgYtBAM5AwFCAAAYGAJNAABcAABIDQ" + "5qAAAoJRV7AACFAAAoKSdJHByLAAAwLwk1NQA1MzFJKyo" + "4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb" + "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEg" + "AB3dnd4d2+OjACDhYKcmACJi4iQkpWspgCYmJm5swCmqa" + "zEwACwsbS4ub3X0QLExsPLyszW1Nnc3ODm5ugMBwAWAwP" + "Hm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ" + "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VB" + "kCMAAACcElEQVRIx72WjXKiMBSFQalIFbNiy1pdrJZaRV" + "YR5deGwPs/VRNBSBB2OjvQO0oYjPfj5J6bCcdx8i2Uldx" + "KcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv" + "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJ" + "lRpsUei0JF6Qi+OHyGK7ijXxPklHe/umIllim3iUBMJDI" + "EULxxPP0TVWhhKJoN9fUpdmQLteV8aDgEAg9gIcTjL4F4" + "L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF" + "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7" + "rovDjMpIHHGYeidXn4TQcaxMYqP3RV3C8oCl2WvrlSPaN" + "pGZadRnmPGCk8ylM2okAJ4i9TEe1KersXxSl6jUt5uayi" + "IodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs" + "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzv" + "Bo1I2tLmuL69kSitAweyHKN9b3leDfQMnu3nIIKWfmXnq" + "GVKedJT6QpICbJvf2f8aOsvn68v+k7/cwUQdPoxaMoRTn" + "KFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG" + "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCT" + "Lav3VrqbPvP3vdduGEhQJzilncbpSA4F3vsihErO+dayv" + "/sY5/yRE0GDEXCu2VoNiMlo5i+P2KlgMEvTNk2eYa5XEy" + "h12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA" + "AElFTkSuQmCC">>. create_image_files(Images_dir) -> - Filenames = ["powered-by-ejabberd.png", - "powered-by-erlang.png", - "valid-xhtml10.png", - "vcss.png" - ], - lists:foreach( - fun(Filename) -> - Filename_full = filename:join([Images_dir, Filename]), - {ok, F} = file:open(Filename_full, [write]), - Image = jlib:decode_base64(image_base64(Filename)), - io:format(F, "~s", [Image]), - file:close(F) - end, - Filenames), + Filenames = [<<"powered-by-ejabberd.png">>, + <<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>, + <<"vcss.png">>], + lists:foreach(fun (Filename) -> + Filename_full = fjoin([Images_dir, Filename]), + {ok, F} = file:open(Filename_full, [write]), + Image = jlib:decode_base64(image_base64(Filename)), + io:format(F, <<"~s">>, [Image]), + file:close(F) + end, + Filenames), ok. fw(F, S) -> fw(F, S, [], html). -fw(F, S, O) when is_list(O) -> - fw(F, S, O, html); +fw(F, S, O) when is_list(O) -> fw(F, S, O, html); fw(F, S, FileFormat) when is_atom(FileFormat) -> fw(F, S, [], FileFormat). fw(F, S, O, FileFormat) -> - S1 = io_lib:format(S ++ "~n", O), + S1 = list_to_binary(io_lib:format(binary_to_list(S) ++ "~n", O)), S2 = case FileFormat of - html -> - S1; - plaintext -> - ejabberd_regexp:greplace(S1, "<[^>]*>", "") + html -> S1; + plaintext -> + ejabberd_regexp:greplace(S1, <<"<[^>]*>">>, <<"">>) end, io:format(F, S2, []). -put_header(_, _, _, _, _, _, _, _, _, plaintext) -> - ok; -put_header(F, Room, Date, CSSFile, Lang, Hour_offset, Date_prev, Date_next, Top_link, FileFormat) -> - fw(F, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"), - fw(F, "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"~s\" lang=\"~s\">", [Lang, Lang]), - fw(F, "<head>"), - fw(F, "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />"), - fw(F, "<title>~s - ~s</title>", [htmlize(Room#room.title), Date]), +put_header(_, _, _, _, _, _, _, _, _, plaintext) -> ok; +put_header(F, Room, Date, CSSFile, Lang, Hour_offset, + Date_prev, Date_next, Top_link, FileFormat) -> + fw(F, + <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD " + "XHTML 1.0 Transitional//EN\" \"http://www.w3." + "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>), + fw(F, + <<"<html xmlns=\"http://www.w3.org/1999/xhtml\" " + "xml:lang=\"~s\" lang=\"~s\">">>, + [Lang, Lang]), + fw(F, <<"<head>">>), + fw(F, + <<"<meta http-equiv=\"Content-Type\" content=\"t" + "ext/html; charset=utf-8\" />">>), + fw(F, <<"<title>~s - ~s</title>">>, + [htmlize(Room#room.title), Date]), put_header_css(F, CSSFile), put_header_script(F), - fw(F, "</head>"), - fw(F, "<body>"), + fw(F, <<"</head>">>), + fw(F, <<"<body>">>), {Top_url, Top_text} = Top_link, - fw(F, "<div style=\"text-align: right;\"><a style=\"color: #AAAAAA; font-family: monospace; text-decoration: none; font-weight: bold;\" href=\"~s\">~s</a></div>", [Top_url, Top_text]), - fw(F, "<div class=\"roomtitle\">~s</div>", [htmlize(Room#room.title)]), - fw(F, "<a class=\"roomjid\" href=\"xmpp:~s?join\">~s</a>", [Room#room.jid, Room#room.jid]), - fw(F, "<div class=\"logdate\">~s<span class=\"w3c\"><a class=\"nav\" href=\"~s\"><</a> <a class=\"nav\" href=\".\/\">^</a> <a class=\"nav\" href=\"~s\">></a></span></div>", [Date, Date_prev, Date_next]), - case {htmlize(Room#room.subject_author), htmlize(Room#room.subject)} of - {"", ""} -> ok; - {SuA, Su} -> fw(F, "<div class=\"roomsubject\">~s~s~s</div>", [SuA, ?T(" has set the subject to: "), Su]) + fw(F, + <<"<div style=\"text-align: right;\"><a " + "style=\"color: #AAAAAA; font-family: " + "monospace; text-decoration: none; font-weight" + ": bold;\" href=\"~s\">~s</a></div>">>, + [Top_url, Top_text]), + fw(F, <<"<div class=\"roomtitle\">~s</div>">>, + [htmlize(Room#room.title)]), + fw(F, + <<"<a class=\"roomjid\" href=\"xmpp:~s?join\">~s" + "</a>">>, + [Room#room.jid, Room#room.jid]), + fw(F, + <<"<div class=\"logdate\">~s<span class=\"w3c\">" + "<a class=\"nav\" href=\"~s\"><</a> " + "<a class=\"nav\" href=\"./\">^</a> <a " + "class=\"nav\" href=\"~s\">></a></span></di" + "v>">>, + [Date, Date_prev, Date_next]), + case {htmlize(Room#room.subject_author), + htmlize(Room#room.subject)} + of + {<<"">>, <<"">>} -> ok; + {SuA, Su} -> + fw(F, <<"<div class=\"roomsubject\">~s~s~s</div>">>, + [SuA, ?T(<<" has set the subject to: ">>), Su]) end, - RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), + RoomConfig = roomconfig_to_string(Room#room.config, + Lang, FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), Occupants = get_room_occupants(Room#room.jid), - RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), + RoomOccupants = roomoccupants_to_string(Occupants, + FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), - Time_offset_str = case Hour_offset<0 of - true -> io_lib:format("~p", [Hour_offset]); - false -> io_lib:format("+~p", [Hour_offset]) + Time_offset_str = case Hour_offset < 0 of + true -> io_lib:format("~p", [Hour_offset]); + false -> io_lib:format("+~p", [Hour_offset]) end, - fw(F, "<br/><a class=\"ts\">GMT~s</a><br/>", [Time_offset_str]). + fw(F, <<"<br/><a class=\"ts\">GMT~s</a><br/>">>, + [Time_offset_str]). put_header_css(F, false) -> - fw(F, "<style type=\"text/css\">"), - fw(F, "<!--"), - fw(F, ".ts {color: #AAAAAA; text-decoration: none;}"), - fw(F, ".mrcm {color: #009900; font-style: italic; font-weight: bold;}"), - fw(F, ".msc {color: #009900; font-style: italic; font-weight: bold;}"), - fw(F, ".msm {color: #000099; font-style: italic; font-weight: bold;}"), - fw(F, ".mj {color: #009900; font-style: italic;}"), - fw(F, ".ml {color: #009900; font-style: italic;}"), - fw(F, ".mk {color: #009900; font-style: italic;}"), - fw(F, ".mb {color: #009900; font-style: italic;}"), - fw(F, ".mnc {color: #009900; font-style: italic;}"), - fw(F, ".mn {color: #0000AA;}"), - fw(F, ".mne {color: #AA0099;}"), - fw(F, "a.nav {color: #AAAAAA; font-family: monospace; letter-spacing: 3px; text-decoration: none;}"), - fw(F, "div.roomtitle {border-bottom: #224466 solid 3pt; margin-left: 20pt;}"), - fw(F, "div.roomtitle {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; text-decoration: none;}"), - fw(F, "a.roomjid {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt; text-decoration: none;}"), - fw(F, "div.logdate {color: #663399; font-size: 20px; font-weight: bold; font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt; margin-left:80pt; margin-top:20px;}"), - fw(F, "div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif; margin-left: 80pt; margin-bottom: 10px;}"), - fw(F, "div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 50%; text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699; border-right: 4px solid #336699;}"), - fw(F, "div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px;}"), - fw(F, "div.rcos {padding-right: 10px;}"), - fw(F, "div.rcoe {color: green;}"), - fw(F, "div.rcod {color: red;}"), - fw(F, "div.rcoe:after {content: \": v\";}"), - fw(F, "div.rcod:after {content: \": x\";}"), - fw(F, "div.rcot:after {}"), - fw(F, ".legend {width: 100%; margin-top: 30px; border-top: #224466 solid 1pt; padding: 10px 0px 10px 0px; text-align: left; font-family: monospace; letter-spacing: 2px;}"), - fw(F, ".w3c {position: absolute; right: 10px; width: 60%; text-align: right; font-family: monospace; letter-spacing: 1px;}"), - fw(F, "//-->"), - fw(F, "</style>"); - + fw(F, <<"<style type=\"text/css\">">>), + fw(F, <<"<!--">>), + fw(F, + <<".ts {color: #AAAAAA; text-decoration: " + "none;}">>), + fw(F, + <<".mrcm {color: #009900; font-style: italic; " + "font-weight: bold;}">>), + fw(F, + <<".msc {color: #009900; font-style: italic; " + "font-weight: bold;}">>), + fw(F, + <<".msm {color: #000099; font-style: italic; " + "font-weight: bold;}">>), + fw(F, <<".mj {color: #009900; font-style: italic;}">>), + fw(F, <<".ml {color: #009900; font-style: italic;}">>), + fw(F, <<".mk {color: #009900; font-style: italic;}">>), + fw(F, <<".mb {color: #009900; font-style: italic;}">>), + fw(F, <<".mnc {color: #009900; font-style: italic;}">>), + fw(F, <<".mn {color: #0000AA;}">>), + fw(F, <<".mne {color: #AA0099;}">>), + fw(F, + <<"a.nav {color: #AAAAAA; font-family: " + "monospace; letter-spacing: 3px; text-decorati" + "on: none;}">>), + fw(F, + <<"div.roomtitle {border-bottom: #224466 " + "solid 3pt; margin-left: 20pt;}">>), + fw(F, + <<"div.roomtitle {color: #336699; font-size: " + "24px; font-weight: bold; font-family: " + "sans-serif; letter-spacing: 3px; text-decorat" + "ion: none;}">>), + fw(F, + <<"a.roomjid {color: #336699; font-size: " + "24px; font-weight: bold; font-family: " + "sans-serif; letter-spacing: 3px; margin-left: " + "20pt; text-decoration: none;}">>), + fw(F, + <<"div.logdate {color: #663399; font-size: " + "20px; font-weight: bold; font-family: " + "sans-serif; letter-spacing: 2px; border-botto" + "m: #224466 solid 1pt; margin-left:80pt; " + "margin-top:20px;}">>), + fw(F, + <<"div.roomsubject {color: #336699; font-size: " + "18px; font-family: sans-serif; margin-left: " + "80pt; margin-bottom: 10px;}">>), + fw(F, + <<"div.rc {color: #336699; font-size: 12px; " + "font-family: sans-serif; margin-left: " + "50%; text-align: right; background: " + "#f3f6f9; border-bottom: 1px solid #336699; " + "border-right: 4px solid #336699;}">>), + fw(F, + <<"div.rct {font-weight: bold; background: " + "#e3e6e9; padding-right: 10px;}">>), + fw(F, <<"div.rcos {padding-right: 10px;}">>), + fw(F, <<"div.rcoe {color: green;}">>), + fw(F, <<"div.rcod {color: red;}">>), + fw(F, <<"div.rcoe:after {content: \": v\";}">>), + fw(F, <<"div.rcod:after {content: \": x\";}">>), + fw(F, <<"div.rcot:after {}">>), + fw(F, + <<".legend {width: 100%; margin-top: 30px; " + "border-top: #224466 solid 1pt; padding: " + "10px 0px 10px 0px; text-align: left; " + "font-family: monospace; letter-spacing: " + "2px;}">>), + fw(F, + <<".w3c {position: absolute; right: 10px; " + "width: 60%; text-align: right; font-family: " + "monospace; letter-spacing: 1px;}">>), + fw(F, <<"//-->">>), + fw(F, <<"</style>">>); put_header_css(F, CSSFile) -> - fw(F, "<link rel=\"stylesheet\" type=\"text/css\" href=\"~s\" media=\"all\">", [CSSFile]). + fw(F, + <<"<link rel=\"stylesheet\" type=\"text/css\" " + "href=\"~s\" media=\"all\">">>, + [CSSFile]). put_header_script(F) -> - fw(F, "<script type=\"text/javascript\">"), - fw(F, "function sh(e) // Show/Hide an element"), - fw(F, "{if(document.getElementById(e).style.display=='none')"), - fw(F, "{document.getElementById(e).style.display='block';}"), - fw(F, "else {document.getElementById(e).style.display='none';}}"), - fw(F, "</script>"). + fw(F, <<"<script type=\"text/javascript\">">>), + fw(F, <<"function sh(e) // Show/Hide an element">>), + fw(F, + <<"{if(document.getElementById(e).style.display=" + "='none')">>), + fw(F, + <<"{document.getElementById(e).style.display='bl" + "ock';}">>), + fw(F, + <<"else {document.getElementById(e).style.displa" + "y='none';}}">>), + fw(F, <<"</script>">>). put_room_config(_F, _RoomConfig, _Lang, plaintext) -> ok; put_room_config(F, RoomConfig, Lang, _FileFormat) -> {_, Now2, _} = now(), - fw(F, "<div class=\"rc\">"), - fw(F, "<div class=\"rct\" onclick=\"sh('a~p');return false;\">~s</div>", [Now2, ?T("Room Configuration")]), - fw(F, "<div class=\"rcos\" id=\"a~p\" style=\"display: none;\" ><br/>~s</div>", [Now2, RoomConfig]), - fw(F, "</div>"). - -put_room_occupants(_F, _RoomOccupants, _Lang, plaintext) -> + fw(F, <<"<div class=\"rc\">">>), + fw(F, + <<"<div class=\"rct\" onclick=\"sh('a~p');return " + "false;\">~s</div>">>, + [Now2, ?T(<<"Room Configuration">>)]), + fw(F, + <<"<div class=\"rcos\" id=\"a~p\" style=\"displa" + "y: none;\" ><br/>~s</div>">>, + [Now2, RoomConfig]), + fw(F, <<"</div>">>). + +put_room_occupants(_F, _RoomOccupants, _Lang, + plaintext) -> ok; -put_room_occupants(F, RoomOccupants, Lang, _FileFormat) -> +put_room_occupants(F, RoomOccupants, Lang, + _FileFormat) -> {_, Now2, _} = now(), - fw(F, "<div class=\"rc\">"), - fw(F, "<div class=\"rct\" onclick=\"sh('o~p');return false;\">~s</div>", [Now2, ?T("Room Occupants")]), - fw(F, "<div class=\"rcos\" id=\"o~p\" style=\"display: none;\" ><br/>~s</div>", [Now2, RoomOccupants]), - fw(F, "</div>"). - -%% htmlize -%% The default behaviour is to ignore the nofollow spam prevention on links -%% (NoFollow=false) -htmlize(S1) -> - htmlize(S1, html). - -htmlize(S1, plaintext) -> - S1; + fw(F, <<"<div class=\"rc\">">>), + fw(F, + <<"<div class=\"rct\" onclick=\"sh('o~p');return " + "false;\">~s</div>">>, + [Now2, ?T(<<"Room Occupants">>)]), + fw(F, + <<"<div class=\"rcos\" id=\"o~p\" style=\"displa" + "y: none;\" ><br/>~s</div>">>, + [Now2, RoomOccupants]), + fw(F, <<"</div>">>). + +htmlize(S1) -> htmlize(S1, html). + +htmlize(S1, plaintext) -> S1; htmlize(S1, FileFormat) -> htmlize(S1, false, FileFormat). -%% The NoFollow parameter tell if the spam prevention should be applied to the link found -%% true means 'apply nofollow on links'. -htmlize(S1, _NoFollow, plaintext) -> - S1; +htmlize(S1, _NoFollow, plaintext) -> S1; htmlize(S1, NoFollow, _FileFormat) -> - S2_list = string:tokens(S1, "\n"), - lists:foldl( - fun(Si, Res) -> - Si2 = htmlize2(Si, NoFollow), - case Res of - "" -> Si2; - _ -> Res ++ "<br/>" ++ Si2 - end - end, - "", - S2_list). + S2_list = str:tokens(S1, <<"\n">>), + lists:foldl(fun (Si, Res) -> + Si2 = htmlize2(Si, NoFollow), + case Res of + <<"">> -> Si2; + _ -> <<Res/binary, "<br/>", Si2/binary>> + end + end, + <<"">>, S2_list). htmlize2(S1, NoFollow) -> - S2 = ejabberd_regexp:greplace(S1, "\\&", "\\&"), - S3 = ejabberd_regexp:greplace(S2, "<", "\\<"), - S4 = ejabberd_regexp:greplace(S3, ">", "\\>"), - S5 = ejabberd_regexp:greplace(S4, "((http|https|ftp)://|(mailto|xmpp):)[^] )\'\"}]+", - link_regexp(NoFollow)), - %% Remove 'right-to-left override' unicode character 0x202e - S6 = ejabberd_regexp:greplace(S5, " ", "\\ \\ "), - S7 = ejabberd_regexp:greplace(S6, "\\t", "\\ \\ \\ \\ "), - ejabberd_regexp:greplace(S7, [226,128,174], "[RLO]"). - -%% Regexp link -%% Add the nofollow rel attribute when required -link_regexp(false) -> - "<a href=\"&\">&</a>"; + S2 = ejabberd_regexp:greplace(S1, <<"\\&">>, + <<"\\&">>), + S3 = ejabberd_regexp:greplace(S2, <<"<">>, + <<"\\<">>), + S4 = ejabberd_regexp:greplace(S3, <<">">>, + <<"\\>">>), + S5 = ejabberd_regexp:greplace(S4, + <<"((http|https|ftp)://|(mailto|xmpp):)[^] " + ")'\"}]+">>, + link_regexp(NoFollow)), + S6 = ejabberd_regexp:greplace(S5, <<" ">>, + <<"\\ \\ ">>), + S7 = ejabberd_regexp:greplace(S6, <<"\\t">>, + <<"\\ \\ \\ \\ ">>), + ejabberd_regexp:greplace(S7, <<226, 128, 174>>, + <<"[RLO]">>). + +link_regexp(false) -> <<"<a href=\"&\">&</a>">>; link_regexp(true) -> - "<a href=\"&\" rel=\"nofollow\">&</a>". + <<"<a href=\"&\" rel=\"nofollow\">&</a>">>. get_room_info(RoomJID, Opts) -> - Title = - case lists:keysearch(title, 1, Opts) of - {value, {_, T}} -> T; - false -> "" - end, - Subject = - case lists:keysearch(subject, 1, Opts) of - {value, {_, S}} -> S; - false -> "" - end, - SubjectAuthor = - case lists:keysearch(subject_author, 1, Opts) of - {value, {_, SA}} -> SA; - false -> "" - end, - #room{jid = jlib:jid_to_string(RoomJID), - title = Title, - subject = Subject, - subject_author = SubjectAuthor, - config = Opts - }. + Title = case lists:keysearch(title, 1, Opts) of + {value, {_, T}} -> T; + false -> <<"">> + end, + Subject = case lists:keysearch(subject, 1, Opts) of + {value, {_, S}} -> S; + false -> <<"">> + end, + SubjectAuthor = case lists:keysearch(subject_author, 1, + Opts) + of + {value, {_, SA}} -> SA; + false -> <<"">> + end, + #room{jid = jlib:jid_to_string(RoomJID), title = Title, + subject = Subject, subject_author = SubjectAuthor, + config = Opts}. roomconfig_to_string(Options, Lang, FileFormat) -> - %% Get title, if available Title = case lists:keysearch(title, 1, Options) of - {value, Tuple} -> [Tuple]; - false -> [] + {value, Tuple} -> [Tuple]; + false -> [] end, - - %% Remove title from list Os1 = lists:keydelete(title, 1, Options), - - %% Order list Os2 = lists:sort(Os1), - - %% Add title to ordered list Options2 = Title ++ Os2, - - lists:foldl( - fun({Opt, Val}, R) -> - case get_roomconfig_text(Opt) of - undefined -> - R; - OptT -> - OptText = ?T(OptT), - R2 = case Val of - false -> "<div class=\"rcod\">" ++ OptText ++ "</div>"; - true -> "<div class=\"rcoe\">" ++ OptText ++ "</div>"; - "" -> "<div class=\"rcod\">" ++ OptText ++ "</div>"; - T -> - case Opt of - password -> "<div class=\"rcoe\">" ++ OptText ++ "</div>"; - max_users -> "<div class=\"rcot\">" ++ OptText ++ ": \"" ++ htmlize(integer_to_list(T), FileFormat) ++ "\"</div>"; - title -> "<div class=\"rcot\">" ++ OptText ++ ": \"" ++ htmlize(T, FileFormat) ++ "\"</div>"; - description -> "<div class=\"rcot\">" ++ OptText ++ ": \"" ++ htmlize(T, FileFormat) ++ "\"</div>"; - allow_private_messages_from_visitors -> "<div class=\"rcot\">" ++ OptText ++ ": \"" ++ htmlize(?T(atom_to_list(T)), FileFormat) ++ "\"</div>"; - _ -> "\"" ++ T ++ "\"" - end - end, - R ++ R2 - end - end, - "", - Options2). - -get_roomconfig_text(title) -> "Room title"; -get_roomconfig_text(persistent) -> "Make room persistent"; -get_roomconfig_text(public) -> "Make room public searchable"; -get_roomconfig_text(public_list) -> "Make participants list public"; -get_roomconfig_text(password_protected) -> "Make room password protected"; -get_roomconfig_text(password) -> "Password"; -get_roomconfig_text(anonymous) -> "This room is not anonymous"; -get_roomconfig_text(members_only) -> "Make room members-only"; -get_roomconfig_text(moderated) -> "Make room moderated"; -get_roomconfig_text(members_by_default) -> "Default users as participants"; -get_roomconfig_text(allow_change_subj) -> "Allow users to change the subject"; -get_roomconfig_text(allow_private_messages) -> "Allow users to send private messages"; -get_roomconfig_text(allow_private_messages_from_visitors) -> "Allow visitors to send private messages to"; -get_roomconfig_text(allow_query_users) -> "Allow users to query other users"; -get_roomconfig_text(allow_user_invites) -> "Allow users to send invites"; -get_roomconfig_text(logging) -> "Enable logging"; -get_roomconfig_text(allow_visitor_nickchange) -> "Allow visitors to change nickname"; -get_roomconfig_text(allow_visitor_status) -> "Allow visitors to send status text in presence updates"; -get_roomconfig_text(captcha_protected) -> "Make room captcha protected"; -get_roomconfig_text(description) -> "Room description"; + lists:foldl(fun ({Opt, Val}, R) -> + case get_roomconfig_text(Opt) of + undefined -> R; + OptT -> + OptText = (?T(OptT)), + R2 = case Val of + false -> + <<"<div class=\"rcod\">", + OptText/binary, "</div>">>; + true -> + <<"<div class=\"rcoe\">", + OptText/binary, "</div>">>; + <<"">> -> + <<"<div class=\"rcod\">", + OptText/binary, "</div>">>; + T -> + case Opt of + password -> + <<"<div class=\"rcoe\">", + OptText/binary, "</div>">>; + max_users -> + <<"<div class=\"rcot\">", + OptText/binary, ": \"", + (htmlize(jlib:integer_to_binary(T), + FileFormat))/binary, + "\"</div>">>; + title -> + <<"<div class=\"rcot\">", + OptText/binary, ": \"", + (htmlize(T, + FileFormat))/binary, + "\"</div>">>; + description -> + <<"<div class=\"rcot\">", + OptText/binary, ": \"", + (htmlize(T, + FileFormat))/binary, + "\"</div>">>; + allow_private_messages_from_visitors -> + <<"<div class=\"rcot\">", + OptText/binary, ": \"", + (htmlize(?T((jlib:atom_to_binary(T))), + FileFormat))/binary, + "\"</div>">>; + _ -> <<"\"", T/binary, "\"">> + end + end, + <<R/binary, R2/binary>> + end + end, + <<"">>, Options2). + +get_roomconfig_text(title) -> <<"Room title">>; +get_roomconfig_text(persistent) -> + <<"Make room persistent">>; +get_roomconfig_text(public) -> + <<"Make room public searchable">>; +get_roomconfig_text(public_list) -> + <<"Make participants list public">>; +get_roomconfig_text(password_protected) -> + <<"Make room password protected">>; +get_roomconfig_text(password) -> <<"Password">>; +get_roomconfig_text(anonymous) -> + <<"This room is not anonymous">>; +get_roomconfig_text(members_only) -> + <<"Make room members-only">>; +get_roomconfig_text(moderated) -> + <<"Make room moderated">>; +get_roomconfig_text(members_by_default) -> + <<"Default users as participants">>; +get_roomconfig_text(allow_change_subj) -> + <<"Allow users to change the subject">>; +get_roomconfig_text(allow_private_messages) -> + <<"Allow users to send private messages">>; +get_roomconfig_text(allow_private_messages_from_visitors) -> + <<"Allow visitors to send private messages to">>; +get_roomconfig_text(allow_query_users) -> + <<"Allow users to query other users">>; +get_roomconfig_text(allow_user_invites) -> + <<"Allow users to send invites">>; +get_roomconfig_text(logging) -> <<"Enable logging">>; +get_roomconfig_text(allow_visitor_nickchange) -> + <<"Allow visitors to change nickname">>; +get_roomconfig_text(allow_visitor_status) -> + <<"Allow visitors to send status text in " + "presence updates">>; +get_roomconfig_text(captcha_protected) -> + <<"Make room captcha protected">>; +get_roomconfig_text(description) -> + <<"Room description">>; %% get_roomconfig_text(subject) -> "Subject"; %% get_roomconfig_text(subject_author) -> "Subject author"; -get_roomconfig_text(max_users) -> "Maximum Number of Occupants"; +get_roomconfig_text(max_users) -> + <<"Maximum Number of Occupants">>; get_roomconfig_text(_) -> undefined. -%% Users = [{JID, Nick, Role}] roomoccupants_to_string(Users, _FileFormat) -> Res = [role_users_to_string(RoleS, Users1) - || {RoleS, Users1} <- group_by_role(Users), Users1 /= []], - lists:flatten(["<div class=\"rcot\">", Res, "</div>"]). + || {RoleS, Users1} <- group_by_role(Users), + Users1 /= []], + iolist_to_binary([<<"<div class=\"rcot\">">>, Res, <<"</div>">>]). -%% Users = [{JID, Nick, Role}] group_by_role(Users) -> - {Ms, Ps, Vs, Ns} = - lists:foldl( - fun({JID, Nick, moderator}, {Mod, Par, Vis, Non}) -> - {[{JID, Nick}]++Mod, Par, Vis, Non}; - ({JID, Nick, participant}, {Mod, Par, Vis, Non}) -> - {Mod, [{JID, Nick}]++Par, Vis, Non}; - ({JID, Nick, visitor}, {Mod, Par, Vis, Non}) -> - {Mod, Par, [{JID, Nick}]++Vis, Non}; - ({JID, Nick, none}, {Mod, Par, Vis, Non}) -> - {Mod, Par, Vis, [{JID, Nick}]++Non} - end, - {[], [], [], []}, - Users), - case Ms of [] -> []; _ -> [{"Moderator", Ms}] end - ++ case Ms of [] -> []; _ -> [{"Participant", Ps}] end - ++ case Ms of [] -> []; _ -> [{"Visitor", Vs}] end - ++ case Ms of [] -> []; _ -> [{"None", Ns}] end. - -%% Role = atom() -%% Users = [{JID, Nick}] + {Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick, + moderator}, + {Mod, Par, Vis, Non}) -> + {[{JID, Nick}] ++ Mod, Par, Vis, + Non}; + ({JID, Nick, participant}, + {Mod, Par, Vis, Non}) -> + {Mod, [{JID, Nick}] ++ Par, Vis, + Non}; + ({JID, Nick, visitor}, + {Mod, Par, Vis, Non}) -> + {Mod, Par, [{JID, Nick}] ++ Vis, + Non}; + ({JID, Nick, none}, + {Mod, Par, Vis, Non}) -> + {Mod, Par, Vis, [{JID, Nick}] ++ Non} + end, + {[], [], [], []}, Users), + case Ms of + [] -> []; + _ -> [{<<"Moderator">>, Ms}] + end + ++ + case Ms of + [] -> []; + _ -> [{<<"Participant">>, Ps}] + end + ++ + case Ms of + [] -> []; + _ -> [{<<"Visitor">>, Vs}] + end + ++ + case Ms of + [] -> []; + _ -> [{<<"None">>, Ns}] + end. + role_users_to_string(RoleS, Users) -> SortedUsers = lists:keysort(2, Users), - UsersString = [[Nick, "<br/>"] || {_JID, Nick} <- SortedUsers], - [RoleS, ": ", UsersString]. + UsersString = << <<Nick/binary, "<br/>">> + || {_JID, Nick} <- SortedUsers >>, + <<RoleS/binary, ": ", UsersString/binary>>. get_room_occupants(RoomJIDString) -> RoomJID = jlib:string_to_jid(RoomJIDString), @@ -936,19 +1112,25 @@ get_room_occupants(RoomJIDString) -> MucService = RoomJID#jid.lserver, StateData = get_room_state(RoomName, MucService), [{U#user.jid, U#user.nick, U#user.role} - || {_, U} <- ?DICT:to_list(StateData#state.users)]. + || {_, U} <- (?DICT):to_list(StateData#state.users)]. + +-spec get_room_state(binary(), binary()) -> muc_room_state(). get_room_state(RoomName, MucService) -> - case mnesia:dirty_read(muc_online_room, {RoomName, MucService}) of - [R] -> - RoomPid = R#muc_online_room.pid, - get_room_state(RoomPid); - [] -> - #state{} + case mnesia:dirty_read(muc_online_room, + {RoomName, MucService}) + of + [R] -> + RoomPid = R#muc_online_room.pid, + get_room_state(RoomPid); + [] -> #state{} end. +-spec get_room_state(pid()) -> muc_room_state(). + get_room_state(RoomPid) -> - {ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, get_state), + {ok, R} = gen_fsm:sync_send_all_state_event(RoomPid, + get_state), R. get_proc_name(Host) -> @@ -956,6 +1138,13 @@ get_proc_name(Host) -> calc_hour_offset(TimeHere) -> TimeZero = calendar:now_to_universal_time(now()), - TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600, - TimeZeroHour = calendar:datetime_to_gregorian_seconds(TimeZero) div 3600, + TimeHereHour = + calendar:datetime_to_gregorian_seconds(TimeHere) div + 3600, + TimeZeroHour = + calendar:datetime_to_gregorian_seconds(TimeZero) div + 3600, TimeHereHour - TimeZeroHour. + +fjoin(FileList) -> + list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index ecf9c9581..6c139d5c5 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -25,37 +25,27 @@ %%%---------------------------------------------------------------------- -module(mod_muc_room). + -author('alexey@process-one.net'). -define(GEN_FSM, p1_fsm). -behaviour(?GEN_FSM). - %% External exports --export([start_link/10, - start_link/8, - start_link/2, - start/10, - start/8, - start/2, - migrate/3, - route/4, - moderate_room_history/2, - persist_recent_messages/1]). +-export([start_link/10, start_link/8, start_link/2, + start/10, start/8, 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]). +-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, @@ -64,865 +54,866 @@ %-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)). + +-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), + +-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) -> - ?SUPERVISOR_START([Host, ServerHost, Access, Room, HistorySize, PersistHistory, - RoomShaper, Creator, Nick, DefRoomOpts]). +-endif. -start(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts) -> - Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), - supervisor:start_child( - Supervisor, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, - Opts]). +start(Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Creator, Nick, + DefRoomOpts) -> + ?SUPERVISOR_START([Host, ServerHost, Access, Room, + HistorySize, PersistHistory, RoomShaper, Creator, Nick, + DefRoomOpts]). + +start(Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Opts) -> + Supervisor = gen_mod:get_module_proc(ServerHost, + ejabberd_mod_muc_sup), + supervisor:start_child(Supervisor, + [Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Opts]). start(StateName, StateData) -> ServerHost = StateData#state.server_host, ?SUPERVISOR_START([StateName, StateData]). -start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, - Creator, Nick, DefRoomOpts) -> - ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, - RoomShaper, Creator, Nick, DefRoomOpts], - ?FSMOPTS). - -start_link(Host, ServerHost, Access, Room, HistorySize, PersistHistory, RoomShaper, Opts) -> - ?GEN_FSM:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, PersistHistory, - RoomShaper, Opts], - ?FSMOPTS). +start_link(Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Creator, Nick, + DefRoomOpts) -> + (?GEN_FSM):start_link(?MODULE, + [Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Creator, Nick, + DefRoomOpts], + ?FSMOPTS). + +start_link(Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Opts) -> + (?GEN_FSM):start_link(?MODULE, + [Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Opts], + ?FSMOPTS). start_link(StateName, StateData) -> - ?GEN_FSM:start_link(?MODULE, [StateName, StateData], ?FSMOPTS). + (?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}). + (?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 -%%%---------------------------------------------------------------------- + (?GEN_FSM):sync_send_all_state_event(FsmRef, + persist_recent_messages). -%%---------------------------------------------------------------------- -%% 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]) -> +init([Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Creator, _Nick, + DefRoomOpts]) -> process_flag(trap_exit, true), Shaper = shaper:new(RoomShaper), State = set_affiliation(Creator, owner, - #state{host = Host, - server_host = ServerHost, - access = Access, - room = Room, + #state{host = Host, server_host = ServerHost, + access = Access, room = Room, history = lqueue_new(HistorySize), persist_history = PersistHistory, - jid = jlib:make_jid(Room, Host, ""), - just_created = true, - room_shaper = Shaper}), + 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 + if (State1#state.config)#config.persistent -> + mod_muc:store_room(State1#state.server_host, + State1#state.host, State1#state.room, + make_opts(State1)); + true -> ok end, - ?INFO_MSG("Created MUC room ~s@~s by ~s", + ?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]) -> +init([Host, ServerHost, Access, Room, HistorySize, + PersistHistory, RoomShaper, Opts]) -> process_flag(trap_exit, true), Shaper = shaper:new(RoomShaper), - State = set_opts(Opts, #state{host = Host, - server_host = ServerHost, - 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}), + State = set_opts(Opts, + #state{host = Host, server_host = ServerHost, + 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]) -> +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}, +normal_state({route, From, <<"">>, + #xmlel{name = <<"message">>, attrs = Attrs, + children = Els} = + Packet}, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), + 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, - mod_muc, 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 + 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, + mod_muc, min_message_interval, + fun(I) when is_number(I), + I>=0 -> + I + end, 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 -> + mod_muc: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 -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation( - IJID, - member, - StateData), - case (NSD#state.config)#config.persistent of - true -> - mod_muc: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; + 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 -> - {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), + 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} + _ -> 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}, +normal_state({route, From, <<"">>, + #xmlel{name = <<"iq">>} = 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} + #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 = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, + XMLNS}], + children = 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}, + #xmlel{name = <<"presence">>} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = now_to_usec(now()), MinPresenceInterval = - trunc(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, 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} + trunc(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_presence_interval, + fun(I) when is_number(I), I>=0 -> + I + end, 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}, + #xmlel{name = <<"message">>, attrs = 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 + 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; - {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 + 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}, + #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), - StanzaId = xml:get_attr_s("id", Attrs), + 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 + 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), +handle_event({service_message, Msg}, _StateName, + StateData) -> + MessagePkt = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"body">>, attrs = [], + children = [{xmlcdata, Msg}]}]}, + send_multiple( + StateData#state.jid, + StateData#state.server_host, + StateData#state.users, + MessagePkt), + 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", +handle_event({destroy, Reason}, _StateName, + StateData) -> + {result, [], stop} = destroy_room(#xmlel{name = + <<"destroy">>, + attrs = + [{<<"xmlns">>, ?NS_MUC_OWNER}], + children = + case Reason of + none -> []; + _Else -> + [#xmlel{name = + <<"reason">>, + attrs = [], + children = + [{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", + ?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({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) -> +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, iolist_to_binary(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) -> +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) -> +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) -> +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}. +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), +print_state(StateData) -> StateData. + +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} + 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), +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} + 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) -> +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} + {{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 +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 +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} + {migrate, StateData, + {Node, ?MODULE, start, [StateName, StateData]}, 0}; + true -> {next_state, StateName, StateData} end; -handle_info('shutdown', _StateName, StateData) -> - {stop, 'shutdown', StateData}; +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), + [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" + 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), + ItemAttrs = [{<<"affiliation">>, <<"none">>}, + {<<"role">>, <<"none">>}], + ReasonEl = #xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, ReasonT}]}, + Packet = #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = + [#xmlel{name = <<"item">>, + attrs = ItemAttrs, + children = [ReasonEl]}, + #xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"332">>}], + children = []}]}]}, + (?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 + if Reason == shutdown -> persist_muc_history(StateData); + true -> ok end, - mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(), + mod_muc:room_destroyed(StateData#state.host, + StateData#state.room, self(), StateData#state.server_host), ok. @@ -930,592 +921,596 @@ terminate(Reason, _StateName, StateData) -> %%% Internal functions %%%---------------------------------------------------------------------- -load_history(_Host, _Room, false, Queue) -> - Queue; +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}. + ?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(jlib:binary_to_integer(Ts)), + jlib:binary_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)), + iolist_to_binary(atom_to_list(HaveSubject)), + iolist_to_binary(integer_to_list(calendar:datetime_to_gregorian_seconds(Timestamp))), + iolist_to_binary(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}). + (?GEN_FSM):send_event(Pid, + {route, From, ToNick, Packet}). -process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, +process_groupchat_message(From, + #xmlel{name = <<"message">>, attrs = Attrs} = Packet, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), + 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 -> - mod_muc: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} + 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 -> + mod_muc: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 -> + send_multiple( + jlib:jid_replace_resource(StateData#state.jid, FromNick), + StateData#state.server_host, + StateData#state.users, + Packet), + 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) -> +is_user_allowed_message_nonparticipant(JID, + StateData) -> case get_service_affiliation(JID, StateData) of - owner -> - true; - _ -> false + 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} + 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, +process_presence(From, Nick, + #xmlel{name = <<"presence">>, attrs = Attrs} = 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), - mod_muc: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} + 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), + mod_muc: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(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(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). + (?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 + 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_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 == "" -> +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} + {OriginalId, Resource} -> + JIDWithResource = jlib:jid_replace_resource(JID, + Resource), + {is_user_online(JIDWithResource, StateData), OriginalId, + JIDWithResource} catch - _:_ -> - {is_user_online(JID, StateData), StanzaId, JID} + _:_ -> {is_user_online(JID, StateData), StanzaId, JID} end. -handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) -> +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 -> + 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) -> +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"), + <<"berd", + (jlib:encode_base64(<<"ejab\000", + OriginalId/binary, "\000", + Resource/binary>>))/binary>>. + +stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> + StanzaId = jlib:decode_base64(StanzaIdBase64), + [<<"ejab">>, OriginalId, Resource] = + str:tokens(StanzaId, <<"\000">>), {OriginalId, Resource}. change_stanzaid(NewId, Packet) -> - {xmlelement, Name, Attrs, Els} = jlib:remove_attr("id", Packet), - {xmlelement, Name, [{"id", NewId} | Attrs], Els}. + #xmlel{name = Name, attrs = Attrs, children = Els} = + jlib:remove_attr(<<"id">>, Packet), + #xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs], + children = 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" + 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" + 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 + <<"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 + <<"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 +decide_fate_message(<<"error">>, Packet, From, + StateData) -> 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 + %% 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 + {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. -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 + <<"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. + 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}. + #xmlel{children = EEls} = xml:get_subtag(Packet, + <<"error">>), + [Condition] = [Name + || #xmlel{name = Name, + attrs = [{<<"xmlns">>, ?NS_STANZAS}], + children = []} + <- 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). - + ErrorCondition = get_error_condition(Packet), + Reason2 = iolist_to_binary( + io_lib:format(binary_to_list(Reason1) ++ ": " ++ "~s", + [ErrorCondition])), + NewState = add_user_presence_un(From, + #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, + <<"unavailable">>}], + children = + [#xmlel{name = <<"status">>, + attrs = [], + children = + [{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, <<"">>). 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}, + 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; + {_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 + 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, + end, case Res of - {A, _Reason} -> - A; - _ -> - Res + {A, _Reason} -> A; + _ -> Res end. get_service_affiliation(JID, StateData) -> - {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = + {_AccessRoute, _AccessCreate, AccessAdmin, + _AccessPersistent} = StateData#state.access, - case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of - allow -> - owner; - _ -> - none + 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); + {U, S, <<"">>} -> + (?DICT):fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], StateData#state.users); _ -> - {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, + 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 + 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 + 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) -> @@ -1527,269 +1522,254 @@ is_moderator(Jid, StateData) -> get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), - if - MaxUsers =< ServiceMaxUsers -> MaxUsers; - true -> ServiceMaxUsers + if MaxUsers =< ServiceMaxUsers -> MaxUsers; + true -> ServiceMaxUsers end. get_service_max_users(StateData) -> gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users, ?MAX_USERS_DEFAULT). + mod_muc, max_users, + fun(I) when is_integer(I), I>0 -> I end, + ?MAX_USERS_DEFAULT). get_max_users_admin_threshold(StateData) -> gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users_admin_threshold, 5). + mod_muc, max_users_admin_threshold, + fun(I) when is_integer(I), I>0 -> I end, + 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, - mod_muc, user_message_shaper, none)), - PresenceShaper = - shaper:new(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, user_presence_shaper, none)), - #activity{message_shaper = MessageShaper, - presence_shaper = PresenceShaper} + StateData#state.activity) + of + {ok, _P, A} -> A; + error -> + MessageShaper = + shaper:new(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, user_message_shaper, + fun(A) when is_atom(A) -> A end, + none)), + PresenceShaper = + shaper:new(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, user_presence_shaper, + fun(A) when is_atom(A) -> A end, + 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, - mod_muc, min_message_interval, 0), + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_message_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 0), MinPresenceInterval = - gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, min_presence_interval, 0), + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_presence_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 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, + 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 + 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 + {{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), + 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), + 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, <<"">>). remove_online_user(JID, StateData, Reason) -> LJID = jlib:jid_tolower(JID), - {ok, #user{nick = Nick}} = - ?DICT:find(LJID, StateData#state.users), + {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 + 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}. +filter_presence(#xmlel{name = <<"presence">>, + attrs = Attrs, children = Els}) -> + FEls = lists:filter(fun (El) -> + case El of + {xmlcdata, _} -> false; + #xmlel{attrs = Attrs1} -> + XMLNS = xml:get_attr_s(<<"xmlns">>, + Attrs1), + NS_MUC = ?NS_MUC, + Size = byte_size(NS_MUC), + case XMLNS of + <<NS_MUC:Size/binary, _/binary>> -> + false; + _ -> + true + end + end + end, + Els), + #xmlel{name = <<"presence">>, attrs = Attrs, + children = FEls}. + +strip_status(#xmlel{name = <<"presence">>, + attrs = Attrs, children = Els}) -> + FEls = lists:filter(fun (#xmlel{name = <<"status">>}) -> + false; + (_) -> true + end, + Els), + #xmlel{name = <<"presence">>, attrs = Attrs, + children = 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), + 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), + 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 + 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 + 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) -> @@ -1798,1301 +1778,1357 @@ higher_presence(Pres1, 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 + case xml:get_subtag(PresencePacket, <<"priority">>) of + false -> 0; + SubEl -> + case catch + jlib:binary_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. + [{_, #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 + <<"">> -> 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)). + 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), +add_new_user(From, Nick, + #xmlel{attrs = Attrs, children = 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, + 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), + ServiceAffiliation = get_service_affiliation(From, + StateData), NConferences = tab_count_user(From), - MaxConferences = gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, max_user_conferences, 10), + MaxConferences = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_user_conferences, + fun(I) when is_integer(I), I>0 -> I end, + 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, + (Affiliation == admin orelse Affiliation == owner) + andalso NUsers < MaxAdminUsers + orelse NUsers < MaxUsers) + andalso NConferences < MaxConferences, Collision, - mod_muc: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 + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, From, Nick), + get_default_role(Affiliation, StateData)} + of + {false, _, _, _} -> + 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(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(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 = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"body">>, + attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"This room is not anonymous">>)}]}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + <<"100">>}], + children = + []}]}]}, + 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 = #xmlel{name = <<"message">>, + attrs = [{<<"id">>, ID}], + children = 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) -> +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 +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 + 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) +extract_password([]) -> false; +extract_password([#xmlel{attrs = Attrs} = 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). +extract_password([_ | Els]) -> extract_password(Els). count_stanza_shift(Nick, Els, StateData) -> HL = lqueue_to_list(StateData#state.history), - Since = extract_history(Els, "since"), + Since = extract_history(Els, <<"since">>), Shift0 = case Since of - false -> - 0; - _ -> - Sin = calendar:datetime_to_gregorian_seconds(Since), - count_seconds_shift(Sin, HL) + false -> 0; + _ -> + Sin = calendar:datetime_to_gregorian_seconds(Since), + count_seconds_shift(Sin, HL) end, - Seconds = extract_history(Els, "seconds"), + 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) + 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"), + MaxStanzas = extract_history(Els, <<"maxstanzas">>), Shift2 = case MaxStanzas of - false -> - 0; - _ -> - count_maxstanzas_shift(MaxStanzas, HL) + false -> 0; + _ -> count_maxstanzas_shift(MaxStanzas, HL) end, - MaxChars = extract_history(Els, "maxchars"), + MaxChars = extract_history(Els, <<"maxchars">>), Shift3 = case MaxChars of - false -> - 0; - _ -> - count_maxchars_shift(Nick, MaxChars, HL) + 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)). + 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 + 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), + NLen = byte_size(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, []) -> Shift; calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> - if - MaxSize >= Size -> - Shift; - true -> - calc_shift(MaxSize, Size - S, Shift + 1, 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) +extract_history([], _Type) -> false; +extract_history([#xmlel{attrs = Attrs} = 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 jlib:binary_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, <<"">>, 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 + {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) -> + lists:foreach(fun (J) -> send_new_presence(J, Reason, StateData) - end, LJIDs). + end, + LJIDs). send_new_presence(NJID, StateData) -> - 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. + #user{nick = Nick} = + (?DICT):fetch(jlib:jid_tolower(NJID), + StateData#state.users), 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), + {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}] + 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 + <<"">> -> []; + _ -> + [#xmlel{name = <<"reason">>, + attrs = [], + children = + [{xmlcdata, Reason}]}] + end, + Status = case StateData#state.just_created of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"201">>}], + children = []}]; + false -> [] + end, + Status2 = case + (StateData#state.config)#config.anonymous + == false + andalso NJID == Info#user.jid + of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"100">>}], + children = []} + | Status]; + false -> Status + end, + Status3 = case NJID == Info#user.jid of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"110">>}], + children = []} + | Status2]; + false -> Status2 + end, + Packet = xml:append_subtags(Presence, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs + = + ItemAttrs, + children + = + ItemEls} + | Status3]}]), + route_stanza(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet) 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)). - + (?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)). - + {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, + [#xmlel{name = + <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs, + children + = + []}]}]), + 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. - + (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 -> [] + {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), + 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), + 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}] + 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 = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, + <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs1, + children = + []}, + #xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + <<"303">>}], + children = + []}]}]}, + Packet2 = xml:append_subtags(Presence, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs2, + children + = + []}]}]), + 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, - 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)). - + (?DICT):to_list(StateData#state.users)). lqueue_new(Max) -> - #lqueue{queue = queue:new(), - len = 0, - max = 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; +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}) -> +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} + 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, 0) -> Q; lqueue_cut(Q, N) -> - {_, Q1} = queue:out(Q), - lqueue_cut(Q1, N - 1). + {_, 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 + 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, + 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), + [jlib:timestamp_to_xml(TimeStamp, utc, + SenderJid, <<"">>), + 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}, + 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))). - + 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) + <<"">> -> ok; + Nick -> + Subject = StateData#state.subject, + Packet = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"subject">>, attrs = [], + children = [{xmlcdata, Subject}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [{xmlcdata, + <<Nick/binary, + (translate:translate(Lang, + <<" has set the subject to: ">>))/binary, + Subject/binary>>}]}]}, + 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) + 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 + 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, + #xmlel{children = 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)} + 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 + 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)). + 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) -> + lists:map(fun ({JID, {Affiliation, Reason}}) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"affiliation">>, + affiliation_to_list(Affiliation)}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = + [#xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, Reason}]}]}; + ({JID, Affiliation}) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"affiliation">>, + affiliation_to_list(Affiliation)}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = []} + 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)}], - []}. + #xmlel{name = <<"item">>, + attrs = + [{<<"role">>, role_to_list(Role)}, + {<<"affiliation">>, affiliation_to_list(Affiliation)}, + {<<"nick">>, Nick}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = []}. search_role(Role, StateData) -> - lists:filter( - fun({_, #user{role = R}}) -> - Role == R - end, ?DICT:to_list(StateData#state.users)). + 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)). - + 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 -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, NSD#state.room, - make_opts(NSD)); - _ -> - ok - end, - {result, [], NSD}; - Err -> - Err + 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 -> + mod_muc: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) -> +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], + [{xmlcdata, _} | Items], Lang, StateData, Res) -> + find_changed_items(UJID, UAffiliation, URole, Items, + Lang, StateData, Res); +find_changed_items(UJID, UAffiliation, URole, + [#xmlel{name = <<"item">>, attrs = Attrs} = 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 + TJID = case xml:get_attr(<<"jid">>, Attrs) of + {value, S} -> + case jlib:string_to_jid(S) of + error -> + ErrText = iolist_to_binary( + 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 = iolist_to_binary( + 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 + {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 role: ~s"), - [StrRole]), - {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; - SRole -> + ErrText1 = iolist_to_binary( + 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, - 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, + 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 = [{Jidx, role, SRole, Reason} || Jidx <- JIDs], - find_changed_items( - UJID, - UAffiliation, URole, - Items, Lang, StateData, - [MoreRes | Res]); - _ -> - {error, ?ERR_NOT_ALLOWED} + 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; - Err -> - Err + end + end; + {value, StrRole} -> + case catch list_to_role(StrRole) of + {'EXIT', _} -> + ErrText1 = iolist_to_binary( + 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, +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) -> +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) -> +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) -> +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + TRole, role, Value, _ServiceAf) + when TRole == Value -> nothing; -can_change_ra(FAffiliation, _FRole, - outcast, _TRole, +can_change_ra(FAffiliation, _FRole, outcast, _TRole, affiliation, none, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - outcast, _TRole, +can_change_ra(FAffiliation, _FRole, outcast, _TRole, affiliation, member, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - outcast, _TRole, +can_change_ra(owner, _FRole, outcast, _TRole, affiliation, admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - outcast, _TRole, +can_change_ra(owner, _FRole, outcast, _TRole, affiliation, owner, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - none, _TRole, +can_change_ra(FAffiliation, _FRole, none, _TRole, affiliation, outcast, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - none, _TRole, +can_change_ra(FAffiliation, _FRole, none, _TRole, affiliation, member, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - none, _TRole, - affiliation, admin, _ServiceAf) -> +can_change_ra(owner, _FRole, none, _TRole, affiliation, + admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - none, _TRole, - affiliation, owner, _ServiceAf) -> +can_change_ra(owner, _FRole, none, _TRole, affiliation, + owner, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - member, _TRole, +can_change_ra(FAffiliation, _FRole, member, _TRole, affiliation, outcast, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - member, _TRole, +can_change_ra(FAffiliation, _FRole, member, _TRole, affiliation, none, _ServiceAf) - when (FAffiliation == owner) or (FAffiliation == admin) -> + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - member, _TRole, +can_change_ra(owner, _FRole, member, _TRole, affiliation, admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - member, _TRole, +can_change_ra(owner, _FRole, member, _TRole, affiliation, owner, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - admin, _TRole, - affiliation, _Affiliation, _ServiceAf) -> +can_change_ra(owner, _FRole, admin, _TRole, affiliation, + _Affiliation, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - owner, _TRole, - affiliation, _Affiliation, _ServiceAf) -> +can_change_ra(owner, _FRole, owner, _TRole, affiliation, + _Affiliation, _ServiceAf) -> check_owner; -can_change_ra(_FAffiliation, _FRole, - _TAffiliation, _TRole, - affiliation, _Value, _ServiceAf) -> +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + _TRole, affiliation, _Value, _ServiceAf) -> false; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, visitor, - role, none, _ServiceAf) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + visitor, role, none, _ServiceAf) -> true; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, visitor, - role, participant, _ServiceAf) -> +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) -> +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) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + participant, role, none, _ServiceAf) -> true; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, participant, - role, visitor, _ServiceAf) -> +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) -> +can_change_ra(FAffiliation, _FRole, _TAffiliation, + participant, role, moderator, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, _FRole, - owner, moderator, +can_change_ra(_FAffiliation, _FRole, owner, moderator, role, visitor, _ServiceAf) -> false; -can_change_ra(owner, _FRole, - _TAffiliation, moderator, +can_change_ra(owner, _FRole, _TAffiliation, moderator, role, visitor, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - admin, moderator, +can_change_ra(_FAffiliation, _FRole, admin, moderator, role, visitor, _ServiceAf) -> false; -can_change_ra(admin, _FRole, - _TAffiliation, moderator, +can_change_ra(admin, _FRole, _TAffiliation, moderator, role, visitor, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - owner, moderator, +can_change_ra(_FAffiliation, _FRole, owner, moderator, role, participant, _ServiceAf) -> false; -can_change_ra(owner, _FRole, - _TAffiliation, moderator, +can_change_ra(owner, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - admin, moderator, +can_change_ra(_FAffiliation, _FRole, admin, moderator, role, participant, _ServiceAf) -> false; -can_change_ra(admin, _FRole, - _TAffiliation, moderator, +can_change_ra(admin, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - _TAffiliation, _TRole, - role, _Value, _ServiceAf) -> +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). -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 + {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), + 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(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), +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)). - - + 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 + <<"">> -> []; + _ -> + [#xmlel{name = <<"reason">>, + attrs = [], + children = + [{xmlcdata, Reason}]}] + end, + Packet = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs, + children = + ItemEls}, + #xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + Code}], + children = + []}]}]}, + route_stanza(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet) + end, + (?DICT):to_list(StateData#state.users)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff @@ -3100,601 +3136,813 @@ send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) -> 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)} + owner -> + #xmlel{children = Els} = SubEl, + case xml:remove_cdata(Els) of + [#xmlel{name = <<"x">>} = 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; + [#xmlel{name = <<"destroy">>} = 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)} + owner -> + #xmlel{children = 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 = iolist_to_binary( + 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)) + 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)) + 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, + IsNameAccepted = case + lists:keysearch(<<"muc#roomconfig_roomname">>, 1, + jlib:parse_xdata_submit(XEl)) + of + {value, {_, [N]}} -> + byte_size(N) =< + gen_mod:get_module_opt(StateData#state.server_host, mod_muc, max_room_name, - infinite); - _ -> - true - end, - IsDescAccepted = - case lists:keysearch("muc#roomconfig_roomdesc", 1, - jlib:parse_xdata_submit(XEl)) of - {value, {_, [D]}} -> - length(D) =< gen_mod:get_module_opt(StateData#state.server_host, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> I + end, infinity); + _ -> true + end, + IsDescAccepted = case + lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, + jlib:parse_xdata_submit(XEl)) + of + {value, {_, [D]}} -> + byte_size(D) =< + gen_mod:get_module_opt(StateData#state.server_host, mod_muc, max_room_desc, - infinite); - _ -> - true - end, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> + I + end, infinity); + _ -> 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 + 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}]}]}). + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, Type}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). -define(BOOLXFIELD(Label, Var, Val), - ?XFIELD("boolean", Label, Var, + ?XFIELD(<<"boolean">>, Label, Var, case Val of - true -> "1"; - _ -> "0" + true -> <<"1">>; + _ -> <<"0">> end)). -define(STRINGXFIELD(Label, Var, Val), - ?XFIELD("text-single", Label, Var, Val)). + ?XFIELD(<<"text-single">>, Label, Var, Val)). -define(PRIVATEXFIELD(Label, Var, Val), - ?XFIELD("text-private", 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]}). + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"jid-multi">>}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, jlib:jid_to_string(JID)}]} + || JID <- JIDList]}). get_default_room_maxusers(RoomState) -> - DefRoomOpts = gen_mod:get_module_opt(RoomState#state.server_host, mod_muc, default_room_options, []), + DefRoomOpts = + gen_mod:get_module_opt(RoomState#state.server_host, + mod_muc, default_room_options, + fun(L) when is_list(L) -> L end, + []), RoomState2 = set_opts(DefRoomOpts, RoomState), (RoomState2#state.config)#config.max_users. get_config(Lang, StateData, From) -> - {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, + {_AccessRoute, _AccessCreate, _AccessAdmin, + AccessPersistent} = + StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), - DefaultRoomMaxUsers = get_default_room_maxusers(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}], + {MaxUsersRoomInteger, MaxUsersRoomString} = case + get_max_users(StateData) + of + N when is_integer(N) -> + {N, + erlang:integer_to_list(N)}; + _ -> {0, <<"none">>} + end, + Res = [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Configuration of room ~s">>), + [jlib:jid_to_string(StateData#state.jid)]))}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"hidden">>}, + {<<"var">>, <<"FORM_TYPE">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{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), + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Maximum Number of Occupants">>)}, + {<<"var">>, <<"muc#roomconfig_maxusers">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, MaxUsersRoomString}]}] + ++ + if is_integer(ServiceMaxUsers) -> []; + true -> + [#xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"No limit">>)}], + children = + [#xmlel{name = <<"value">>, + attrs = [], + children = + [{xmlcdata, + <<"none">>}]}]}] + end + ++ + [#xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + jlib:integer_to_binary(N)}], + children = + [#xmlel{name = <<"value">>, + attrs = [], + children = + [{xmlcdata, + jlib:integer_to_binary(N)}]}]} + || N + <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoomInteger + | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Present real Jabber IDs to">>)}, + {<<"var">>, <<"muc#roomconfig_whois">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + if Config#config.anonymous -> + <<"moderators">>; + true -> <<"anyone">> + end}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"moderators only">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"moderators">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"anyone">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{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)), + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Allow visitors to send private messages to">>)}, + {<<"var">>, + <<"allow_private_messages_from_visitors">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + case + Config#config.allow_private_messages_from_visitors + of + anyone -> <<"anyone">>; + moderators -> <<"moderators">>; + nobody -> <<"nobody">> + end}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"nobody">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, <<"nobody">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"moderators only">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"moderators">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"anyone">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{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">>, + (jlib:integer_to_binary(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, + [#xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"You need an x:data capable client to " + "configure room">>)}]}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], + children = 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 + 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} + <<"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} + case catch jlib:binary_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) -> + 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_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} + | Opts], + Config) -> ?SET_STRING_XOPT(description, Val); -set_xoption([{"muc#roomconfig_changesubject", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_change_subj, Val); -set_xoption([{"allow_query_users", [Val]} | Opts], Config) -> +set_xoption([{<<"allow_query_users">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(allow_query_users, Val); -set_xoption([{"allow_private_messages", [Val]} | Opts], Config) -> +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) -> +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} + <<"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_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_visitor_status, Val); -set_xoption([{"muc#roomconfig_allowvisitornickchange", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); -set_xoption([{"muc#roomconfig_publicroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(public, Val); -set_xoption([{"public_list", [Val]} | Opts], Config) -> +set_xoption([{<<"public_list">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(public_list, Val); -set_xoption([{"muc#roomconfig_persistentroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_persistentroom">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(persistent, Val); -set_xoption([{"muc#roomconfig_moderatedroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(moderated, Val); -set_xoption([{"members_by_default", [Val]} | Opts], Config) -> +set_xoption([{<<"members_by_default">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(members_by_default, Val); -set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(members_only, Val); -set_xoption([{"captcha_protected", [Val]} | Opts], Config) -> +set_xoption([{<<"captcha_protected">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(captcha_protected, Val); -set_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_user_invites, Val); -set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(password_protected, Val); -set_xoption([{"muc#roomconfig_roomsecret", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} + | Opts], + Config) -> ?SET_STRING_XOPT(password, Val); -set_xoption([{"anonymous", [Val]} | Opts], Config) -> +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) -> +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} + <<"moderators">> -> + ?SET_BOOL_XOPT(anonymous, + (iolist_to_binary(integer_to_list(1)))); + <<"anyone">> -> + ?SET_BOOL_XOPT(anonymous, + (iolist_to_binary(integer_to_list(0)))); + _ -> {error, ?ERR_BAD_REQUEST} end; -set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} + | Opts], + Config) -> case Val of - "none" -> - ?SET_STRING_XOPT(max_users, none); - _ -> - ?SET_NAT_XOPT(max_users, Val) + <<"none">> -> ?SET_STRING_XOPT(max_users, none); + _ -> ?SET_NAT_XOPT(max_users, Val) end; -set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(logging, Val); -set_xoption([{"muc#roomconfig_captcha_whitelist", Vals} | Opts], Config) -> +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([{<<"FORM_TYPE">>, _} | Opts], Config) -> set_xoption(Opts, Config); set_xoption([_ | _Opts], _Config) -> {error, ?ERR_BAD_REQUEST}. - change_config(Config, StateData) -> NSD = StateData#state{config = Config}, case {(StateData#state.config)#config.persistent, - Config#config.persistent} of - {_, true} -> - mod_muc:store_room(NSD#state.server_host, NSD#state.host, - NSD#state.room, make_opts(NSD)); - {true, false} -> - mod_muc:forget_room(NSD#state.server_host, NSD#state.host, - NSD#state.room); - {false, false} -> - ok + Config#config.persistent} + of + {_, true} -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, NSD#state.room, make_opts(NSD)); + {true, false} -> + mod_muc: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} + 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}}). + 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)). -set_opts([], StateData) -> - StateData; +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 + 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(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(public), ?MAKE_CONFIG_OPT(public_list), ?MAKE_CONFIG_OPT(persistent), ?MAKE_CONFIG_OPT(moderated), ?MAKE_CONFIG_OPT(members_by_default), @@ -3702,471 +3950,515 @@ make_opts(StateData) -> ?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(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)}, + (?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} - ]. - - + {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)), + lists:foreach(fun ({_LJID, Info}) -> + Nick = Info#user.nick, + ItemAttrs = [{<<"affiliation">>, <<"none">>}, + {<<"role">>, <<"none">>}], + Packet = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs, + children = + []}, + 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 -> - mod_muc:forget_room( - StateData#state.server_host, - StateData#state.host, StateData#state.room); - false -> - ok - end, + true -> + mod_muc: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(FEATURE(Var), + #xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}], + children = []}). -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), - case Opt of - true -> - ?FEATURE(Fiftrue); - false -> - ?FEATURE(Fiffalse) - end). + 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}. + {result, + [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"type">>, <<"text">>}, + {<<"name">>, get_title(StateData)}], + children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC}], children = []}, + ?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}]}]}). + #xmlel{name = <<"field">>, + attrs = [{<<"type">>, Type}, {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). -define(RFIELD(Label, Var, Val), - {xmlelement, "field", [{"label", translate:translate(Lang, Label)}, - {"var", Var}], - [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + #xmlel{name = <<"field">>, + attrs = + [{<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{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)) - ]}]. + Len = (?DICT):size(StateData#state.users), + RoomDescription = + (StateData#state.config)#config.description, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], + children = + [?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>, + <<"http://jabber.org/protocol/muc#roominfo">>), + ?RFIELD(<<"Room description">>, + <<"muc#roominfo_description">>, RoomDescription), + ?RFIELD(<<"Number of occupants">>, + <<"muc#roominfo_occupants">>, + (iolist_to_binary(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 + 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) -> +process_iq_captcha(_From, get, _Lang, _SubEl, + _StateData) -> {error, ?ERR_NOT_ALLOWED}; - -process_iq_captcha(_From, set, _Lang, SubEl, StateData) -> +process_iq_captcha(_From, set, _Lang, SubEl, + StateData) -> case ejabberd_captcha:process_reply(SubEl) of - ok -> - {result, [], StateData}; - _ -> - {error, ?ERR_NOT_ACCEPTABLE} + ok -> {result, [], StateData}; + _ -> {error, ?ERR_NOT_ACCEPTABLE} end. get_title(StateData) -> case (StateData#state.config)#config.title of - "" -> - StateData#state.room; - Name -> - Name + <<"">> -> 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 + 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))/binary,Tail/binary>>}; + 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, ") + true -> <<"">>; + _ -> translate:translate(Lang, <<"private, ">>) end, - Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, StateData#state.users), - " (" ++ Desc ++ integer_to_list(Len) ++ ")". + Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0, + StateData#state.users), + <<" (", Desc/binary, + (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. 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)). + lists:map(fun ({_LJID, Info}) -> + Nick = Info#user.nick, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({StateData#state.room, + StateData#state.host, + Nick})}, + {<<"name">>, Nick}], + children = []} + 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). + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = 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")) - ]}]}. + #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"normal">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Voice request">>)}]}, + #xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Either approve or decline the voice " + "request.">>)}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"var">>, <<"FORM_TYPE">>}, + {<<"type">>, <<"hidden">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"http://jabber.org/protocol/muc#request">>}]}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"var">>, <<"muc#role">>}, + {<<"type">>, <<"hidden">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{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">>, + (jlib:binary_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). + 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). + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = 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). + lists:foldl(fun (#xmlel{name = <<"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). + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = 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), + 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}) + [#xmlel{name = <<"x">>, children = 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 + [#xmlel{name = <<"invite">>} = 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 + 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 + 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 = [#xmlel{name = <<"invite">>, + attrs = [{<<"from">>, jlib:jid_to_string(From)}], + children = + [#xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, Reason}]}] + ++ ContinueEl}], + PasswdEl = 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 + [#xmlel{name = <<"password">>, attrs = [], + children = + [{xmlcdata, + (StateData#state.config)#config.password}]}]; + _ -> [] + end, + Body = #xmlel{name = <<"body">>, attrs = [], + children = + [{xmlcdata, + iolist_to_binary( + [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">>))/binary, + " '", + ((StateData#state.config)#config.password)/binary, + "'">>; + _ -> <<"">> + end + , + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end])}]}, + Msg = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"normal">>}], + children = + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = IEl ++ PasswdEl}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XCONFERENCE}, + {<<"jid">>, + jlib:jid_to_string({StateData#state.room, + StateData#state.host, + <<"">>})}], + children = [{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) -> +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) + {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), + #xmlel{name = <<"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}, +send_decline_invitation({Packet, XEl, DEl, ToJID}, + RoomJID, FromJID) -> + FromString = + jlib:jid_to_string(jlib:jid_remove_resource(FromJID)), + #xmlel{name = <<"decline">>, attrs = DAttrs, + children = DEls} = + DEl, + DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), + DAttrs3 = [{<<"from">>, FromString} | DAttrs2], + DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, + children = 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) -> +replace_subelement(#xmlel{name = Name, attrs = Attrs, + children = 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)), + SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, + NewSubEl), + #xmlel{name = Name, attrs = Attrs, children = 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)); + when Type == roomconfig_change_disabledlogging -> + 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 + true -> + mod_muc_log:add_to_log(StateData#state.server_host, + Type, Data, StateData#state.jid, + make_opts(StateData)); + false -> ok end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -4177,45 +4469,46 @@ tab_add_online_user(JID, StateData) -> 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}). - + 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}). + 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 + 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)). +element_size(El) -> byte_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) + 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. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Multicast + +send_multiple(From, Server, Users, Packet) -> + JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], + ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl index ecc044909..b42e4757d 100644 --- a/src/mod_muc/mod_muc_room.hrl +++ b/src/mod_muc/mod_muc_room.hrl @@ -22,70 +22,96 @@ -define(MAX_USERS_DEFAULT, 200). -define(SETS, gb_sets). + -define(DICT, dict). --record(lqueue, {queue, len, max}). +-record(lqueue, +{ + queue :: queue(), + len :: integer(), + max :: integer() +}). + +-type lqueue() :: #lqueue{}. + +-record(config, +{ + title = <<"">> :: binary(), + description = <<"">> :: binary(), + allow_change_subj = true :: boolean(), + allow_query_users = true :: boolean(), + allow_private_messages = true :: boolean(), + allow_private_messages_from_visitors = anyone :: anyone | moderators | nobody , + allow_visitor_status = true :: boolean(), + allow_visitor_nickchange = true :: boolean(), + public = true :: boolean(), + public_list = true :: boolean(), + persistent = false :: boolean(), + moderated = true :: boolean(), + captcha_protected = false :: boolean(), + members_by_default = true :: boolean(), + members_only = false :: boolean(), + allow_user_invites = false :: boolean(), + password_protected = false :: boolean(), + password = <<"">> :: binary(), + anonymous = true :: boolean(), + allow_voice_requests = true :: boolean(), + voice_request_min_interval = 1800 :: non_neg_integer(), + max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none, + logging = false :: boolean(), + captcha_whitelist = (?SETS):empty() :: gb_set() +}). + +-type config() :: #config{}. + +-type role() :: moderator | participant | visitor | none. + +-record(user, +{ + jid :: jid(), + nick :: binary(), + role :: role(), + last_presence :: xmlel() +}). --record(config, {title = "", - description = "", - allow_change_subj = true, - allow_query_users = true, - allow_private_messages = true, - allow_private_messages_from_visitors = anyone, - allow_visitor_status = true, - allow_visitor_nickchange = true, - public = true, - public_list = true, - persistent = false, - moderated = true, - captcha_protected = false, - members_by_default = true, - members_only = false, - allow_user_invites = false, - password_protected = false, - password = "", - anonymous = true, - allow_voice_requests = true, - voice_request_min_interval = 1800, - max_users = ?MAX_USERS_DEFAULT, - logging = false, - captcha_whitelist = ?SETS:empty() - }). +-record(activity, +{ + message_time = 0 :: integer(), + presence_time = 0 :: integer(), + message_shaper :: shaper:shaper(), + presence_shaper :: shaper:shaper(), + message :: xmlel(), + presence :: {binary(), xmlel()} +}). --record(user, {jid, - nick, - role, - last_presence}). +-record(state, +{ + room = <<"">> :: binary(), + host = <<"">> :: binary(), + server_host = <<"">> :: binary(), + access = {none,none,none,none} :: {atom(), atom(), atom(), atom()}, + jid = #jid{} :: jid(), + config = #config{} :: config(), + users = (?DICT):new() :: dict(), + last_voice_request_time = treap:empty() :: treap:treap(), + robots = (?DICT):new() :: dict(), + nicks = (?DICT):new() :: dict(), + affiliations = (?DICT):new() :: dict(), + history :: lqueue(), + persist_history = false :: boolean(), + subject = <<"">> :: binary(), + subject_author = <<"">> :: binary(), + just_created = false :: boolean(), + activity = treap:empty() :: treap:treap(), + room_shaper = none :: shaper:shaper(), + room_queue = queue:new() :: queue() +}). --record(activity, {message_time = 0, - presence_time = 0, - message_shaper, - presence_shaper, - message, - presence}). +-record(muc_online_users, {us = {<<>>, <<>>} :: {binary(), binary()}, + resource = <<>> :: binary() | '_', + room = <<>> :: binary() | '_', + host = <<>> :: binary() | '_'}). --record(state, {room, - host, - server_host, - mod, - access, - jid, - config = #config{}, - users = ?DICT:new(), - last_voice_request_time = treap:empty(), - robots = ?DICT:new(), - nicks = ?DICT:new(), - affiliations = ?DICT:new(), - history, - persist_history = false, - subject = "", - subject_author = "", - just_created = false, - activity = treap:empty(), - room_shaper, - room_queue = queue:new()}). +-type muc_online_users() :: #muc_online_users{}. --record(muc_online_users, {us, - resource, - room, - host}). +-type muc_room_state() :: #state{}. |