+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2021 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_conversejs).
+
+-author('alexey@process-one.net').
+
+-behaviour(gen_mod).
+
+-export([start/2, stop/1, reload/3, process/2, depends/2,
+ mod_opt_type/1, mod_options/1, mod_doc/0]).
+
+-include_lib("xmpp/include/xmpp.hrl").
+-include("logger.hrl").
+-include("ejabberd_http.hrl").
+-include("translate.hrl").
+-include("ejabberd_web_admin.hrl").
+
+start(_Host, _Opts) ->
+ ok.
+
+stop(_Host) ->
+ ok.
+
+reload(_Host, _NewOpts, _OldOpts) ->
+ ok.
+
+depends(_Host, _Opts) ->
+ [].
+
+process([], #request{method = 'GET'}) ->
+ Host = ejabberd_config:get_myname(),
+ Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain),
+ Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script),
+ CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css),
+ Init = [{<<"discover_connection_methods">>, false},
+ {<<"jid">>, Domain},
+ {<<"default_domain">>, Domain},
+ {<<"domain_placeholder">>, Domain},
+ {<<"view_mode">>, <<"fullscreen">>}],
+ Init2 =
+ case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of
+ undefined -> Init;
+ WSURL -> [{<<"websocket_url">>, WSURL} | Init]
+ end,
+ Init3 =
+ case gen_mod:get_module_opt(Host, ?MODULE, bosh_service_url) of
+ undefined -> Init2;
+ BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2]
+ end,
+ {200, [html],
+ [<<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>,
+ <<"">>]};
+process(_, _) ->
+ ejabberd_web:error(not_found).
+
+mod_opt_type(bosh_service_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(websocket_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(conversejs_script) ->
+ econf:binary();
+mod_opt_type(conversejs_css) ->
+ econf:binary();
+mod_opt_type(default_domain) ->
+ econf:binary().
+
+mod_options(_) ->
+ [{bosh_service_url, undefined},
+ {websocket_url, undefined},
+ {default_domain, ejabberd_config:get_myname()},
+ {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>},
+ {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}].
+
+mod_doc() ->
+ #{desc =>
+ [?T("This module serves a simple page for the "
+ "https://conversejs.org/[Converse] XMPP web browser client."), "",
+ ?T("This module is available since ejabberd 21.12."), "",
+ ?T("To use this module, in addition to adding it to the 'modules' "
+ "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers]."), "",
+ ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "",
+ ?T("By default, the options 'conversejs_css' and 'conversejs_script'"
+ " point to the public Converse.js client. Alternatively, you can"
+ " host the client locally using _`mod_http_fileserver`_.")
+ ],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /websocket: ejabberd_http_ws",
+ " /conversejs: mod_conversejs",
+ "",
+ "modules:",
+ " mod_conversejs:",
+ " websocket_url: \"ws://example.org:5280/websocket\""],
+ opts =>
+ [{websocket_url,
+ #{value => ?T("WebsocketURL"),
+ desc =>
+ ?T("A websocket URL to which Converse.js can connect to.")}},
+ {bosh_service_url,
+ #{value => ?T("BoshURL"),
+ desc =>
+ ?T("BOSH service URL to which Converse.js can connect to.")}},
+ {default_domain,
+ #{value => ?T("Domain"),
+ desc =>
+ ?T("Specify a domain to act as the default for user JIDs. "
+ "The default value is the first domain defined in the "
+ "ejabberd configuration file.")}},
+ {conversejs_script,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js main script URL.")}},
+ {conversejs_css,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js CSS URL.")}}]
+ }.
diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl
new file mode 100644
index 000000000..9e53978ea
--- /dev/null
+++ b/src/mod_conversejs_opt.erl
@@ -0,0 +1,41 @@
+%% Generated automatically
+%% DO NOT EDIT: run `make options` instead
+
+-module(mod_conversejs_opt).
+
+-export([bosh_service_url/1]).
+-export([conversejs_css/1]).
+-export([conversejs_script/1]).
+-export([default_domain/1]).
+-export([websocket_url/1]).
+
+-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+bosh_service_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(bosh_service_url, Opts);
+bosh_service_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url).
+
+-spec conversejs_css(gen_mod:opts() | global | binary()) -> binary().
+conversejs_css(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_css, Opts);
+conversejs_css(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css).
+
+-spec conversejs_script(gen_mod:opts() | global | binary()) -> binary().
+conversejs_script(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_script, Opts);
+conversejs_script(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_script).
+
+-spec default_domain(gen_mod:opts() | global | binary()) -> binary().
+default_domain(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(default_domain, Opts);
+default_domain(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, default_domain).
+
+-spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+websocket_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(websocket_url, Opts);
+websocket_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, websocket_url).
+
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 427833584..023df39ca 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -30,6 +30,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, process/2, depends/2,
+ format_arg/2,
mod_options/1, mod_doc/0]).
-include_lib("xmpp/include/xmpp.hrl").
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index abb2333cc..9bf154f58 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -28,6 +28,7 @@
-protocol({xep, 313, '0.6.1'}).
-protocol({xep, 334, '0.2'}).
-protocol({xep, 359, '0.5.0'}).
+-protocol({xep, 441, '0.2.0'}).
-behaviour(gen_mod).
diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl
index ca025e3d2..e7737804e 100644
--- a/src/mod_mqtt_session.erl
+++ b/src/mod_mqtt_session.erl
@@ -1134,8 +1134,8 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) ->
%%% Authentication
%%%===================================================================
-spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}.
-parse_credentials(#connect{client_id = <<>>}) ->
- parse_credentials(#connect{client_id = p1_rand:get_string()});
+parse_credentials(#connect{client_id = <<>>} = C) ->
+ parse_credentials(C#connect{client_id = p1_rand:get_string()});
parse_credentials(#connect{username = <<>>, client_id = ClientID}) ->
Host = ejabberd_config:get_myname(),
JID = case jid:make(ClientID, Host) of
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index b2ebc5c61..72f386b00 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -69,6 +69,7 @@
get_online_rooms_by_user/3,
can_use_nick/4,
get_subscribed_rooms/2,
+ remove_user/2,
procname/2,
route/1, unhibernate_room/3]).
@@ -122,6 +123,8 @@
start(Host, Opts) ->
case mod_muc_sup:start(Host) of
{ok, _} ->
+ ejabberd_hooks:add(remove_user, Host, ?MODULE,
+ remove_user, 50),
MyHosts = gen_mod:get_opt_hosts(Opts),
Mod = gen_mod:db_mod(Opts, ?MODULE),
RMod = gen_mod:ram_db_mod(Opts, ?MODULE),
@@ -133,6 +136,8 @@ start(Host, Opts) ->
end.
stop(Host) ->
+ ejabberd_hooks:delete(remove_user, Host, ?MODULE,
+ remove_user, 50),
Proc = mod_muc_sup:procname(Host),
supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
@@ -1122,6 +1127,32 @@ count_online_rooms(ServerHost, Host) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
RMod:count_online_rooms(ServerHost, Host).
+-spec remove_user(binary(), binary()) -> ok.
+remove_user(User, Server) ->
+ LUser = jid:nodeprep(User),
+ LServer = jid:nameprep(Server),
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ case erlang:function_exported(Mod, remove_user, 2) of
+ true ->
+ Mod:remove_user(LUser, LServer);
+ false ->
+ ok
+ end,
+ JID = jid:make(User, Server),
+ lists:foreach(
+ fun(Host) ->
+ lists:foreach(
+ fun({_, _, Pid}) ->
+ mod_muc_room:change_item_async(
+ Pid, JID, affiliation, none, <<"User removed">>),
+ mod_muc_room:change_item_async(
+ Pid, JID, role, none, <<"User removed">>)
+ end,
+ get_online_rooms(LServer, Host))
+ end,
+ gen_mod:get_module_opt_hosts(LServer, mod_muc)),
+ ok.
+
opts_to_binary(Opts) ->
lists:map(
fun({title, Title}) ->
@@ -1225,6 +1256,8 @@ mod_opt_type(user_message_shaper) ->
econf:atom();
mod_opt_type(user_presence_shaper) ->
econf:atom();
+mod_opt_type(cleanup_affiliations_on_start) ->
+ econf:bool();
mod_opt_type(default_room_options) ->
econf:options(
#{allow_change_subj => econf:bool(),
@@ -1302,6 +1335,7 @@ mod_options(Host) ->
{preload_rooms, true},
{hibernation_timeout, infinity},
{vcard, undefined},
+ {cleanup_affiliations_on_start, false},
{default_room_options,
[{allow_change_subj,true},
{allow_private_messages,true},
@@ -1580,6 +1614,11 @@ mod_doc() ->
" -",
" work: true",
" street: Elm Street"]}]}},
+ {cleanup_affiliations_on_start,
+ #{value => "true | false",
+ desc =>
+ ?T("Remove affiliations for non-existing local users on startup. "
+ "The default value is 'false'.")}},
{default_room_options,
#{value => ?T("Options"),
desc =>
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 2abeee45c..ac2d887fe 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -40,8 +40,11 @@
change_room_option/4, get_room_options/2,
set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3,
web_menu_main/2, web_page_main/2, web_menu_host/3,
- subscribe_room/4, unsubscribe_room/2, get_subscribers/2,
- web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]).
+ subscribe_room/4, subscribe_room_many/3,
+ unsubscribe_room/2, get_subscribers/2,
+ web_page_host/3,
+ mod_opt_type/1, mod_options/1,
+ get_commands_spec/0, find_hosts/1]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
@@ -281,7 +284,7 @@ get_commands_spec() ->
#ejabberd_commands{name = send_direct_invitation, tags = [muc_room],
desc = "Send a direct invitation to several destinations",
- longdesc = "Since ejabberd 20.10, this command is "
+ longdesc = "Since ejabberd 20.12, this command is "
"asynchronous: the API call may return before the "
"server has send all the invitations.\n\n"
"Password and Message can also be: none. "
@@ -331,6 +334,26 @@ get_commands_spec() ->
args = [{user, binary}, {nick, binary}, {room, binary},
{nodes, binary}],
result = {nodes, {list, {node, string}}}},
+ #ejabberd_commands{name = subscribe_room_many, tags = [muc_room],
+ desc = "Subscribe several users to a MUC conference",
+ longdesc = "This command accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)",
+ module = ?MODULE, function = subscribe_room_many,
+ args_desc = ["Users JIDs and nicks",
+ "the room to subscribe",
+ "nodes separated by commas: ,"],
+ args_example = [[{"tom@localhost", "Tom"},
+ {"jerry@localhost", "Jerry"}],
+ "room1@conference.localhost",
+ "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"],
+ args = [{users, {list,
+ {user, {tuple,
+ [{jid, binary},
+ {nick, binary}
+ ]}}
+ }},
+ {room, binary},
+ {nodes, binary}],
+ result = {res, rescode}},
#ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
desc = "Unsubscribe from a MUC conference",
module = ?MODULE, function = unsubscribe_room,
@@ -710,7 +733,7 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->
maybe_store_room(ServerHost, Host, Name, RoomOpts) ->
case proplists:get_bool(persistent, RoomOpts) of
true ->
- {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
+ {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
ok;
false ->
ok
@@ -860,7 +883,14 @@ get_online_rooms(ServiceArg) ->
|| {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)]
end, Hosts).
-get_all_rooms(Host) ->
+get_all_rooms(ServiceArg) ->
+ Hosts = find_services(ServiceArg),
+ lists:flatmap(
+ fun(Host) ->
+ get_all_rooms2(Host)
+ end, Hosts).
+
+get_all_rooms2(Host) ->
ServerHost = ejabberd_router:host_of_route(Host),
OnlineRooms = get_online_rooms(Host),
OnlineMap = lists:foldl(
@@ -1324,6 +1354,18 @@ subscribe_room(User, Nick, Room, Nodes) ->
throw({error, "Malformed room JID"})
end.
+subscribe_room_many(Users, Room, Nodes) ->
+ MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global),
+ if
+ length(Users) > MaxUsers ->
+ throw({error, "Too many users in subscribe_room_many command"});
+ true ->
+ lists:foreach(
+ fun({User, Nick}) ->
+ subscribe_room(User, Nick, Room, Nodes)
+ end, Users)
+ end.
+
unsubscribe_room(User, Room) ->
try jid:decode(Room) of
#jid{luser = Name, lserver = Host} when Name /= <<"">> ->
@@ -1406,11 +1448,22 @@ find_hosts(ServerHost) ->
[]
end.
-mod_options(_) -> [].
+mod_opt_type(subscribe_room_many_max_users) ->
+ econf:int().
+
+mod_options(_) ->
+ [{subscribe_room_many_max_users, 50}].
mod_doc() ->
#{desc =>
[?T("This module provides commands to administer local MUC "
"services and their MUC rooms. It also provides simple "
"WebAdmin pages to view the existing rooms."), "",
- ?T("This module depends on _`mod_muc`_.")]}.
+ ?T("This module depends on _`mod_muc`_.")],
+ opts =>
+ [{subscribe_room_many_max_users,
+ #{value => ?T("Number"),
+ desc =>
+ ?T("How many users can be subscribed to a room at once using "
+ "the 'subscribe_room_many' command. "
+ "The default value is '50'.")}}]}.
diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl
new file mode 100644
index 000000000..18ca64af7
--- /dev/null
+++ b/src/mod_muc_admin_opt.erl
@@ -0,0 +1,13 @@
+%% Generated automatically
+%% DO NOT EDIT: run `make options` instead
+
+-module(mod_muc_admin_opt).
+
+-export([subscribe_room_many_max_users/1]).
+
+-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer().
+subscribe_room_many_max_users(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(subscribe_room_many_max_users, Opts);
+subscribe_room_many_max_users(Host) ->
+ gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users).
+
diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl
index 760a5d7c8..4b9e8b806 100644
--- a/src/mod_muc_opt.erl
+++ b/src/mod_muc_opt.erl
@@ -9,6 +9,7 @@
-export([access_mam/1]).
-export([access_persistent/1]).
-export([access_register/1]).
+-export([cleanup_affiliations_on_start/1]).
-export([db_type/1]).
-export([default_room_options/1]).
-export([hibernation_timeout/1]).
@@ -73,6 +74,12 @@ access_register(Opts) when is_map(Opts) ->
access_register(Host) ->
gen_mod:get_module_opt(Host, mod_muc, access_register).
+-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean().
+cleanup_affiliations_on_start(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(cleanup_affiliations_on_start, Opts);
+cleanup_affiliations_on_start(Host) ->
+ gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start).
+
-spec db_type(gen_mod:opts() | global | binary()) -> atom().
db_type(Opts) when is_map(Opts) ->
gen_mod:get_opt(db_type, Opts);
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 035e851fd..aaf3e8895 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -27,6 +27,8 @@
-author('alexey@process-one.net').
+-protocol({xep, 317, '0.1'}).
+
-behaviour(p1_fsm).
%% External exports
@@ -48,6 +50,7 @@
set_config/2,
get_state/1,
change_item/5,
+ change_item_async/5,
config_reloaded/1,
subscribe/4,
unsubscribe/2,
@@ -76,6 +79,12 @@
-define(DEFAULT_MAX_USERS_PRESENCE,1000).
+-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>).
+-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>).
+-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>).
+-define(MAX_HATS_USERS, 100).
+-define(MAX_HATS_PER_USER, 10).
+
%-define(DBGFSM, true).
-ifdef(DBGFSM).
@@ -194,6 +203,11 @@ change_item(Pid, JID, Type, AffiliationOrRole, Reason) ->
{error, notfound}
end.
+-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok.
+change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) ->
+ p1_fsm:send_all_state_event(
+ Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}).
+
-spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}.
get_state(Pid) ->
try p1_fsm:sync_send_all_state_event(Pid, get_state)
@@ -298,7 +312,8 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType])
room_shaper = Shaper}),
add_to_log(room_existence, started, State),
ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]),
- {ok, normal_state, reset_hibernate_timer(State)}.
+ State1 = cleanup_affiliations(State),
+ {ok, normal_state, reset_hibernate_timer(State1)}.
normal_state({route, <<"">>,
#message{from = From, type = Type, lang = Lang} = Packet},
@@ -446,6 +461,8 @@ normal_state({route, <<"">>,
process_iq_mucsub(From, IQ, StateData);
#xcaptcha{} ->
process_iq_captcha(From, IQ, StateData);
+ #adhoc_command{} ->
+ process_iq_adhoc(From, IQ, StateData);
_ ->
Txt = ?T("The feature requested is not "
"supported by the conference"),
@@ -664,6 +681,16 @@ handle_event({set_affiliations, Affiliations},
StateName, StateData) ->
NewStateData = set_affiliations(Affiliations, StateData),
{next_state, StateName, NewStateData};
+handle_event({process_item_change, Item, UJID}, StateName, StateData) ->
+ case process_item_change(Item, StateData, UJID) of
+ {error, _} ->
+ {next_state, StateName, StateData};
+ StateData ->
+ {next_state, StateName, StateData};
+ NSD ->
+ store_room(NSD),
+ {next_state, StateName, NSD}
+ end;
handle_event(_Event, StateName, StateData) ->
{next_state, StateName, StateData}.
@@ -712,6 +739,8 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
case process_item_change(Item, StateData, UJID) of
{error, _} = Err ->
{reply, Err, StateName, StateData};
+ StateData ->
+ {reply, {ok, StateData}, StateName, StateData};
NSD ->
store_room(NSD),
{reply, {ok, NSD}, StateName, NSD}
@@ -1405,6 +1434,12 @@ is_occupant_or_admin(JID, StateData) ->
_ -> false
end.
+%% Check if the user is an admin or owner.
+-spec is_admin(jid(), state()) -> boolean().
+is_admin(JID, StateData) ->
+ FAffiliation = get_affiliation(JID, StateData),
+ FAffiliation == admin orelse FAffiliation == owner.
+
%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-spec decide_fate_message(message(), jid(), state()) ->
@@ -1602,7 +1637,7 @@ do_get_affiliation_fallback(JID, StateData) ->
-spec get_affiliations(state()) -> affiliations().
get_affiliations(#state{config = #config{persistent = false}} = StateData) ->
- get_affiliations_callback(StateData);
+ get_affiliations_fallback(StateData);
get_affiliations(StateData) ->
Room = StateData#state.room,
Host = StateData#state.host,
@@ -1610,13 +1645,13 @@ get_affiliations(StateData) ->
Mod = gen_mod:db_mod(ServerHost, mod_muc),
case Mod:get_affiliations(ServerHost, Room, Host) of
{error, _} ->
- get_affiliations_callback(StateData);
+ get_affiliations_fallback(StateData);
{ok, Affiliations} ->
Affiliations
end.
--spec get_affiliations_callback(state()) -> affiliations().
-get_affiliations_callback(StateData) ->
+-spec get_affiliations_fallback(state()) -> affiliations().
+get_affiliations_fallback(StateData) ->
StateData#state.affiliations.
-spec get_service_affiliation(jid(), state()) -> owner | none.
@@ -1935,7 +1970,7 @@ filter_presence(Presence) ->
XMLNS = xmpp:get_ns(El),
case catch binary:part(XMLNS, 0, size(?NS_MUC)) of
?NS_MUC -> false;
- _ -> true
+ _ -> XMLNS /= ?NS_HATS
end
end, xmpp:get_els(Presence)),
xmpp:set_els(Presence, Els).
@@ -2485,9 +2520,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
Pres = if Presence == undefined -> #presence{};
true -> Presence
end,
- Packet = xmpp:set_subtag(
- Pres, #muc_user{items = [Item],
- status_codes = StatusCodes}),
+ Packet = xmpp:set_subtag(
+ add_presence_hats(NJID, Pres, StateData),
+ #muc_user{items = [Item],
+ status_codes = StatusCodes}),
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
@@ -2536,7 +2572,9 @@ send_existing_presences1(ToJID, StateData) ->
false -> Item0
end,
Packet = xmpp:set_subtag(
- Presence, #muc_user{items = [Item]}),
+ add_presence_hats(
+ FromJID, Presence, StateData),
+ #muc_user{items = [Item]}),
send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
end
@@ -3579,7 +3617,8 @@ get_config(Lang, StateData, From) ->
{allow_voice_requests, Config#config.allow_voice_requests},
{allow_subscription, Config#config.allow_subscription},
{voice_request_min_interval, Config#config.voice_request_min_interval},
- {pubsub, Config#config.pubsub}]
+ {pubsub, Config#config.pubsub},
+ {enable_hats, Config#config.enable_hats}]
++
case ejabberd_captcha:is_feature_available() of
true ->
@@ -3667,6 +3706,7 @@ set_config(Opts, Config, ServerHost, Lang) ->
({maxusers, V}, C) -> C#config{max_users = V};
({enablelogging, V}, C) -> C#config{logging = V};
({pubsub, V}, C) -> C#config{pubsub = V};
+ ({enable_hats, V}, C) -> C#config{enable_hats = V};
({lang, L}, C) -> C#config{lang = L};
({captcha_whitelist, Js}, C) ->
LJIDs = [jid:tolower(J) || J <- Js],
@@ -3897,6 +3937,9 @@ set_opts([{Opt, Val} | Opts], StateData) ->
allow_subscription ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
+ enable_hats ->
+ StateData#state{config =
+ (StateData#state.config)#config{enable_hats = Val}};
lang ->
StateData#state{config =
(StateData#state.config)#config{lang = Val}};
@@ -3927,6 +3970,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
end,
StateData#state{subject = Subj};
subject_author -> StateData#state{subject_author = Val};
+ hats_users ->
+ Hats = maps:from_list(
+ lists:map(fun({U, H}) -> {U, maps:from_list(H)} end,
+ Val)),
+ StateData#state{hats_users = Hats};
_ -> StateData
end,
set_opts(Opts, NSD).
@@ -3983,6 +4031,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.vcard),
?MAKE_CONFIG_OPT(#config.vcard_xupdate),
?MAKE_CONFIG_OPT(#config.pubsub),
+ ?MAKE_CONFIG_OPT(#config.enable_hats),
?MAKE_CONFIG_OPT(#config.lang),
{captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@@ -3990,6 +4039,9 @@ make_opts(StateData) ->
maps:to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
{subject_author, StateData#state.subject_author},
+ {hats_users,
+ lists:map(fun({U, H}) -> {U, maps:to_list(H)} end,
+ maps:to_list(StateData#state.hats_users))},
{hibernation_time, erlang:system_time(microsecond)},
{subscribers, Subscribers}].
@@ -4080,6 +4132,7 @@ maybe_forget_room(StateData) ->
make_disco_info(_From, StateData) ->
Config = StateData#state.config,
Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+ ?NS_COMMANDS,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
@@ -4119,6 +4172,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
DiscoInfo = make_disco_info(From, StateData),
Extras = iq_disco_info_extras(Lang, StateData, false),
{result, DiscoInfo#disco_info{xdata = [Extras]}};
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-list">>,
+ name = translate:translate(
+ Lang, ?T("Commands"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = Node}]},
StateData) ->
@@ -4199,6 +4323,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>
{result, #disco_items{}}
end
end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_items{
+ items = [#disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_ADD_CMD,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_REMOVE_CMD,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_LIST_CMD,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = Node}]},
+ StateData)
+ when Node == ?MUC_HAT_ADD_CMD;
+ Node == ?MUC_HAT_REMOVE_CMD;
+ Node == ?MUC_HAT_LIST_CMD ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result, #disco_items{}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->
Txt = ?T("Node not found"),
{error, xmpp:err_item_not_found(Txt, Lang)}.
@@ -4441,6 +4605,271 @@ get_mucroom_disco_items(StateData) ->
end, [], StateData#state.nicks),
#disco_items{items = Items}.
+-spec process_iq_adhoc(jid(), iq(), state()) ->
+ {result, adhoc_command()} |
+ {result, adhoc_command(), state()} |
+ {error, stanza_error()}.
+process_iq_adhoc(_From, #iq{type = get}, _StateData) ->
+ {error, xmpp:err_bad_request()};
+process_iq_adhoc(From, #iq{type = set, lang = Lang1,
+ sub_els = [#adhoc_command{} = Request]},
+ StateData) ->
+ % Ad-Hoc Commands are used only for Hats here
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ #adhoc_command{lang = Lang2, node = Node,
+ action = Action, xdata = XData} = Request,
+ Lang = case Lang2 of
+ <<"">> -> Lang1;
+ _ -> Lang2
+ end,
+ case {Node, Action} of
+ {_, cancel} ->
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = canceled, lang = Lang,
+ node = Node})};
+ {?MUC_HAT_ADD_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Add a hat to a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_ADD_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ Title = case xmpp_util:get_xdata_values(
+ <<"hat_title">>, XData) of
+ [] -> <<"">>;
+ [T] -> T
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ case add_hat(JID, URI, Title, StateData) of
+ {ok, NewStateData} ->
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ {error, size_limit} ->
+ Txt = ?T("Hats limit exceeded"),
+ {error, xmpp:err_not_allowed(Txt, Lang)}
+ end;
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_ADD_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_ADD_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_REMOVE_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Remove a hat from a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ NewStateData = del_hat(JID, URI, StateData),
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_REMOVE_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_REMOVE_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_LIST_CMD, execute} ->
+ Hats = get_all_hats(StateData),
+ Items =
+ lists:map(
+ fun({JID, URI, Title}) ->
+ [#xdata_field{
+ var = <<"jid">>,
+ values = [jid:encode(JID)]},
+ #xdata_field{
+ var = <<"hat_title">>,
+ values = [URI]},
+ #xdata_field{
+ var = <<"hat_uri">>,
+ values = [Title]}]
+ end, Hats),
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("List of users with hats")),
+ type = result,
+ reported =
+ [#xdata_field{
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ var = <<"jid">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat URI")),
+ var = <<"hat_uri">>}],
+ items = Items},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = completed,
+ xdata = Form})};
+ {?MUC_HAT_LIST_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ _ ->
+ {error, xmpp:err_item_not_found()}
+ end;
+ _ ->
+ {error, xmpp:err_forbidden()}
+ end.
+
+-spec add_hat(jid(), binary(), binary(), state()) ->
+ {ok, state()} | {error, size_limit}.
+add_hat(JID, URI, Title, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:put(URI, Title, UserHats),
+ USize = maps:size(UserHats2),
+ if
+ USize =< ?MAX_HATS_PER_USER ->
+ Hats2 = maps:put(LJID, UserHats2, Hats),
+ Size = maps:size(Hats2),
+ if
+ Size =< ?MAX_HATS_USERS ->
+ {ok, StateData#state{hats_users = Hats2}};
+ true ->
+ {error, size_limit}
+ end;
+ true ->
+ {error, size_limit}
+ end.
+
+-spec del_hat(jid(), binary(), state()) -> state().
+del_hat(JID, URI, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:remove(URI, UserHats),
+ Hats2 =
+ case maps:size(UserHats2) of
+ 0 ->
+ maps:remove(LJID, Hats);
+ _ ->
+ maps:put(LJID, UserHats2, Hats)
+ end,
+ StateData#state{hats_users = Hats2}.
+
+-spec get_all_hats(state()) -> list({jid(), binary(), binary()}).
+get_all_hats(StateData) ->
+ lists:flatmap(
+ fun({LJID, H}) ->
+ JID = jid:make(LJID),
+ lists:map(fun({URI, Title}) -> {JID, URI, Title} end,
+ maps:to_list(H))
+ end,
+ maps:to_list(StateData#state.hats_users)).
+
+-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
+add_presence_hats(JID, Pres, StateData) ->
+ case (StateData#state.config)#config.enable_hats of
+ true ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ case maps:size(UserHats) of
+ 0 -> Pres;
+ _ ->
+ Items =
+ lists:map(fun({URI, Title}) ->
+ #muc_hat{uri = URI, title = Title}
+ end,
+ maps:to_list(UserHats)),
+ xmpp:set_subtag(Pres,
+ #muc_hats{hats = Items})
+ end;
+ false ->
+ Pres
+ end.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support
@@ -4687,7 +5116,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload1]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WJ, Packet1, true);
+ WJ, Packet1, false);
true -> ok
end,
if WN /= [] ->
@@ -4703,7 +5132,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload2]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WN, Packet2, true);
+ WN, Packet2, false);
true -> ok
end.
@@ -4927,6 +5356,23 @@ muc_subscribers_put(Subscriber, MUCSubscribers) ->
subscriber_nodes = NewSubNodes}.
+cleanup_affiliations(State) ->
+ case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of
+ true ->
+ Affiliations =
+ maps:filter(
+ fun({LUser, LServer, _}, _) ->
+ case ejabberd_router:is_my_host(LServer) of
+ true ->
+ ejabberd_auth:user_exists(LUser, LServer);
+ false ->
+ true
+ end
+ end, State#state.affiliations),
+ State#state{affiliations = Affiliations};
+ false ->
+ State
+ end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaningful content
diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl
index 1310cde7b..8aa7ad62b 100644
--- a/src/mod_muc_sql.erl
+++ b/src/mod_muc_sql.erl
@@ -38,7 +38,7 @@
register_online_user/4, unregister_online_user/4,
count_online_rooms_by_user/3, get_online_rooms_by_user/3,
get_subscribed_rooms/3, get_rooms_without_subscribers/2,
- find_online_room_by_pid/2]).
+ find_online_room_by_pid/2, remove_user/2]).
-export([set_affiliation/6, set_affiliations/4, get_affiliation/5,
get_affiliations/3, search_affiliation/4]).
@@ -465,6 +465,13 @@ get_subscribed_rooms(LServer, Host, Jid) ->
{error, db_failure}
end.
+remove_user(LUser, LServer) ->
+ SJID = jid:encode(jid:make(LUser, LServer)),
+ ejabberd_sql:sql_query(
+ LServer,
+ ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")),
+ ok.
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl
index 161d3a4c4..fa076da70 100644
--- a/src/mod_multicast.erl
+++ b/src/mod_multicast.erl
@@ -35,7 +35,7 @@
%% API
-export([start/2, stop/1, reload/3,
- user_send_packet/1]).
+ user_send_packet/1]).
%% gen_server callbacks
-export([init/1, handle_info/2, handle_call/3,
@@ -51,11 +51,6 @@
response,
ts :: integer()}).
--record(dest, {jid_string :: binary() | none,
- jid_jid :: jid() | undefined,
- type :: bcc | cc | noreply | ofrom | replyroom | replyto | to,
- address :: address()}).
-
-type limit_value() :: {default | custom, integer()}.
-record(limits, {message :: limit_value(),
presence :: limit_value()}).
@@ -63,14 +58,6 @@
-record(service_limits, {local :: #limits{},
remote :: #limits{}}).
--type routing() :: route_single | {route_multicast, binary(), #service_limits{}}.
-
--record(group, {server :: binary(),
- dests :: [#dest{}],
- multicast :: routing() | undefined,
- others :: [address()],
- addresses :: [address()]}).
-
-record(state, {lserver :: binary(),
lservice :: binary(),
access :: atom(),
@@ -117,7 +104,7 @@ reload(LServerS, NewOpts, OldOpts) ->
user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
case xmpp:get_subtag(Packet, #addresses{}) of
#addresses{list = Addresses} ->
- {ToDeliver, _Delivereds} = split_addresses_todeliver(Addresses),
+ {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses),
NewState =
lists:foldl(
fun(Address, St) ->
@@ -138,7 +125,7 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
undefined ->
St
end
- end, C2SState, ToDeliver),
+ end, C2SState, CC ++ BCC),
{Packet, NewState};
false ->
Acc
@@ -308,19 +295,10 @@ iq_vcard(Lang, State) ->
%%%-------------------------
-spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'.
-route_trusted(LServiceS, LServerS, FromJID,
- Destinations, Packet) ->
- Packet_stripped = Packet,
- Delivereds = [],
- Dests2 = lists:map(
- fun(D) ->
- #dest{jid_string = jid:encode(D),
- jid_jid = D, type = bcc,
- address = #address{type = bcc, jid = D}}
- end, Destinations),
- Groups = group_dests(Dests2),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
+route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) ->
+ Addresses = [#address{type = bcc, jid = D} || D <- Destinations],
+ Groups = group_by_destinations(Addresses, #{}),
+ route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet).
-spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'.
route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
@@ -356,50 +334,88 @@ route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) ->
FromJID = xmpp:get_from(Packet),
ok = check_access(LServerS, Access, FromJID),
- {ok, Packet_stripped, Addresses} = strip_addresses_element(Packet),
- {To_deliver, Delivereds} = split_addresses_todeliver(Addresses),
- Dests = convert_dest_record(To_deliver),
- {Dests2, Not_jids} = split_dests_jid(Dests),
- report_not_jid(FromJID, Packet, Not_jids),
- ok = check_limit_dests(SLimits, FromJID, Packet, Dests2),
- Groups = group_dests(Dests2),
+ {ok, PacketStripped, Addresses} = strip_addresses_element(Packet),
+ {CC, BCC, NotJids, Rest} = partition_addresses(Addresses),
+ report_not_jid(FromJID, Packet, NotJids),
+ ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)),
+ Groups0 = group_by_destinations(CC, #{}),
+ Groups = group_by_destinations(BCC, Groups0),
ok = check_relay(FromJID#jid.server, LServerS, Groups),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
+ route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped).
--spec route_common(binary(), binary(), jid(), [#group{}],
- [address()], stanza()) -> 'ok'.
-route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped) ->
- Groups2 = look_cached_servers(LServerS, LServiceS, Groups),
- Groups3 = build_others_xml(Groups2),
- Groups4 = add_addresses(Delivereds, Groups3),
- AGroups = decide_action_groups(Groups4),
- act_groups(FromJID, Packet_stripped, LServiceS,
- AGroups).
+-spec mark_as_delivered([address()]) -> [address()].
+mark_as_delivered(Addresses) ->
+ [A#address{delivered = true} || A <- Addresses].
--spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'.
-act_groups(FromJID, Packet_stripped, LServiceS, AGroups) ->
+-spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok.
+route_individual(From, CC, BCC, Other, Packet) ->
+ CCDelivered = mark_as_delivered(CC),
+ Addresses = CCDelivered ++ Other,
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
lists:foreach(
- fun(AGroup) ->
- perform(FromJID, Packet_stripped, LServiceS,
- AGroup)
- end, AGroups).
-
--spec perform(jid(), stanza(), binary(),
- {routing(), #group{}}) -> 'ok'.
-perform(From, Packet, _,
- {route_single, Group}) ->
+ fun(#address{jid = To}) ->
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To))
+ end, CC),
lists:foreach(
- fun(ToUser) ->
- Group_others = strip_other_bcc(ToUser, Group#group.others),
- route_packet(From, ToUser, Packet,
- Group_others, Group#group.addresses)
- end, Group#group.dests);
-perform(From, Packet, _,
- {{route_multicast, JID, RLimits}, Group}) ->
- route_packet_multicast(From, JID, Packet,
- Group#group.dests, Group#group.addresses, RLimits).
+ fun(#address{jid = To} = Address) ->
+ Packet2 = case Addresses of
+ [] ->
+ Packet;
+ _ ->
+ xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}])
+ end,
+ ejabberd_router:route(xmpp:set_from_to(Packet2, From, To))
+ end, BCC).
+
+-spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok.
+route_chunk(From, To, Packet, Addresses) ->
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)).
+
+-spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok.
+route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) ->
+ ok;
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, CC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, BCC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit - length(CC), BCC),
+ route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) ->
+ route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses).
+
+-spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok.
+route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) ->
+ {_Type, Limit} = get_limit_number(element(1, Packet),
+ Limits),
+ route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses).
+
+-spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok.
+route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) ->
+ maps:fold(
+ fun(Server, {CC, BCC}, _) ->
+ OtherCC = maps:fold(
+ fun(Server2, _, Res) when Server2 == Server ->
+ Res;
+ (_, {CC2, _}, Res) ->
+ mark_as_delivered(CC2) ++ Res
+ end, [], Groups),
+ case search_server_on_cache(Server,
+ LServer, LService,
+ {?MAXTIME_CACHE_POSITIVE,
+ ?MAXTIME_CACHE_NEGATIVE}) of
+ route_single ->
+ route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet);
+ {route_multicast, Service, Limits} ->
+ route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits)
+ end
+ end, ok, Groups).
%%%-------------------------
%%% Check access permission
@@ -425,245 +441,89 @@ strip_addresses_element(Packet) ->
throw(eadsele)
end.
-%%%-------------------------
-%%% Strip third-party bcc 'addresses'
-%%%-------------------------
-
-strip_other_bcc(#dest{jid_jid = ToUserJid}, Group_others) ->
- lists:filter(
- fun(#address{jid = JID, type = Type}) ->
- case {JID, Type} of
- {ToUserJid, bcc} -> true;
- {_, bcc} -> false;
- _ -> true
- end
- end,
- Group_others).
-
%%%-------------------------
%%% Split Addresses
%%%-------------------------
--spec split_addresses_todeliver([address()]) -> {[address()], [address()]}.
-split_addresses_todeliver(Addresses) ->
- lists:partition(
- fun(#address{delivered = true}) ->
- false;
- (#address{type = Type}) ->
- case Type of
- to -> true;
- cc -> true;
- bcc -> true;
- _ -> false
- end
- end, Addresses).
+partition_addresses(Addresses) ->
+ lists:foldl(
+ fun(#address{delivered = true} = A, {C, B, I, D}) ->
+ {C, B, I, [A | D]};
+ (#address{type = T, jid = undefined} = A, {C, B, I, D})
+ when T == to; T == cc; T == bcc ->
+ {C, B, [A | I], D};
+ (#address{type = T} = A, {C, B, I, D})
+ when T == to; T == cc ->
+ {[A | C], B, I, D};
+ (#address{type = bcc} = A, {C, B, I, D}) ->
+ {C, [A | B], I, D};
+ (A, {C, B, I, D}) ->
+ {C, B, I, [A | D]}
+ end, {[], [], [], []}, Addresses).
%%%-------------------------
%%% Check does not exceed limit of destinations
%%%-------------------------
--spec check_limit_dests(#service_limits{}, jid(), stanza(), [address()]) -> ok.
-check_limit_dests(SLimits, FromJID, Packet,
- Addresses) ->
+-spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok.
+check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) ->
SenderT = sender_type(FromJID),
Limits = get_slimit_group(SenderT, SLimits),
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- case length(Addresses) > Limit_number of
+ StanzaType = type_of_stanza(Packet),
+ {_Type, Limit} = get_limit_number(StanzaType,
+ Limits),
+ case NumOfAddresses > Limit of
false -> ok;
true -> throw(etoorec)
end.
-%%%-------------------------
-%%% Convert Destination XML to record
-%%%-------------------------
--spec convert_dest_record([address()]) -> [#dest{}].
-convert_dest_record(Addrs) ->
- lists:map(
- fun(#address{jid = undefined, type = Type} = Addr) ->
- #dest{jid_string = none,
- type = Type, address = Addr};
- (#address{jid = JID, type = Type} = Addr) ->
- #dest{jid_string = jid:encode(JID), jid_jid = JID,
- type = Type, address = Addr}
- end, Addrs).
-
-%%%-------------------------
-%%% Split destinations by existence of JID
-%%% and send error messages for other dests
-%%%-------------------------
-
--spec split_dests_jid([#dest{}]) -> {[#dest{}], [#dest{}]}.
-split_dests_jid(Dests) ->
- lists:partition(fun (Dest) ->
- case Dest#dest.jid_string of
- none -> false;
- _ -> true
- end
- end,
- Dests).
-
--spec report_not_jid(jid(), stanza(), [#dest{}]) -> any().
-report_not_jid(From, Packet, Dests) ->
- Dests2 = [fxml:element_to_binary(xmpp:encode(Dest#dest.address))
- || Dest <- Dests],
- [route_error(
- xmpp:set_from_to(Packet, From, From), jid_malformed,
- str:format(?T("This service can not process the address: ~s"), [D]))
- || D <- Dests2].
+-spec report_not_jid(jid(), stanza(), [address()]) -> any().
+report_not_jid(From, Packet, Addresses) ->
+ lists:foreach(
+ fun(Address) ->
+ route_error(
+ xmpp:set_from_to(Packet, From, From), jid_malformed,
+ str:format(?T("This service can not process the address: ~s"),
+ [fxml:element_to_binary(xmpp:encode(Address))]))
+ end, Addresses).
%%%-------------------------
%%% Group destinations by their servers
%%%-------------------------
--spec group_dests([#dest{}]) -> [#group{}].
-group_dests(Dests) ->
- D = lists:foldl(fun (Dest, Dict) ->
- ServerS = (Dest#dest.jid_jid)#jid.server,
- dict:append(ServerS, Dest, Dict)
- end,
- dict:new(), Dests),
- Keys = dict:fetch_keys(D),
- [#group{server = Key, dests = dict:fetch(Key, D),
- addresses = [], others = []}
- || Key <- Keys].
-
-%%%-------------------------
-%%% Look for cached responses
-%%%-------------------------
-
-look_cached_servers(LServerS, LServiceS, Groups) ->
- [look_cached(LServerS, LServiceS, Group) || Group <- Groups].
-
-look_cached(LServerS, LServiceS, G) ->
- Maxtime_positive = (?MAXTIME_CACHE_POSITIVE),
- Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE),
- Cached_response = search_server_on_cache(G#group.server,
- LServerS, LServiceS,
- {Maxtime_positive,
- Maxtime_negative}),
- G#group{multicast = Cached_response}.
-
-%%%-------------------------
-%%% Build delivered XML element
-%%%-------------------------
-
-build_others_xml(Groups) ->
- [Group#group{others =
- build_other_xml(Group#group.dests)}
- || Group <- Groups].
-
-build_other_xml(Dests) ->
- lists:foldl(fun (Dest, R) ->
- XML = Dest#dest.address,
- case Dest#dest.type of
- to -> [add_delivered(XML) | R];
- cc -> [add_delivered(XML) | R];
- _ -> [XML | R]
- end
- end,
- [], Dests).
-
--spec add_delivered(address()) -> address().
-add_delivered(Addr) ->
- Addr#address{delivered = true}.
-
-%%%-------------------------
-%%% Add preliminary packets
-%%%-------------------------
-
-add_addresses(Delivereds, Groups) ->
- Ps = [Group#group.others || Group <- Groups],
- add_addresses2(Delivereds, Groups, [], [], Ps).
-
-add_addresses2(_, [], Res, _, []) -> Res;
-add_addresses2(Delivereds, [Group | Groups], Res, Pa,
- [Pi | Pz]) ->
- Addresses = lists:append([Delivereds] ++ Pa ++ Pz),
- Group2 = Group#group{addresses = Addresses},
- add_addresses2(Delivereds, Groups, [Group2 | Res],
- [Pi | Pa], Pz).
-
-%%%-------------------------
-%%% Decide action groups
-%%%-------------------------
-
--spec decide_action_groups([#group{}]) -> [{routing(), #group{}}].
-decide_action_groups(Groups) ->
- [{Group#group.multicast, Group}
- || Group <- Groups].
+group_by_destinations(Addrs, Map) ->
+ lists:foldl(
+ fun
+ (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {[Addr | CC], BCC}
+ end, {[Addr], []}, Map2);
+ (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {CC, [Addr | BCC]}
+ end, {[], [Addr]}, Map2)
+ end, Map, Addrs).
%%%-------------------------
%%% Route packet
%%%-------------------------
--spec route_packet(jid(), #dest{}, stanza(), [addresses()], [addresses()]) -> 'ok'.
-route_packet(From, ToDest, Packet, Others, Addresses) ->
- Dests = case ToDest#dest.type of
- bcc -> [];
- _ -> [ToDest]
- end,
- route_packet2(From, ToDest#dest.jid_string, Dests,
- Packet, {Others, Addresses}).
-
--spec route_packet_multicast(jid(), binary(), stanza(), [#dest{}], [address()], #limits{}) -> 'ok'.
-route_packet_multicast(From, ToS, Packet, Dests,
- Addresses, Limits) ->
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- Fragmented_dests = fragment_dests(Dests, Limit_number),
- lists:foreach(fun(DFragment) ->
- route_packet2(From, ToS, DFragment, Packet,
- Addresses)
- end, Fragmented_dests).
-
--spec route_packet2(jid(), binary(), [#dest{}], stanza(), {[address()], [address()]} | [address()]) -> 'ok'.
-route_packet2(From, ToS, Dests, Packet, Addresses) ->
- Els = case append_dests(Dests, Addresses) of
- [] ->
- xmpp:get_els(Packet);
- ACs ->
- [#addresses{list = ACs}|xmpp:get_els(Packet)]
- end,
- Packet2 = xmpp:set_els(Packet, Els),
- ToJID = stj(ToS),
- ejabberd_router:route(xmpp:set_from_to(Packet2, From, ToJID)).
-
--spec append_dests([#dest{}], {[address()], [address()]} | [address()]) -> [address()].
-append_dests(_Dests, {Others, Addresses}) ->
- Addresses ++ Others;
-append_dests([], Addresses) -> Addresses;
-append_dests([Dest | Dests], Addresses) ->
- append_dests(Dests, [Dest#dest.address | Addresses]).
-
%%%-------------------------
%%% Check relay
%%%-------------------------
--spec check_relay(binary(), binary(), [#group{}]) -> ok.
+-spec check_relay(binary(), binary(), #{}) -> ok.
check_relay(RS, LS, Gs) ->
- case check_relay_required(RS, LS, Gs) of
- false -> ok;
- true -> throw(edrelay)
+ case lists:suffix(str:tokens(LS, <<".">>),
+ str:tokens(RS, <<".">>)) orelse
+ (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of
+ true -> ok;
+ _ -> throw(edrelay)
end.
--spec check_relay_required(binary(), binary(), [#group{}]) -> boolean().
-check_relay_required(RServer, LServerS, Groups) ->
- case lists:suffix(str:tokens(LServerS, <<".">>),
- str:tokens(RServer, <<".">>)) of
- true -> false;
- false -> check_relay_required(LServerS, Groups)
- end.
-
--spec check_relay_required(binary(), [#group{}]) -> boolean().
-check_relay_required(LServerS, Groups) ->
- lists:any(fun (Group) -> Group#group.server /= LServerS
- end,
- Groups).
-
%%%-------------------------
%%% Check protocol support: Send request
%%%-------------------------
@@ -1060,20 +920,6 @@ get_slimit_group(local, SLimits) ->
get_slimit_group(remote, SLimits) ->
SLimits#service_limits.remote.
-fragment_dests(Dests, Limit_number) ->
- {R, _} = lists:foldl(fun (Dest, {Res, Count}) ->
- case Count of
- Limit_number ->
- Head2 = [Dest], {[Head2 | Res], 0};
- _ ->
- [Head | Tail] = Res,
- Head2 = [Dest | Head],
- {[Head2 | Tail], Count + 1}
- end
- end,
- {[[]], 0}, Dests),
- R.
-
%%%-------------------------
%%% Limits: XEP-0128 Service Discovery Extensions
%%%-------------------------
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index 2e40d8f0e..d161ec10c 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -95,7 +95,7 @@
terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).
%% ejabberd commands
--export([get_commands_spec/0, delete_old_items/1]).
+-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]).
-export([route/1]).
@@ -3431,6 +3431,14 @@ max_items(Host, Options) ->
end
end.
+-spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity.
+item_expire(Host, Options) ->
+ case get_option(Options, item_expire) of
+ I when is_integer(I), I < 0 -> 0;
+ I when is_integer(I) -> I;
+ _ -> get_max_item_expire_node(Host)
+ end.
+
-spec get_configure_xfields(_, pubsub_node_config:result(),
binary(), [binary()]) -> [xdata_field()].
get_configure_xfields(_Type, Options, Lang, Groups) ->
@@ -3504,17 +3512,24 @@ decode_node_config(undefined, _, _) ->
decode_node_config(#xdata{fields = Fs}, Host, Lang) ->
try
Config = pubsub_node_config:decode(Fs),
- Max = get_max_items_node(Host),
- case {check_opt_range(max_items, Config, Max),
+ MaxItems = get_max_items_node(Host),
+ MaxExpiry = get_max_item_expire_node(Host),
+ case {check_opt_range(max_items, Config, MaxItems),
+ check_opt_range(item_expire, Config, MaxExpiry),
check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of
- {true, true} ->
+ {true, true, true} ->
Config;
- {true, false} ->
+ {true, true, false} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_payload_size">>,
?NS_PUBSUB_NODE_CONFIG}});
- {false, _} ->
+ {true, false, _} ->
+ erlang:error(
+ {pubsub_node_config,
+ {bad_var_value, <<"pubsub#item_expire">>,
+ ?NS_PUBSUB_NODE_CONFIG}});
+ {false, _, _} ->
erlang:error(
{pubsub_node_config,
{bad_var_value, <<"pubsub#max_items">>,
@@ -3560,20 +3575,24 @@ decode_get_pending(#xdata{fields = Fs}, Lang) ->
end.
-spec check_opt_range(atom(), [proplists:property()],
- non_neg_integer() | unlimited | undefined) -> boolean().
-check_opt_range(_Opt, _Opts, undefined) ->
- true;
+ non_neg_integer() | unlimited | infinity) -> boolean().
check_opt_range(_Opt, _Opts, unlimited) ->
true;
+check_opt_range(_Opt, _Opts, infinity) ->
+ true;
check_opt_range(Opt, Opts, Max) ->
case proplists:get_value(Opt, Opts, Max) of
max -> true;
Val -> Val =< Max
end.
--spec get_max_items_node(host()) -> undefined | unlimited | non_neg_integer().
+-spec get_max_items_node(host()) -> unlimited | non_neg_integer().
get_max_items_node(Host) ->
- config(Host, max_items_node, undefined).
+ config(Host, max_items_node, ?MAXITEMS).
+
+-spec get_max_item_expire_node(host()) -> infinity | non_neg_integer().
+get_max_item_expire_node(Host) ->
+ config(Host, max_item_expire_node, infinity).
-spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer().
get_max_subscriptions_node(Host) ->
@@ -4181,16 +4200,63 @@ delete_old_items(N) ->
ok
end.
+-spec delete_expired_items() -> ok | error.
+delete_expired_items() ->
+ Results = lists:flatmap(
+ fun(Host) ->
+ case tree_action(Host, get_all_nodes, [Host]) of
+ Nodes when is_list(Nodes) ->
+ lists:map(
+ fun(#pubsub_node{id = Nidx, type = Type,
+ options = Options}) ->
+ case item_expire(Host, Options) of
+ infinity ->
+ ok;
+ Seconds ->
+ case node_action(
+ Host, Type,
+ remove_expired_items,
+ [Nidx, Seconds]) of
+ {result, []} ->
+ ok;
+ {result, [_|_]} ->
+ unset_cached_item(
+ Host, Nidx);
+ {error, _} ->
+ error
+ end
+ end
+ end, Nodes);
+ _ ->
+ error
+ end
+ end, ejabberd_option:hosts()),
+ case lists:member(error, Results) of
+ true ->
+ error;
+ false ->
+ ok
+ end.
+
-spec get_commands_spec() -> [ejabberd_commands()].
get_commands_spec() ->
[#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge],
desc = "Keep only NUMBER of PubSub items per node",
+ note = "added in 21.12",
module = ?MODULE, function = delete_old_items,
args_desc = ["Number of items to keep per node"],
args = [{number, integer}],
result = {res, rescode},
result_desc = "0 if command failed, 1 when succeeded",
args_example = [1000],
+ result_example = ok},
+ #ejabberd_commands{name = delete_expired_pubsub_items, tags = [purge],
+ desc = "Delete expired PubSub items",
+ note = "added in 21.12",
+ module = ?MODULE, function = delete_expired_items,
+ args = [],
+ result = {res, rescode},
+ result_desc = "0 if command failed, 1 when succeeded",
result_example = ok}].
-spec mod_opt_type(atom()) -> econf:validator().
@@ -4204,6 +4270,8 @@ mod_opt_type(last_item_cache) ->
econf:bool();
mod_opt_type(max_items_node) ->
econf:non_neg_int(unlimited);
+mod_opt_type(max_item_expire_node) ->
+ econf:timeout(second, infinity);
mod_opt_type(max_nodes_discoitems) ->
econf:non_neg_int(infinity);
mod_opt_type(max_subscriptions_node) ->
@@ -4251,6 +4319,7 @@ mod_options(Host) ->
{ignore_pep_from_offline, true},
{last_item_cache, false},
{max_items_node, ?MAXITEMS},
+ {max_item_expire_node, infinity},
{max_nodes_discoitems, 100},
{nodetree, ?STDTREE},
{pep_mapping, []},
@@ -4329,11 +4398,17 @@ mod_doc() ->
" so many nodes, caching last items speeds up pubsub "
"and allows to raise user connection rate. The cost "
"is memory usage, as every item is stored in memory.")}},
+ {max_item_expire_node,
+ #{value => "timeout() | infinity",
+ note => "added in 21.12",
+ desc =>
+ ?T("Specify the maximum item epiry time. Default value "
+ "is: 'infinity'.")}},
{max_items_node,
#{value => "non_neg_integer() | infinity",
desc =>
?T("Define the maximum number of items that can be "
- "stored in a node. Default value is: '10'.")}},
+ "stored in a node. Default value is: '1000'.")}},
{max_nodes_discoitems,
#{value => "pos_integer() | infinity",
desc =>
diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl
index 8db5532f6..cb3c014b9 100644
--- a/src/mod_pubsub_opt.erl
+++ b/src/mod_pubsub_opt.erl
@@ -11,6 +11,7 @@
-export([hosts/1]).
-export([ignore_pep_from_offline/1]).
-export([last_item_cache/1]).
+-export([max_item_expire_node/1]).
-export([max_items_node/1]).
-export([max_nodes_discoitems/1]).
-export([max_subscriptions_node/1]).
@@ -68,7 +69,13 @@ last_item_cache(Opts) when is_map(Opts) ->
last_item_cache(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache).
--spec max_items_node(gen_mod:opts() | global | binary()) -> non_neg_integer().
+-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
+max_item_expire_node(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(max_item_expire_node, Opts);
+max_item_expire_node(Host) ->
+ gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node).
+
+-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer().
max_items_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_items_node, Opts);
max_items_node(Host) ->
diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl
index f89b904c2..c024a12d1 100644
--- a/src/mod_push_sql.erl
+++ b/src/mod_push_sql.erl
@@ -52,7 +52,7 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) ->
case ?SQL_UPSERT(LServer, "push_session",
["!username=%(LUser)s",
"!server_host=%(LServer)s",
- "!timestamp=%(TS)d",
+ "timestamp=%(TS)d",
"!service=%(Service)s",
"!node=%(Node)s",
"xml=%(XML)s"]) of
diff --git a/src/mod_register.erl b/src/mod_register.erl
index 379318da6..b85efd57c 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -32,11 +32,13 @@
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, stream_feature_register/2,
- c2s_unauthenticated_packet/2, try_register/4,
+ c2s_unauthenticated_packet/2, try_register/4, try_register/5,
process_iq/1, send_registration_notifications/3,
mod_opt_type/1, mod_options/1, depends/2,
format_error/1, mod_doc/0]).
+-deprecated({try_register, 4}).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
@@ -283,7 +285,7 @@ try_register_or_set_password(User, Server, Password,
_ when CaptchaSucceed ->
case check_from(From, Server) of
allow ->
- case try_register(User, Server, Password, Source, Lang) of
+ case try_register(User, Server, Password, Source, ?MODULE, Lang) of
ok ->
xmpp:make_iq_result(IQ);
{error, Error} ->
@@ -328,6 +330,13 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) ->
xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))
end.
+try_register(User, Server, Password, SourceRaw, Module) ->
+ Modules = mod_register_opt:allow_modules(Server),
+ case (Modules == all) orelse lists:member(Module, Modules) of
+ true -> try_register(User, Server, Password, SourceRaw);
+ false -> {error, eaccess}
+ end.
+
try_register(User, Server, Password, SourceRaw) ->
case jid:is_nodename(User) of
false ->
@@ -363,8 +372,8 @@ try_register(User, Server, Password, SourceRaw) ->
end
end.
-try_register(User, Server, Password, SourceRaw, Lang) ->
- case try_register(User, Server, Password, SourceRaw) of
+try_register(User, Server, Password, SourceRaw, Module, Lang) ->
+ case try_register(User, Server, Password, SourceRaw, Module) of
ok ->
JID = jid:make(User, Server),
Source = may_remove_resource(SourceRaw),
@@ -597,6 +606,8 @@ mod_opt_type(access_from) ->
econf:acl();
mod_opt_type(access_remove) ->
econf:acl();
+mod_opt_type(allow_modules) ->
+ econf:either(all, econf:list(econf:atom()));
mod_opt_type(captcha_protected) ->
econf:bool();
mod_opt_type(ip_access) ->
@@ -623,6 +634,7 @@ mod_options(_Host) ->
[{access, all},
{access_from, none},
{access_remove, all},
+ {allow_modules, all},
{captcha_protected, false},
{ip_access, all},
{password_strength, 0},
@@ -661,6 +673,13 @@ mod_doc() ->
desc =>
?T("Specify rules to restrict access for user unregistration. "
"By default any user is able to unregister their account.")}},
+ {allow_modules,
+ #{value => "all | [Module, ...]",
+ note => "added in 21.12",
+ desc =>
+ ?T("List of modules that can register accounts, or 'all'. "
+ "The default value is 'all', which is equivalent to "
+ "something like '[mod_register, mod_register_web]'.")}},
{captcha_protected,
#{value => "true | false",
desc =>
diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl
index 53c6ca6ea..e7236424c 100644
--- a/src/mod_register_opt.erl
+++ b/src/mod_register_opt.erl
@@ -6,6 +6,7 @@
-export([access/1]).
-export([access_from/1]).
-export([access_remove/1]).
+-export([allow_modules/1]).
-export([captcha_protected/1]).
-export([ip_access/1]).
-export([password_strength/1]).
@@ -31,6 +32,12 @@ access_remove(Opts) when is_map(Opts) ->
access_remove(Host) ->
gen_mod:get_module_opt(Host, mod_register, access_remove).
+-spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()].
+allow_modules(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(allow_modules, Opts);
+allow_modules(Host) ->
+ gen_mod:get_module_opt(Host, mod_register, allow_modules).
+
-spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean().
captcha_protected(Opts) when is_map(Opts) ->
gen_mod:get_opt(captcha_protected, Opts);
diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl
index 0e216c81c..0cf4bcff8 100644
--- a/src/mod_register_web.erl
+++ b/src/mod_register_web.erl
@@ -85,7 +85,7 @@ process([Section],
process([<<"new">>],
#request{method = 'POST', q = Q, ip = {Ip, _Port},
lang = Lang, host = _HTTPHost}) ->
- case form_new_post(Q) of
+ case form_new_post(Q, Ip) of
{success, ok, {Username, Host, _Password}} ->
Jid = jid:make(Username, Host),
mod_register:send_registration_notifications(?MODULE, Jid, Ip),
@@ -290,10 +290,10 @@ form_new_get2(Host, Lang, CaptchaEls) ->
%%% Formulary new POST
%%%----------------------------------------------------------------------
-form_new_post(Q) ->
+form_new_post(Q, Ip) ->
case catch get_register_parameters(Q) of
[Username, Host, Password, Password, Id, Key] ->
- form_new_post(Username, Host, Password, {Id, Key});
+ form_new_post(Username, Host, Password, {Id, Key}, Ip);
[_Username, _Host, _Password, _Password2, false, false] ->
{error, passwords_not_identical};
[_Username, _Host, _Password, _Password2, Id, Key] ->
@@ -312,13 +312,12 @@ get_register_parameters(Q) ->
[<<"username">>, <<"host">>, <<"password">>, <<"password2">>,
<<"id">>, <<"key">>]).
-form_new_post(Username, Host, Password,
- {false, false}) ->
- register_account(Username, Host, Password);
-form_new_post(Username, Host, Password, {Id, Key}) ->
+form_new_post(Username, Host, Password, {false, false}, Ip) ->
+ register_account(Username, Host, Password, Ip);
+form_new_post(Username, Host, Password, {Id, Key}, Ip) ->
case ejabberd_captcha:check_captcha(Id, Key) of
captcha_valid ->
- register_account(Username, Host, Password);
+ register_account(Username, Host, Password, Ip);
captcha_non_valid -> {error, captcha_non_valid};
captcha_not_found -> {error, captcha_non_valid}
end.
@@ -502,11 +501,11 @@ form_del_get(Host, Lang) ->
{<<"Content-Type">>, <<"text/html">>}],
ejabberd_web:make_xhtml(HeadEls, Els)}.
-%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} |
+%% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} |
%% {success, exists, {Username, Host, Password}} |
%% {error, not_allowed} |
%% {error, invalid_jid}
-register_account(Username, Host, Password) ->
+register_account(Username, Host, Password, Ip) ->
try mod_register_opt:access(Host) of
Access ->
case jid:make(Username, Host) of
@@ -514,16 +513,15 @@ register_account(Username, Host, Password) ->
JID ->
case acl:match_rule(Host, Access, JID) of
deny -> {error, not_allowed};
- allow -> register_account2(Username, Host, Password)
+ allow -> register_account2(Username, Host, Password, Ip)
end
end
catch _:{module_not_loaded, mod_register, _Host} ->
{error, host_unknown}
end.
-register_account2(Username, Host, Password) ->
- case ejabberd_auth:try_register(Username, Host,
- Password)
+register_account2(Username, Host, Password, Ip) ->
+ case mod_register:try_register(Username, Host, Password, Ip, ?MODULE)
of
ok ->
{success, ok, {Username, Host, Password}};
@@ -579,12 +577,8 @@ get_error_text({error, exists}) ->
?T("The account already exists");
get_error_text({error, password_incorrect}) ->
?T("Incorrect password");
-get_error_text({error, invalid_jid}) ->
- ?T("The username is not valid");
get_error_text({error, host_unknown}) ->
?T("Host unknown");
-get_error_text({error, not_allowed}) ->
- ?T("Not allowed");
get_error_text({error, account_doesnt_exist}) ->
?T("Account doesn't exist");
get_error_text({error, account_exists}) ->
@@ -594,7 +588,9 @@ get_error_text({error, password_not_changed}) ->
get_error_text({error, passwords_not_identical}) ->
?T("The passwords are different");
get_error_text({error, wrong_parameters}) ->
- ?T("Wrong parameters in the web formulary").
+ ?T("Wrong parameters in the web formulary");
+get_error_text({error, Why}) ->
+ mod_register:format_error(Why).
mod_options(_) ->
[].
diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl
index 76ddb29dd..ebfcde463 100644
--- a/src/mod_roster_sql.erl
+++ b/src/mod_roster_sql.erl
@@ -80,9 +80,10 @@ get_roster(LUser, LServer) ->
[]
end,
GroupsDict = lists:foldl(fun({J, G}, Acc) ->
- dict:append(J, G, Acc)
+ Gs = maps:get(J, Acc, []),
+ maps:put(J, [G | Gs], Acc)
end,
- dict:new(), JIDGroups),
+ maps:new(), JIDGroups),
{ok, lists:flatmap(
fun(I) ->
case raw_to_record(LServer, I) of
@@ -90,10 +91,7 @@ get_roster(LUser, LServer) ->
error -> [];
R ->
SJID = jid:encode(R#roster.jid),
- Groups = case dict:find(SJID, GroupsDict) of
- {ok, Gs} -> Gs;
- error -> []
- end,
+ Groups = maps:get(SJID, GroupsDict, []),
[R#roster{groups = Groups}]
end
end, Items)};
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 13ff90466..358a8df32 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -870,12 +870,15 @@ c2s_self_presence(Acc) ->
Acc.
-spec unset_presence(binary(), binary(), binary(), binary()) -> ok.
-unset_presence(LUser, LServer, Resource, Status) ->
+unset_presence(User, Server, Resource, Status) ->
+ LUser = jid:nodeprep(User),
+ LServer = jid:nameprep(Server),
+ LResource = jid:resourceprep(Resource),
Resources = ejabberd_sm:get_user_resources(LUser,
LServer),
?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p "
"(~p resources)",
- [LUser, LServer, Resource, Status, length(Resources)]),
+ [LUser, LServer, LResource, Status, length(Resources)]),
case length(Resources) of
0 ->
lists:foreach(
diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl
index 08fbe8793..e842ab261 100644
--- a/src/mod_shared_roster_ldap.erl
+++ b/src/mod_shared_roster_ldap.erl
@@ -689,9 +689,9 @@ mod_doc() ->
?T("- Connection parameters: The module also accepts the "
"connection parameters, all of which default to the top-level "
"parameter of the same name, if unspecified. "
- "See http://../database-ldap/#ldap-connection[LDAP Connection] "
+ "See http://../ldap/#ldap-connection[LDAP Connection] "
"section for more information about them."), "",
- ?T("Check also the http://../database-ldap/#configuration-examples"
+ ?T("Check also the http://../ldap/#ldap-examples"
"[Configuration examples] section to get details about "
"retrieving the roster, "
"and configuration examples including Flat DIT and Deep DIT.")],
@@ -721,13 +721,13 @@ mod_doc() ->
"name of roster entries (usually full names of people in "
"the roster). See also the parameters 'ldap_userdesc' and "
"'ldap_useruid'. For more information check the LDAP "
- "http://../database-ldap/#filters[Filters] section.")}},
+ "http://../ldap/#filters[Filters] section.")}},
{ldap_filter,
#{desc =>
?T("Additional filter which is AND-ed together "
"with \"User Filter\" and \"Group Filter\". "
"For more information check the LDAP "
- "http://../database-ldap/#filters[Filters] section.")}},
+ "http://../ldap/#filters[Filters] section.")}},
%% Attributes:
{ldap_groupattr,
#{desc =>
@@ -785,7 +785,7 @@ mod_doc() ->
#{desc =>
?T("A regex for extracting user ID from the value of the "
"attribute named by 'ldap_memberattr'. Check the LDAP "
- "http://../database-ldap/#control-parameters"
+ "http://../ldap/#control-parameters"
"[Control Parameters] section.")}},
{ldap_auth_check,
#{value => "true | false",
diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl
index 6e7592453..cbb671639 100644
--- a/src/mod_stun_disco.erl
+++ b/src/mod_stun_disco.erl
@@ -646,7 +646,7 @@ get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) ->
{undefined, get_turn_ipv6_addr(Opts)};
get_listener_ips(#{ip := {_, _, _, _} = IP}) ->
{IP, undefined};
-get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) ->
+get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) ->
{undefined, IP}.
-spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined.
diff --git a/src/node_flat.erl b/src/node_flat.erl
index c597b9ce9..55dea0d8d 100644
--- a/src/node_flat.erl
+++ b/src/node_flat.erl
@@ -40,7 +40,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -432,6 +432,22 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx),
+ ExpT = misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000)),
+ ExpItems = lists:filtermap(
+ fun(#pubsub_item{itemid = {ItemId, _},
+ modification = {ModT, _}}) when ModT < ExpT ->
+ {true, ItemId};
+ (#pubsub_item{}) ->
+ false
+ end, Items),
+ del_items(Nidx, ExpItems),
+ {result, ExpItems}.
+
%% @doc Triggers item deletion.
%% Default plugin: The user performing the deletion must be the node owner
%% or a publisher, or PublishModel being open.
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index 240dc3760..f9c8a209d 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -43,7 +43,7 @@
create_node_permission/6, create_node/2, delete_node/1, purge_node/2,
subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -285,6 +285,23 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ ExpT = encode_now(
+ misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000))),
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d "
+ "and creation < %(ExpT)s")) of
+ {selected, RItems} ->
+ ItemIds = [ItemId || {ItemId} <- RItems],
+ del_items(Nidx, ItemIds),
+ {result, ItemIds};
+ _ ->
+ {result, []}
+ end.
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
SubKey = jid:tolower(Publisher),
GenKey = jid:remove_resource(SubKey),
diff --git a/src/node_pep.erl b/src/node_pep.erl
index 44388ca31..66431b948 100644
--- a/src/node_pep.erl
+++ b/src/node_pep.erl
@@ -36,7 +36,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -81,10 +81,12 @@ features() ->
[<<"create-nodes">>,
<<"auto-create">>,
<<"auto-subscribe">>,
+ <<"config-node">>,
<<"delete-nodes">>,
<<"delete-items">>,
<<"filtered-notifications">>,
<<"modify-affiliations">>,
+ <<"multi-items">>,
<<"outcast-affiliation">>,
<<"persistent-items">>,
<<"publish">>,
@@ -142,6 +144,9 @@ remove_extra_items(Nidx, MaxItems) ->
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl
index c0cf2b166..3bb66bc4c 100644
--- a/src/node_pep_sql.erl
+++ b/src/node_pep_sql.erl
@@ -38,7 +38,7 @@
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
publish_item/7, delete_item/4,
- remove_extra_items/2, remove_extra_items/3,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -99,6 +99,9 @@ remove_extra_items(Nidx, MaxItems) ->
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat_sql:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl
index 3992a4034..8f5c35f84 100644
--- a/src/prosody2ejabberd.erl
+++ b/src/prosody2ejabberd.erl
@@ -118,7 +118,7 @@ eval_file(Path) ->
case luerl:eval(NewData, State1) of
{ok, _} = Res ->
Res;
- {error, Why} = Err ->
+ {error, Why, _} = Err ->
?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]),
Err
end;
diff --git a/src/rest.erl b/src/rest.erl
index d724352f2..1bb5c5ef7 100644
--- a/src/rest.erl
+++ b/src/rest.erl
@@ -191,13 +191,26 @@ base_url(Server, Path) ->
_ -> Url
end.
+-ifdef(HAVE_URI_STRING).
+uri_hack(Str) ->
+ case uri_string:normalize("%25") of
+ "%" -> % This hack around bug in httpc >21 <23.2
+ binary:replace(Str, <<"%25">>, <<"%2525">>, [global]);
+ _ -> Str
+ end.
+-else.
+uri_hack(Str) ->
+ Str.
+-endif.
+
url(Url, []) ->
Url;
url(Url, Params) ->
L = [<<"&", (iolist_to_binary(Key))/binary, "=",
(misc:url_encode(Value))/binary>>
|| {Key, Value} <- Params],
- <<$&, Encoded/binary>> = iolist_to_binary(L),
+ <<$&, Encoded0/binary>> = iolist_to_binary(L),
+ Encoded = uri_hack(Encoded0),
<>.
url(Server, Path, Params) ->
case binary:split(base_url(Server, Path), <<"?">>) of
diff --git a/test/roster_tests.erl b/test/roster_tests.erl
index a3b6009c9..3092b8cd8 100644
--- a/test/roster_tests.erl
+++ b/test/roster_tests.erl
@@ -224,13 +224,21 @@ get_items(Config, Version) ->
sub_els = [#roster_query{ver = Version}]}) of
#iq{type = result,
sub_els = [#roster_query{ver = NewVersion, items = Items}]} ->
- {NewVersion, Items};
+ {NewVersion, normalize_items(Items)};
#iq{type = result, sub_els = []} ->
{empty, []};
#iq{type = error} = Err ->
xmpp:get_error(Err)
end.
+normalize_items(Items) ->
+ Items2 =
+ lists:map(
+ fun(I) ->
+ I#roster_item{groups = lists:sort(I#roster_item.groups)}
+ end, Items),
+ lists:sort(Items2).
+
get_item(Config, JID) ->
case get_items(Config) of
{_Ver, Items} when is_list(Items) ->
diff --git a/tools/captcha.sh b/tools/captcha.sh
index 9fa4a52c4..7885858a2 100755
--- a/tools/captcha.sh
+++ b/tools/captcha.sh
@@ -15,69 +15,62 @@
INPUT=$1
-if test -n ${BASH_VERSION:-''} ; then
- get_random ()
- {
- R=$RANDOM
- }
-else
- for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done
- get_random ()
- {
- R=${RL%% *}
- RL=${RL#* }
- }
-fi
+for n in $(od -A n -t u2 -N 48 /dev/urandom); do RL="$RL$n "; done
+get_random ()
+{
+ R=${RL%% *}
+ RL=${RL#* }
+}
get_random
-WAVE1_AMPLITUDE=$((2 + $R % 5))
+WAVE1_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE1_LENGTH=$((50 + $R % 25))
+WAVE1_LENGTH=$((50 + R % 25))
get_random
-WAVE2_AMPLITUDE=$((2 + $R % 5))
+WAVE2_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE2_LENGTH=$((50 + $R % 25))
+WAVE2_LENGTH=$((50 + R % 25))
get_random
-WAVE3_AMPLITUDE=$((2 + $R % 5))
+WAVE3_AMPLITUDE=$((2 + R % 5))
get_random
-WAVE3_LENGTH=$((50 + $R % 25))
+WAVE3_LENGTH=$((50 + R % 25))
get_random
-W1_LINE_START_Y=$((10 + $R % 40))
+W1_LINE_START_Y=$((10 + R % 40))
get_random
-W1_LINE_STOP_Y=$((10 + $R % 40))
+W1_LINE_STOP_Y=$((10 + R % 40))
get_random
-W2_LINE_START_Y=$((10 + $R % 40))
+W2_LINE_START_Y=$((10 + R % 40))
get_random
-W2_LINE_STOP_Y=$((10 + $R % 40))
+W2_LINE_STOP_Y=$((10 + R % 40))
get_random
-W3_LINE_START_Y=$((10 + $R % 40))
+W3_LINE_START_Y=$((10 + R % 40))
get_random
-W3_LINE_STOP_Y=$((10 + $R % 40))
+W3_LINE_STOP_Y=$((10 + R % 40))
get_random
-B1_LINE_START_Y=$(($R % 40))
+B1_LINE_START_Y=$((R % 40))
get_random
-B1_LINE_STOP_Y=$(($R % 40))
+B1_LINE_STOP_Y=$((R % 40))
get_random
-B2_LINE_START_Y=$(($R % 40))
+B2_LINE_START_Y=$((R % 40))
get_random
-B2_LINE_STOP_Y=$(($R % 40))
-#B3_LINE_START_Y=$(($R % 40))
-#B3_LINE_STOP_Y=$(($R % 40))
+B2_LINE_STOP_Y=$((R % 40))
+#B3_LINE_START_Y=$((R % 40))
+#B3_LINE_STOP_Y=$((R % 40))
get_random
-B1_LINE_START_X=$(($R % 20))
+B1_LINE_START_X=$((R % 20))
get_random
-B1_LINE_STOP_X=$((100 + $R % 40))
+B1_LINE_STOP_X=$((100 + R % 40))
get_random
-B2_LINE_START_X=$(($R % 20))
+B2_LINE_START_X=$((R % 20))
get_random
-B2_LINE_STOP_X=$((100 + $R % 40))
-#B3_LINE_START_X=$(($R % 20))
-#B3_LINE_STOP_X=$((100 + $R % 40))
+B2_LINE_STOP_X=$((100 + R % 40))
+#B3_LINE_START_X=$((R % 20))
+#B3_LINE_STOP_X=$((100 + R % 40))
get_random
-ROLL_X=$(($R % 40))
+ROLL_X=$((R % 40))
convert -size 180x60 xc:none -pointsize 40 \
\( -clone 0 -fill white \
diff --git a/vars.config.in b/vars.config.in
index 9b3ac7585..04024fd73 100644
--- a/vars.config.in
+++ b/vars.config.in
@@ -51,9 +51,10 @@
{release, true}.
{release_dir, "${SCRIPT_DIR%/*}"}.
{sysconfdir, "{{release_dir}}/etc"}.
+{erts_dir, "{{release_dir}}/erts-${ERTS_VSN#erts-}"}.
{installuser, "@INSTALLUSER@"}.
-{erl, "{{release_dir}}/{{erts_vsn}}/bin/erl"}.
-{epmd, "{{release_dir}}/{{erts_vsn}}/bin/epmd"}.
+{erl, "{{erts_dir}}/bin/erl"}.
+{epmd, "{{erts_dir}}/bin/epmd"}.
{localstatedir, "{{release_dir}}/var"}.
{libdir, "{{release_dir}}/lib"}.
{docdir, "{{release_dir}}/doc"}.