'")],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /api: mod_http_api",
+ "",
+ "modules:",
+ " mod_http_api: {}"]}.
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
index 0cc8d8488..1ff94eb4a 100644
--- a/src/mod_http_upload.erl
+++ b/src/mod_http_upload.erl
@@ -232,8 +232,9 @@ mod_doc() ->
"[XEP-0363: HTTP File Upload]. If the request is accepted, "
"the client receives a URL for uploading the file and "
"another URL from which that file can later be downloaded."), "",
- ?T("In order to use this module, it must be configured as "
- "a 'request_handler' for 'ejabberd_http' listener.")],
+ ?T("In order to use this module, it must be enabled "
+ "in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers].")],
opts =>
[{host,
#{desc => ?T("Deprecated. Use 'hosts' instead.")}},
@@ -320,17 +321,18 @@ mod_doc() ->
"used for file uploads. The keyword @HOST@ is replaced "
"with the virtual host name. NOTE: different virtual "
"hosts cannot use the same PUT URL. "
- "The default value is \"https://@HOST@:5443\".")}},
+ "The default value is \"https://@HOST@:5443/upload\".")}},
{get_url,
#{value => ?T("URL"),
desc =>
?T("This option specifies the initial part of the GET URLs "
- "used for downloading the files. By default, it is set "
+ "used for downloading the files. The default value is 'undefined'. "
+ "When this option is 'undefined', this option is set "
"to the same value as 'put_url'. The keyword @HOST@ is "
"replaced with the virtual host name. NOTE: if GET requests "
"are handled by 'mod_http_upload', the 'get_url' must match the "
"'put_url'. Setting it to a different value only makes "
- "sense if an external web server or 'mod_http_fileserver' "
+ "sense if an external web server or _`mod_http_fileserver`_ "
"is used to serve the uploaded files.")}},
{service_url,
#{desc => ?T("Deprecated.")}},
diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl
index 48b7b1958..5ed7fcefb 100644
--- a/src/mod_http_upload_quota.erl
+++ b/src/mod_http_upload_quota.erl
@@ -27,7 +27,6 @@
-author('holger@zedat.fu-berlin.de').
-define(TIMEOUT, timer:hours(24)).
--define(INITIAL_TIMEOUT, timer:minutes(10)).
-define(FORMAT(Error), file:format_error(Error)).
-behaviour(gen_server).
@@ -64,7 +63,7 @@
max_days :: pos_integer() | infinity,
docroot :: binary(),
disk_usage = #{} :: disk_usage(),
- timers :: [timer:tref()]}).
+ timer :: reference() | undefined}).
-type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}.
-type state() :: #state{}.
@@ -166,12 +165,11 @@ init([ServerHost|_]) ->
DocRoot1 = mod_http_upload_opt:docroot(ServerHost),
DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)),
DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost),
- Timers = if MaxDays == infinity -> [];
- true ->
- {ok, T1} = timer:send_after(?INITIAL_TIMEOUT, sweep),
- {ok, T2} = timer:send_interval(?TIMEOUT, sweep),
- [T1, T2]
- end,
+ Timer = if MaxDays == infinity -> undefined;
+ true ->
+ Timeout = p1_rand:uniform(?TIMEOUT div 2),
+ erlang:send_after(Timeout, self(), sweep)
+ end,
ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
{ok, #state{server_host = ServerHost,
@@ -179,7 +177,7 @@ init([ServerHost|_]) ->
access_hard_quota = AccessHardQuota,
max_days = MaxDays,
docroot = DocRoot3,
- timers = Timers}}.
+ timer = Timer}}.
-spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}.
handle_call(Request, From, State) ->
@@ -249,6 +247,7 @@ handle_info(sweep, #state{server_host = ServerHost,
max_days = MaxDays} = State)
when is_integer(MaxDays), MaxDays > 0 ->
?DEBUG("Got 'sweep' message for ~ts", [ServerHost]),
+ Timer = erlang:send_after(?TIMEOUT, self(), sweep),
case file:list_dir(DocRoot) of
{ok, Entries} ->
BackThen = secs_since_epoch() - (MaxDays * 86400),
@@ -264,17 +263,17 @@ handle_info(sweep, #state{server_host = ServerHost,
?ERROR_MSG("Cannot open document root ~ts: ~ts",
[DocRoot, ?FORMAT(Error)])
end,
- {noreply, State};
+ {noreply, State#state{timer = Timer}};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok.
-terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
+terminate(Reason, #state{server_host = ServerHost, timer = Timer}) ->
?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
- lists:foreach(fun timer:cancel/1, Timers).
+ misc:cancel_timer(Timer).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) ->
diff --git a/src/mod_last.erl b/src/mod_last.erl
index 295a546f2..a7d36c791 100644
--- a/src/mod_last.erl
+++ b/src/mod_last.erl
@@ -344,20 +344,20 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 12542bfa5..abb2333cc 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -148,7 +148,7 @@ start(Host, Opts) ->
ejabberd_hooks:add(check_create_room, Host, ?MODULE,
check_create_room, 50)
end,
- ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
ok;
Err ->
Err
@@ -1456,7 +1456,7 @@ mod_doc() ->
#{value => "true | false",
desc =>
?T("This option determines how ejabberd's "
- "stream management code (see 'mod_stream_mgmt') "
+ "stream management code (see _`mod_stream_mgmt`_) "
"handles unacknowledged messages when the "
"connection is lost. Usually, such messages are "
"either bounced or resent. However, neither is "
@@ -1495,28 +1495,28 @@ mod_doc() ->
#{value => "true | false",
desc =>
?T("Whether to destroy message archive of a room "
- "(see 'mod_muc') when it gets destroyed. "
+ "(see _`mod_muc`_) when it gets destroyed. "
"The default value is 'true'.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
{user_mucsub_from_muc_archive,
#{value => "true | false",
desc =>
diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl
index e5069b9a2..269b4c963 100644
--- a/src/mod_mam_sql.erl
+++ b/src/mod_mam_sql.erl
@@ -72,7 +72,7 @@ remove_from_archive(LUser, LServer, WithJid) ->
end.
delete_old_messages(ServerHost, TimeStamp, Type) ->
- TS = now_to_usec(TimeStamp),
+ TS = misc:now_to_usec(TimeStamp),
case Type of
all ->
ejabberd_sql:sql_query(
@@ -315,7 +315,7 @@ export(_Server) ->
id = _ID, timestamp = TS, peer = Peer,
type = Type, nick = Nick, packet = Pkt})
when LServer == Host ->
- TStmp = now_to_usec(TS),
+ TStmp = misc:now_to_usec(TS),
SUser = case Type of
chat -> LUser;
groupchat -> jid:encode({LUser, LServer, <<>>})
@@ -372,16 +372,6 @@ is_empty_for_room(LServer, LName, LHost) ->
%%%===================================================================
%%% Internal functions
%%%===================================================================
-now_to_usec({MSec, Sec, USec}) ->
- (MSec*1000000 + Sec)*1000000 + USec.
-
-usec_to_now(Int) ->
- Secs = Int div 1000000,
- USec = Int rem 1000000,
- MSec = Secs div 1000000,
- Sec = Secs rem 1000000,
- {MSec, Sec, USec}.
-
make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
Start = proplists:get_value(start, MAMQuery),
End = proplists:get_value('end', MAMQuery),
@@ -432,14 +422,14 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
StartClause = case Start of
{_, _, _} ->
[<<" and timestamp >= ">>,
- integer_to_binary(now_to_usec(Start))];
+ integer_to_binary(misc:now_to_usec(Start))];
_ ->
[]
end,
EndClause = case End of
{_, _, _} ->
[<<" and timestamp <= ">>,
- integer_to_binary(now_to_usec(End))];
+ integer_to_binary(misc:now_to_usec(End))];
_ ->
[]
end,
@@ -526,7 +516,7 @@ make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchi
TSInt ->
try jid:decode(Peer) of
PeerJID ->
- Now = usec_to_now(TSInt),
+ Now = misc:usec_to_now(TSInt),
PeerLJID = jid:tolower(PeerJID),
T = case Kind of
<<"">> -> chat;
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
index 1c43bc8a7..002ef5696 100644
--- a/src/mod_mix.erl
+++ b/src/mod_mix.erl
@@ -24,7 +24,7 @@
-module(mod_mix).
-behaviour(gen_mod).
-behaviour(gen_server).
--protocol({xep, 369, '0.13.0'}).
+-protocol({xep, 369, '0.14.1'}).
%% API
-export([route/1]).
@@ -106,12 +106,12 @@ mod_doc() ->
"experimental feature, updated in 19.02, and is not "
"yet ready to use in production. It's asserted that "
"the MIX protocol is going to replace the MUC protocol "
- "in the future (see 'mod_muc')."), "",
+ "in the future (see _`mod_muc`_)."), "",
?T("To learn more about how to use that feature, you can refer to "
"our tutorial: https://docs.ejabberd.im/tutorials/mix-010/"
"[Getting started with XEP-0369: Mediated Information "
"eXchange (MIX) v0.1]."), "",
- ?T("The module depends on 'mod_mam'.")],
+ ?T("The module depends on _`mod_mam`_.")],
opts =>
[{access_create,
#{value => ?T("AccessName"),
@@ -136,7 +136,7 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}.
-spec route(stanza()) -> ok.
route(#iq{} = IQ) ->
@@ -166,7 +166,7 @@ process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To,
[ServerHost, ?MODULE, <<"">>, Lang]),
Name = mod_mix_opt:name(ServerHost),
Identity = #identity{category = <<"conference">>,
- type = <<"text">>,
+ type = <<"mix">>,
name = translate:translate(Lang, Name)},
Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_MIX_CORE_0, ?NS_MIX_CORE_SEARCHABLE_0,
diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl
index c6348b92f..1fa5c1861 100644
--- a/src/mod_mix_pam.erl
+++ b/src/mod_mix_pam.erl
@@ -120,23 +120,23 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
-spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}.
bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To,
diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl
index 24d033892..5d00408df 100644
--- a/src/mod_mqtt.erl
+++ b/src/mod_mqtt.erl
@@ -278,8 +278,9 @@ listen_options() ->
%%%===================================================================
mod_doc() ->
#{desc =>
- ?T("This module adds support for the MQTT protocol "
- "version '3.1.1' and '5.0'. Remember to configure "
+ ?T("This module adds "
+ "https://docs.ejabberd.im/admin/guide/mqtt/[support for the MQTT] "
+ "protocol version '3.1.1' and '5.0'. Remember to configure "
"'mod_mqtt' in 'modules' and 'listen' sections."),
opts =>
[{access_subscribe,
@@ -326,37 +327,37 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, "
+ ?T("Same as top-level _`queue_type`_ option, "
"but applied to this module only.")}},
{ram_db_type,
#{value => "mnesia",
desc =>
- ?T("Same as top-level 'default_ram_db' option, "
+ ?T("Same as top-level _`default_ram_db`_ option, "
"but applied to this module only.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, "
+ ?T("Same as top-level _`default_db`_ option, "
"but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, "
+ ?T("Same as top-level _`use_cache`_ option, "
"but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, "
+ ?T("Same as top-level _`cache_size`_ option, "
"but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, "
+ ?T("Same as top-level _`cache_missed`_ option, "
"but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, "
+ ?T("Same as top-level _`cache_life_time`_ option, "
"but applied to this module only.")}}]}.
%%%===================================================================
@@ -600,6 +601,23 @@ match([H|T1], [<<"%c">>|T2], U, S, R) ->
R -> match(T1, T2, U, S, R);
_ -> false
end;
+match([H|T1], [<<"%g">>|T2], U, S, R) ->
+ case jid:resourceprep(H) of
+ H ->
+ case acl:loaded_shared_roster_module(S) of
+ undefined -> false;
+ Mod ->
+ case Mod:get_group_opts(S, H) of
+ error -> false;
+ _ ->
+ case Mod:is_user_in_group({U, S}, H, S) of
+ true -> match(T1, T2, U, S, R);
+ _ -> false
+ end
+ end
+ end;
+ _ -> false
+ end;
match([H|T1], [H|T2], U, S, R) ->
match(T1, T2, U, S, R);
match([], [], _, _, _) ->
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 0139db430..b2ebc5c61 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -40,6 +40,7 @@
room_destroyed/4,
store_room/4,
store_room/5,
+ store_changes/4,
restore_room/3,
forget_room/3,
create_room/3,
@@ -91,6 +92,7 @@
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}.
+-callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}.
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error.
-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}.
-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean().
@@ -111,7 +113,8 @@
-callback get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}.
--optional_callbacks([get_subscribed_rooms/3]).
+-optional_callbacks([get_subscribed_rooms/3,
+ store_changes/4]).
%%====================================================================
%% API
@@ -313,6 +316,11 @@ store_room(ServerHost, Host, Name, Opts, ChangesHints) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store_room(LServer, Host, Name, Opts, ChangesHints).
+store_changes(ServerHost, Host, Name, ChangesHints) ->
+ LServer = jid:nameprep(ServerHost),
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ Mod:store_changes(LServer, Host, Name, ChangesHints).
+
restore_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
@@ -570,7 +578,7 @@ unhibernate_room(ServerHost, Host, Room) ->
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
Proc = procname(ServerHost, {Room, Host}),
- case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}) of
+ case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}, 20000) of
{ok, _} = R -> R;
_ -> error
end;
@@ -1358,19 +1366,19 @@ mod_doc() ->
desc =>
?T("To configure who is allowed to create new rooms at the "
"Multi-User Chat service, this option can be used. "
- "By default any account in the local ejabberd server is "
+ "The default value is 'all', which means everyone is "
"allowed to create rooms.")}},
{access_persistent,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'persistent' room option. "
- "By default any account in the local ejabberd server is allowed to "
+ "The default value is 'all', which means everyone is allowed to "
"modify that option.")}},
{access_mam,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'mam' room option. "
- "By default any account in the local ejabberd server is allowed to "
+ "The default value is 'all', which means everyone is allowed to "
"modify that option.")}},
{access_register,
#{value => ?T("AccessName"),
@@ -1386,11 +1394,10 @@ mod_doc() ->
"store room information. The default is the storage defined "
"by the global option 'default_db', or 'mnesia' if omitted.")}},
{ram_db_type,
- #{value => "mnesia",
+ #{value => "mnesia | sql",
desc =>
?T("Define the type of volatile (in-memory) storage where the module "
- "will store room information. The only available value for this "
- "module is 'mnesia'.")}},
+ "will store room information ('muc_online_room' and 'muc_online_users').")}},
{hibernation_timeout,
#{value => "infinity | Seconds",
desc =>
@@ -1487,7 +1494,8 @@ mod_doc() ->
?T("This option defines after how many users in the room, "
"it is considered overcrowded. When a MUC room is considered "
"overcrowed, presence broadcasts are limited to reduce load, "
- "traffic and excessive presence \"storm\" received by participants.")}},
+ "traffic and excessive presence \"storm\" received by participants. "
+ "The default value is '1000'.")}},
{min_message_interval,
#{value => ?T("Number"),
desc =>
@@ -1518,7 +1526,7 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
+ ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
{regexp_room_id,
#{value => "string()",
desc =>
@@ -1635,7 +1643,7 @@ mod_doc() ->
{logging,
#{value => "true | false",
desc =>
- ?T("The public messages are logged using 'mod_muc_log'. "
+ ?T("The public messages are logged using _`mod_muc_log`_. "
"The default value is 'false'.")}},
{members_by_default,
#{value => "true | false",
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index a595d00c3..2abeee45c 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -57,7 +57,7 @@
%%----------------------------
start(Host, _Opts) ->
- ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50),
ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50),
ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50),
@@ -696,15 +696,26 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->
lists:keysort(1, DefRoomOpts)),
case mod_muc:create_room(Host, Name, RoomOpts) of
ok ->
- ok;
+ maybe_store_room(ServerHost, Host, Name, RoomOpts);
{error, _} ->
throw({error, "Unable to start room"})
end;
+ invalid_service ->
+ throw({error, "Invalid 'service'"});
_ ->
throw({error, "Room already exists"})
end
end.
+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),
+ ok;
+ false ->
+ ok
+ end.
+
%% Create the room only in the database.
%% It is required to restart the MUC service for the room to appear.
muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) ->
@@ -1155,6 +1166,7 @@ change_option(Option, Value, Config) ->
anonymous -> Config#config{anonymous = Value};
captcha_protected -> Config#config{captcha_protected = Value};
description -> Config#config{description = Value};
+ lang -> Config#config{lang = Value};
logging -> Config#config{logging = Value};
mam -> Config#config{mam = Value};
max_users -> Config#config{max_users = Value};
@@ -1167,8 +1179,10 @@ change_option(Option, Value, Config) ->
presence_broadcast -> Config#config{presence_broadcast = Value};
public -> Config#config{public = Value};
public_list -> Config#config{public_list = Value};
+ pubsub -> Config#config{pubsub = Value};
title -> Config#config{title = Value};
vcard -> Config#config{vcard = Value};
+ vcard_xupdate -> Config#config{vcard_xupdate = Value};
voice_request_min_interval -> Config#config{voice_request_min_interval = Value}
end.
@@ -1399,4 +1413,4 @@ mod_doc() ->
[?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`_.")]}.
diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl
index 53223a8eb..8bcbc8bc0 100644
--- a/src/mod_muc_log.erl
+++ b/src/mod_muc_log.erl
@@ -1021,7 +1021,7 @@ mod_doc() ->
?T("- URLs on messages and subjects are converted to hyperlinks."), "",
?T("- Timezone used on timestamps is shown on the log files."), "",
?T("- A custom link can be added on top of each page."), "",
- ?T("The module depends on 'mod_muc'.")],
+ ?T("The module depends on _`mod_muc`_.")],
opts =>
[{access_log,
#{value => ?T("AccessName"),
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 2fa08dc79..035e851fd 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -641,7 +641,7 @@ handle_event({service_message, Msg}, _StateName,
MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
send_wrapped_multiple(
StateData#state.jid,
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@@ -705,7 +705,7 @@ handle_sync_event({change_state, NewStateData}, _From,
true ->
ok;
_ ->
- erlang:put(muc_subscribers, NewStateData#state.subscribers)
+ erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers)
end,
{reply, {ok, NewStateData}, StateName, NewStateData};
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
@@ -717,8 +717,10 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
{reply, {ok, NSD}, StateName, NSD}
end;
handle_sync_event(get_subscribers, _From, StateName, StateData) ->
- JIDs = lists:map(fun jid:make/1,
- maps:keys(StateData#state.subscribers)),
+ JIDs = muc_subscribers_fold(
+ fun(_LBareJID, #subscriber{jid = JID}, Acc) ->
+ [JID | Acc]
+ end, [], StateData#state.muc_subscribers),
{reply, {ok, JIDs}, StateName, StateData};
handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
StateName, StateData) ->
@@ -762,7 +764,8 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName,
{reply, {error, get_error_text(Err)}, StateName, StateData}
end;
handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
- IsSubs = try maps:get(jid:split(From), StateData#state.subscribers) of
+ IsSubs = try muc_subscribers_get(
+ jid:split(From), StateData#state.muc_subscribers) of
#subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes}
catch _:{badkey, _} -> false
end,
@@ -899,7 +902,8 @@ terminate(Reason, _StateName,
_ -> ok
end,
tab_remove_online_user(JID, StateData)
- end, [], get_users_and_subscribers(StateData)),
+ end, [], get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),
disable_hibernate_timer(StateData),
case StateData#state.hibernate_timer of
@@ -991,7 +995,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(Node, StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@@ -1197,8 +1201,8 @@ get_participant_data(From, StateData) ->
#user{nick = FromNick, role = Role} ->
{FromNick, Role}
catch _:{badkey, _} ->
- try maps:get(jid:tolower(jid:remove_resource(From)),
- StateData#state.subscribers) of
+ try muc_subscribers_get(jid:tolower(jid:remove_resource(From)),
+ StateData#state.muc_subscribers) of
#subscriber{nick = FromNick} ->
{FromNick, none}
catch _:{badkey, _} ->
@@ -1329,7 +1333,7 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
andalso maps:size(StateData1#state.users) == 0
- andalso maps:size(StateData1#state.subscribers) == 0 of
+ andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~ts because it's temporary "
"and empty",
@@ -1342,6 +1346,17 @@ close_room_if_temporary_and_empty(StateData1) ->
-spec get_users_and_subscribers(state()) -> users().
get_users_and_subscribers(StateData) ->
+ get_users_and_subscribers_aux(
+ StateData#state.muc_subscribers#muc_subscribers.subscribers,
+ StateData).
+
+-spec get_users_and_subscribers_with_node(binary(), state()) -> users().
+get_users_and_subscribers_with_node(Node, StateData) ->
+ get_users_and_subscribers_aux(
+ muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers),
+ StateData).
+
+get_users_and_subscribers_aux(Subscribers, StateData) ->
OnlineSubscribers = maps:fold(
fun(LJID, _, Acc) ->
LBareJID = jid:remove_resource(LJID),
@@ -1365,7 +1380,7 @@ get_users_and_subscribers(StateData) ->
true ->
Acc
end
- end, StateData#state.users, StateData#state.subscribers).
+ end, StateData#state.users, Subscribers).
-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
@@ -1375,7 +1390,7 @@ is_user_online(JID, StateData) ->
-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
LJID = jid:tolower(jid:remove_resource(JID)),
- maps:is_key(LJID, StateData#state.subscribers).
+ muc_subscribers_is_key(LJID, StateData#state.muc_subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
@@ -1869,16 +1884,15 @@ set_subscriber(JID, Nick, Nodes,
#state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
BareJID = jid:remove_resource(JID),
LBareJID = jid:tolower(BareJID),
- Subscribers = maps:put(LBareJID,
- #subscriber{jid = BareJID,
- nick = Nick,
- nodes = Nodes},
- StateData#state.subscribers),
- Nicks = maps:put(Nick, [LBareJID], StateData#state.subscriber_nicks),
- NewStateData = StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks},
+ MUCSubscribers =
+ muc_subscribers_put(
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ StateData#state.muc_subscribers),
+ NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]),
- case not maps:is_key(LBareJID, StateData#state.subscribers) of
+ case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of
true ->
send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData),
ejabberd_hooks:run(muc_subscribed, ServerHost, [ServerHost, Room, Host, BareJID]);
@@ -1956,7 +1970,8 @@ add_user_presence_un(JID, Presence, StateData) ->
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
Users = case maps:get(Nick, StateData#state.nicks, []) of
- [] -> maps:get(Nick, StateData#state.subscriber_nicks, []);
+ [] -> muc_subscribers_get_by_nick(
+ Nick, StateData#state.muc_subscribers);
Us -> Us
end,
[jid:make(LJID) || LJID <- Users].
@@ -2020,10 +2035,10 @@ is_nick_change(JID, Nick, StateData) ->
nick_collision(User, Nick, StateData) ->
UserOfNick = case find_jid_by_nick(Nick, StateData) of
false ->
- try maps:get(Nick, StateData#state.subscriber_nicks) of
- [J] -> J
- catch _:{badkey, _} -> false
- end;
+ case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of
+ [J] -> J;
+ [] -> false
+ end;
J -> J
end,
(UserOfNick /= false andalso
@@ -2433,6 +2448,11 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
false -> {none, #presence{type = unavailable}}
end,
Affiliation = get_affiliation(LJID, StateData),
+ Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
+ true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
+ false -> ?NS_MUCSUB_NODES_PRESENCE
+ end,
+ Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS,
UserMap =
case is_room_overcrowded(StateData) orelse
(not (presence_broadcast_allowed(NJID, StateData) orelse
@@ -2440,7 +2460,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
#{LNJID => UserInfo};
false ->
- get_users_and_subscribers(StateData)
+ %% TODO: optimize further
+ UM1 = get_users_and_subscribers_with_node(Node1, StateData),
+ UM2 = get_users_and_subscribers_with_node(Node2, StateData),
+ maps:merge(UM1, UM2)
end,
maps:fold(
fun(LUJID, Info, _) ->
@@ -2465,10 +2488,6 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
Packet = xmpp:set_subtag(
Pres, #muc_user{items = [Item],
status_codes = StatusCodes}),
- Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
- true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
- false -> ?NS_MUCSUB_NODES_PRESENCE
- end,
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
@@ -2476,7 +2495,6 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
IsOccupant = Info#user.last_presence /= undefined,
if (IsSubscriber and not IsOccupant) and
(IsInitialPresence or (Type == unavailable)) ->
- Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS,
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node2, StateData);
true ->
@@ -2607,11 +2625,13 @@ send_nick_changing(JID, OldNick, StateData,
end;
(_, _, _) ->
ok
- end, ok, get_users_and_subscribers(StateData)).
+ end, ok, get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PRESENCE, StateData)).
-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
+ %% TODO: there should be a better way to check IsOccupant
Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
@@ -2637,7 +2657,8 @@ send_affiliation(JID, Affiliation, StateData) ->
role = none},
Message = #message{id = p1_rand:get_string(),
sub_els = [#muc_user{items = [Item]}]},
- Users = get_users_and_subscribers(StateData),
+ Users = get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
maps:filter(fun(_, #user{role = moderator}) ->
@@ -3271,6 +3292,13 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
StateData) ->
#user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users),
ActorNick = get_actor_nick(MJID, StateData),
+ %% TODO: optimize further
+ UserMap =
+ maps:merge(
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),
maps:fold(
fun(LJID, Info, _) ->
IsSelfPresence = jid:tolower(UJID) == LJID,
@@ -3304,7 +3332,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
true ->
ok
end
- end, ok, get_users_and_subscribers(StateData)).
+ end, ok, UserMap).
-spec get_actor_nick(undefined | jid(), state()) -> binary().
get_actor_nick(undefined, _StateData) ->
@@ -3720,7 +3748,8 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
id = p1_rand:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_CONFIG, StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData);
@@ -3872,26 +3901,23 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{lang = Val}};
subscribers ->
- {Subscribers, Nicks} =
- lists:foldl(
- fun({JID, Nick, Nodes}, {SubAcc, NickAcc}) ->
- BareJID = case JID of
- #jid{} -> jid:remove_resource(JID);
- _ ->
- ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]),
- jid:remove_resource(jid:make(JID))
- end,
- LBareJID = jid:tolower(BareJID),
- {maps:put(
- LBareJID,
- #subscriber{jid = BareJID,
- nick = Nick,
- nodes = Nodes},
- SubAcc),
- maps:put(Nick, [LBareJID], NickAcc)}
- end, {#{}, #{}}, Val),
- StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks};
+ MUCSubscribers =
+ lists:foldl(
+ fun({JID, Nick, Nodes}, MUCSubs) ->
+ BareJID =
+ case JID of
+ #jid{} -> jid:remove_resource(JID);
+ _ ->
+ ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]),
+ jid:remove_resource(jid:make(JID))
+ end,
+ muc_subscribers_put(
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ MUCSubs)
+ end, muc_subscribers_new(), Val),
+ StateData#state{muc_subscribers = MUCSubscribers};
affiliations ->
StateData#state{affiliations = maps:from_list(Val)};
subject ->
@@ -3926,12 +3952,12 @@ set_vcard_xupdate(State) ->
-spec make_opts(state()) -> [{atom(), any()}].
make_opts(StateData) ->
Config = StateData#state.config,
- Subscribers = maps:fold(
+ Subscribers = muc_subscribers_fold(
fun(_LJID, Sub, Acc) ->
[{Sub#subscriber.jid,
Sub#subscriber.nick,
Sub#subscriber.nodes}|Acc]
- end, [], StateData#state.subscribers),
+ end, [], StateData#state.muc_subscribers),
[?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
?MAKE_CONFIG_OPT(#config.allow_change_subj),
?MAKE_CONFIG_OPT(#config.allow_query_users),
@@ -4013,7 +4039,8 @@ destroy_room(DEl, StateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
- end, ok, get_users_and_subscribers(StateData)),
+ end, ok, get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_CONFIG, StateData)),
forget_room(StateData),
{result, undefined, stop}.
@@ -4248,30 +4275,35 @@ process_iq_mucsub(From,
sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
StateData) ->
LBareJID = jid:tolower(jid:remove_resource(From)),
- try maps:get(LBareJID, StateData#state.subscribers) of
+ try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of
#subscriber{nick = Nick1} when Nick1 /= Nick ->
Nodes = get_subscription_nodes(Packet),
- case {nick_collision(From, Nick, StateData),
- mod_muc:can_use_nick(StateData#state.server_host,
- StateData#state.host,
- From, Nick)} of
- {true, _} ->
+ case nick_collision(From, Nick, StateData) of
+ true ->
ErrText = ?T("That nickname is already in use by another occupant"),
{error, xmpp:err_conflict(ErrText, Lang)};
- {_, false} ->
- Err = case Nick of
- <<>> ->
- xmpp:err_jid_malformed(?T("Nickname can't be empty"),
- Lang);
- _ ->
- xmpp:err_conflict(?T("That nickname is registered"
- " by another person"), Lang)
- end,
- {error, Err};
- _ ->
- NewStateData = set_subscriber(From, Nick, Nodes, StateData),
- {result, subscribe_result(Packet), NewStateData}
- end;
+ false ->
+ case mod_muc:can_use_nick(StateData#state.server_host,
+ StateData#state.host,
+ From, Nick) of
+ false ->
+ Err = case Nick of
+ <<>> ->
+ xmpp:err_jid_malformed(
+ ?T("Nickname can't be empty"),
+ Lang);
+ _ ->
+ xmpp:err_conflict(
+ ?T("That nickname is registered"
+ " by another person"), Lang)
+ end,
+ {error, Err};
+ true ->
+ NewStateData =
+ set_subscriber(From, Nick, Nodes, StateData),
+ {result, subscribe_result(Packet), NewStateData}
+ end
+ end;
#subscriber{} ->
Nodes = get_subscription_nodes(Packet),
NewStateData = set_subscriber(From, Nick, Nodes, StateData),
@@ -4298,12 +4330,9 @@ process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
#state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
BareJID = jid:remove_resource(From),
LBareJID = jid:tolower(BareJID),
- try maps:get(LBareJID, StateData#state.subscribers) of
- #subscriber{nick = Nick} ->
- Nicks = maps:remove(Nick, StateData#state.subscriber_nicks),
- Subscribers = maps:remove(LBareJID, StateData#state.subscribers),
- NewStateData = StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks},
+ try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of
+ {MUCSubscribers, #subscriber{nick = Nick}} ->
+ NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
store_room(NewStateData, [{del_subscription, LBareJID}]),
send_subscriptions_change_notifications(BareJID, Nick, unsubscribe, StateData),
ejabberd_hooks:run(muc_unsubscribed, ServerHost, [ServerHost, Room, Host, BareJID]),
@@ -4326,7 +4355,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang,
true ->
ShowJid = IsModerator orelse
(StateData#state.config)#config.anonymous == false,
- Subs = maps:fold(
+ Subs = muc_subscribers_fold(
fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) ->
case ShowJid of
true ->
@@ -4334,7 +4363,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang,
_ ->
[#muc_subscription{nick = N, events = Nodes}|Acc]
end
- end, [], StateData#state.subscribers),
+ end, [], StateData#state.muc_subscribers),
{result, #muc_subscriptions{list = Subs}, StateData};
_ ->
Txt = ?T("Moderator privileges required"),
@@ -4347,8 +4376,7 @@ process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
-spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
- StateData#state{subscribers = #{},
- subscriber_nicks = #{}};
+ StateData#state{muc_subscribers = muc_subscribers_new()};
true ->
StateData
end.
@@ -4597,7 +4625,7 @@ store_room(StateData, ChangesHints) ->
true ->
ok;
_ ->
- erlang:put(muc_subscribers, StateData#state.subscribers)
+ erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers)
end,
ShouldStore = case (StateData#state.config)#config.persistent of
true ->
@@ -4611,7 +4639,15 @@ store_room(StateData, ChangesHints) ->
end
end,
if ShouldStore ->
- store_room_no_checks(StateData, ChangesHints);
+ case erlang:function_exported(Mod, store_changes, 4) of
+ true when ChangesHints /= [] ->
+ mod_muc:store_changes(
+ StateData#state.server_host,
+ StateData#state.host, StateData#state.room,
+ ChangesHints);
+ _ ->
+ store_room_no_checks(StateData, ChangesHints)
+ end;
true ->
ok
end.
@@ -4624,37 +4660,52 @@ store_room_no_checks(StateData, ChangesHints) ->
-spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok.
send_subscriptions_change_notifications(From, Nick, Type, State) ->
- maps:fold(fun(_, #subscriber{nodes = Nodes, jid = JID}, _) ->
- case lists:member(?NS_MUCSUB_NODES_SUBSCRIBERS, Nodes) of
- true ->
- ShowJid = case (State#state.config)#config.anonymous == false orelse
- get_role(JID, State) == moderator orelse
- get_default_role(get_affiliation(JID, State), State) == moderator of
- true -> true;
- _ -> false
- end,
- Payload = case {Type, ShowJid} of
- {subscribe, true} ->
- #muc_subscribe{jid = From, nick = Nick};
- {subscribe, _} ->
- #muc_subscribe{nick = Nick};
- {unsubscribe, true} ->
- #muc_unsubscribe{jid = From, nick = Nick};
- {unsubscribe, _} ->
- #muc_unsubscribe{nick = Nick}
- end,
- Packet = #message{
- sub_els = [#ps_event{
- items = #ps_items{
- node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
- items = [#ps_item{
- id = p1_rand:get_string(),
- sub_els = [Payload]}]}}]},
- ejabberd_router:route(xmpp:set_from_to(Packet, State#state.jid, JID));
- false ->
- ok
- end
- end, ok, State#state.subscribers).
+ {WJ, WN} =
+ maps:fold(
+ fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) ->
+ case (State#state.config)#config.anonymous == false orelse
+ get_role(JID, State) == moderator orelse
+ get_default_role(get_affiliation(JID, State), State) == moderator of
+ true ->
+ {[JID | WithJid], WithNick};
+ _ ->
+ {WithJid, [JID | WithNick]}
+ end
+ end, {[], []},
+ muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS,
+ State#state.muc_subscribers)),
+ if WJ /= [] ->
+ Payload1 = case Type of
+ subscribe -> #muc_subscribe{jid = From, nick = Nick};
+ _ -> #muc_unsubscribe{jid = From, nick = Nick}
+ end,
+ Packet1 = #message{
+ sub_els = [#ps_event{
+ items = #ps_items{
+ node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
+ items = [#ps_item{
+ id = p1_rand:get_string(),
+ sub_els = [Payload1]}]}}]},
+ ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
+ WJ, Packet1, true);
+ true -> ok
+ end,
+ if WN /= [] ->
+ Payload2 = case Type of
+ subscribe -> #muc_subscribe{nick = Nick};
+ _ -> #muc_unsubscribe{nick = Nick}
+ end,
+ Packet2 = #message{
+ sub_els = [#ps_event{
+ items = #ps_items{
+ node = ?NS_MUCSUB_NODES_SUBSCRIBERS,
+ items = [#ps_item{
+ id = p1_rand:get_string(),
+ sub_els = [Payload2]}]}}]},
+ ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
+ WN, Packet2, true);
+ true -> ok
+ end.
-spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok.
send_wrapped(From, To, Packet, Node, State) ->
@@ -4666,7 +4717,7 @@ send_wrapped(From, To, Packet, Node, State) ->
_ -> false
end,
if IsOffline ->
- try maps:get(LBareTo, State#state.subscribers) of
+ try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of
#subscriber{nodes = Nodes, jid = JID} ->
case lists:member(Node, Nodes) of
true ->
@@ -4713,7 +4764,7 @@ send_wrapped(From, To, Packet, Node, State) ->
ejabberd_router:route(xmpp:set_from_to(Packet, From, To))
end.
--spec wrap(jid(), jid(), stanza(), binary(), binary()) -> message().
+-spec wrap(jid(), undefined | jid(), stanza(), binary(), binary()) -> message().
wrap(From, To, Packet, Node, Id) ->
El = xmpp:set_from_to(Packet, From, To),
#message{
@@ -4727,10 +4778,155 @@ wrap(From, To, Packet, Node, Id) ->
-spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok.
send_wrapped_multiple(From, Users, Packet, Node, State) ->
+ {Dir, Wra} =
maps:fold(
- fun(_, #user{jid = To}, _) ->
- send_wrapped(From, To, Packet, Node, State)
- end, ok, Users).
+ fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) ->
+ IsOffline = LP == undefined,
+ if IsOffline ->
+ LBareTo = jid:tolower(jid:remove_resource(To)),
+ case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of
+ {ok, #subscriber{nodes = Nodes}} ->
+ case lists:member(Node, Nodes) of
+ true ->
+ {Direct, [To | Wrapped]};
+ _ ->
+ %% TODO: check that this branch is never called
+ Res
+ end;
+ _ ->
+ Res
+ end;
+ true ->
+ {[To | Direct], Wrapped}
+ end
+ end, {[],[]}, Users),
+ case Dir of
+ [] -> ok;
+ _ ->
+ case Packet of
+ #presence{type = unavailable} ->
+ case xmpp:get_subtag(Packet, #muc_user{}) of
+ #muc_user{destroy = Destroy,
+ status_codes = Codes} ->
+ case Destroy /= undefined orelse
+ (lists:member(110,Codes) andalso
+ not lists:member(303, Codes)) of
+ true ->
+ ejabberd_router_multicast:route_multicast(
+ From, State#state.server_host, Dir,
+ #presence{id = p1_rand:get_string(),
+ type = unavailable}, false);
+ false ->
+ ok
+ end;
+ _ ->
+ false
+ end;
+ _ ->
+ ok
+ end,
+ ejabberd_router_multicast:route_multicast(From, State#state.server_host,
+ Dir, Packet, false)
+ end,
+ case Wra of
+ [] -> ok;
+ _ ->
+ MamEnabled = (State#state.config)#config.mam,
+ Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of
+ #stanza_id{id = Id2} ->
+ Id2;
+ _ ->
+ p1_rand:get_string()
+ end,
+ NewPacket = wrap(From, undefined, Packet, Node, Id),
+ NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled),
+ ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
+ Wra, NewPacket2, true)
+ end.
+
+%%%----------------------------------------------------------------------
+%%% #muc_subscribers API
+%%%----------------------------------------------------------------------
+
+-spec muc_subscribers_new() -> #muc_subscribers{}.
+muc_subscribers_new() ->
+ #muc_subscribers{}.
+
+-spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}.
+muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) ->
+ maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_find(ljid(), #muc_subscribers{}) ->
+ {ok, #subscriber{}} | error.
+muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) ->
+ maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean().
+muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) ->
+ maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_size(#muc_subscribers{}) -> integer().
+muc_subscribers_size(MUCSubscribers) ->
+ maps:size(MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when
+ Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc).
+muc_subscribers_fold(Fun, Init, MUCSubscribers) ->
+ maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}].
+muc_subscribers_get_by_nick(Nick, MUCSubscribers) ->
+ maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []).
+
+-spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers().
+muc_subscribers_get_by_node(Node, MUCSubscribers) ->
+ maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}).
+
+-spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) ->
+ {#muc_subscribers{}, #subscriber{}}.
+muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) ->
+ #muc_subscribers{subscribers = Subs,
+ subscriber_nicks = SubNicks,
+ subscriber_nodes = SubNodes} = MUCSubscribers,
+ Subscriber = maps:get(LJID, Subs),
+ #subscriber{nick = Nick, nodes = Nodes} = Subscriber,
+ NewSubNicks = maps:remove(Nick, SubNicks),
+ NewSubs = maps:remove(LJID, Subs),
+ NewSubNodes =
+ lists:foldl(
+ fun(Node, Acc) ->
+ NodeSubs = maps:get(Node, Acc, #{}),
+ NodeSubs2 = maps:remove(LJID, NodeSubs),
+ maps:put(Node, NodeSubs2, Acc)
+ end, SubNodes, Nodes),
+ {#muc_subscribers{subscribers = NewSubs,
+ subscriber_nicks = NewSubNicks,
+ subscriber_nodes = NewSubNodes}, Subscriber}.
+
+-spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) ->
+ #muc_subscribers{}.
+muc_subscribers_put(Subscriber, MUCSubscribers) ->
+ #subscriber{jid = JID,
+ nick = Nick,
+ nodes = Nodes} = Subscriber,
+ #muc_subscribers{subscribers = Subs,
+ subscriber_nicks = SubNicks,
+ subscriber_nodes = SubNodes} = MUCSubscribers,
+ LJID = jid:tolower(JID),
+ NewSubs = maps:put(LJID, Subscriber, Subs),
+ NewSubNicks = maps:put(Nick, [LJID], SubNicks),
+ NewSubNodes =
+ lists:foldl(
+ fun(Node, Acc) ->
+ NodeSubs = maps:get(Node, Acc, #{}),
+ NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs),
+ maps:put(Node, NodeSubs2, Acc)
+ end, SubNodes, Nodes),
+ #muc_subscribers{subscribers = NewSubs,
+ subscriber_nicks = NewSubNicks,
+ subscriber_nodes = NewSubNodes}.
+
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaningful content
diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl
index 569cfac49..1310cde7b 100644
--- a/src/mod_muc_sql.erl
+++ b/src/mod_muc_sql.erl
@@ -29,7 +29,8 @@
-behaviour(mod_muc_room).
%% API
--export([init/2, store_room/5, restore_room/3, forget_room/3,
+-export([init/2, store_room/5, store_changes/4,
+ restore_room/3, forget_room/3,
can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4,
import/3, export/1]).
-export([register_online_room/4, unregister_online_room/4, find_online_room/3,
@@ -83,6 +84,12 @@ store_room(LServer, Host, Name, Opts, ChangesHints) ->
end,
ejabberd_sql:sql_transaction(LServer, F).
+store_changes(LServer, Host, Name, Changes) ->
+ F = fun () ->
+ [change_room(Host, Name, Change) || Change <- Changes]
+ end,
+ ejabberd_sql:sql_transaction(LServer, F).
+
change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) ->
SJID = jid:encode(JID),
SNodes = misc:term_to_expr(Nodes),
@@ -185,13 +192,20 @@ get_rooms(LServer, Host) ->
{selected, Subs} ->
SubsD = lists:foldl(
fun({Room, Jid, Nick, Nodes}, Dict) ->
- dict:append(Room, {jid:decode(Jid),
- Nick, ejabberd_sql:decode_term(Nodes)}, Dict)
- end, dict:new(), Subs),
+ Sub = {jid:decode(Jid),
+ Nick, ejabberd_sql:decode_term(Nodes)},
+ maps:update_with(
+ Room,
+ fun(SubAcc) ->
+ [Sub | SubAcc]
+ end,
+ [Sub],
+ Dict)
+ end, maps:new(), Subs),
lists:map(
fun({Room, Opts}) ->
OptsD = ejabberd_sql:decode_term(Opts),
- OptsD2 = case {dict:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of
+ OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of
{_, true} ->
store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined),
OptsD;
diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl
index 530bfecea..161d3a4c4 100644
--- a/src/mod_multicast.erl
+++ b/src/mod_multicast.erl
@@ -392,8 +392,9 @@ perform(From, Packet, _,
{route_single, Group}) ->
lists:foreach(
fun(ToUser) ->
+ Group_others = strip_other_bcc(ToUser, Group#group.others),
route_packet(From, ToUser, Packet,
- Group#group.others, Group#group.addresses)
+ Group_others, Group#group.addresses)
end, Group#group.dests);
perform(From, Packet, _,
{{route_multicast, JID, RLimits}, Group}) ->
@@ -424,6 +425,21 @@ 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
%%%-------------------------
@@ -545,7 +561,6 @@ build_other_xml(Dests) ->
case Dest#dest.type of
to -> [add_delivered(XML) | R];
cc -> [add_delivered(XML) | R];
- bcc -> R;
_ -> [XML | R]
end
end,
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index c3fda25db..1d367eb72 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -572,6 +572,16 @@ check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) ->
sub_els = [#xevent{id = ID, offline = true}]},
ejabberd_router:route(NewMsg),
true;
+ % Don't store composing events
+ #xevent{id = V, composing = true} when V /= undefined ->
+ false;
+ % Nor composing stopped events
+ #xevent{id = V, composing = false, delivered = false,
+ displayed = false, offline = false} when V /= undefined ->
+ false;
+ % But store other received notifications
+ #xevent{id = V} when V /= undefined ->
+ true;
_ ->
false
end.
@@ -1315,19 +1325,19 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
[{?T("This example allows power users to have as much as 5000 "
"offline messages, administrators up to 2000, and all the "
diff --git a/src/mod_ping.erl b/src/mod_ping.erl
index 483f2e834..f233b2ae8 100644
--- a/src/mod_ping.erl
+++ b/src/mod_ping.erl
@@ -154,7 +154,7 @@ handle_info({iq_reply, timeout, JID}, State) ->
{noreply, State#state{timers = Timers}};
handle_info({timeout, _TRef, {ping, JID}}, State) ->
Host = State#state.host,
- From = jid:remove_resource(JID),
+ From = jid:make(Host),
IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]},
ejabberd_router:route_iq(IQ, JID,
gen_mod:get_module_proc(Host, ?MODULE),
@@ -300,7 +300,7 @@ mod_doc() ->
desc =>
?T("How long to wait before deeming that a client "
"has not answered a given server ping request. "
- "The default value is '32' seconds.")}},
+ "The default value is 'undefined'.")}},
{send_pings,
#{value => "true | false",
desc =>
@@ -317,8 +317,8 @@ mod_doc() ->
"server ping request in less than period defined "
"in 'ping_ack_timeout' option: "
"'kill' means destroying the underlying connection, "
- "'none' means to do nothing. NOTE: when 'mod_stream_mgmt' "
- "module is loaded and stream management is enabled by "
+ "'none' means to do nothing. NOTE: when _`mod_stream_mgmt`_ "
+ "is loaded and stream management is enabled by "
"a client, killing the client connection doesn't mean "
"killing the client session - the session will be kept "
"alive in order to give the client a chance to resume it. "
diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl
index 4f15b80c4..5ac26c2f5 100644
--- a/src/mod_privacy.erl
+++ b/src/mod_privacy.erl
@@ -887,25 +887,25 @@ mod_doc() ->
"https://xmpp.org/extensions/xep-0191.html"
"[XEP-0191: Blocking Command] which is implemented by "
"'mod_blocking' module. However, you still need "
- "'mod_privacy' loaded in order for 'mod_blocking' to work.")],
+ "'mod_privacy' loaded in order for _`mod_blocking`_ to work.")],
opts =>
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_private.erl b/src/mod_private.erl
index ad36c8494..436aae222 100644
--- a/src/mod_private.erl
+++ b/src/mod_private.erl
@@ -66,7 +66,7 @@ start(Host, Opts) ->
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq),
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
stop(Host) ->
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
@@ -128,23 +128,23 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]},
jid(), jid(), binary(), binary()) ->
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
index a6d4ba446..353a8da27 100644
--- a/src/mod_privilege.erl
+++ b/src/mod_privilege.erl
@@ -106,7 +106,7 @@ mod_doc() ->
?T("WARNING: Security issue: Privileged access gives components "
"access to sensitive data, so permission should be granted "
"carefully, only if you trust a component."), "",
- ?T("NOTE: This module is complementary to 'mod_delegation', "
+ ?T("NOTE: This module is complementary to _`mod_delegation`_, "
"but can also be used separately.")],
opts =>
[{roster,
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index fecb35341..2e40d8f0e 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -45,6 +45,7 @@
-include("mod_roster.hrl").
-include("translate.hrl").
-include("ejabberd_stacktrace.hrl").
+-include("ejabberd_commands.hrl").
-define(STDTREE, <<"tree">>).
-define(STDNODE, <<"flat">>).
@@ -93,6 +94,9 @@
handle_call/3, handle_cast/2, handle_info/2, mod_doc/0,
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([route/1]).
%%====================================================================
@@ -210,7 +214,7 @@
pep_mapping :: [{binary(), binary()}],
ignore_pep_from_offline :: boolean(),
last_item_cache :: boolean(),
- max_items_node :: non_neg_integer(),
+ max_items_node :: non_neg_integer()|unlimited,
max_subscriptions_node :: non_neg_integer()|undefined,
default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}],
nodetree :: binary(),
@@ -337,6 +341,7 @@ init([ServerHost|_]) ->
false ->
ok
end,
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
NodeTree = config(ServerHost, nodetree),
Plugins = config(ServerHost, plugins),
PepMapping = config(ServerHost, pep_mapping),
@@ -806,7 +811,13 @@ terminate(_Reason,
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS),
terminate_plugins(Host, ServerHost, Plugins, TreePlugin),
ejabberd_router:unregister_route(Host)
- end, Hosts).
+ end, Hosts),
+ case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of
+ false ->
+ ejabberd_commands:unregister_commands(get_commands_spec());
+ true ->
+ ok
+ end.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
@@ -3399,14 +3410,14 @@ node_config(_, _, []) ->
%% @doc Return the maximum number of items for a given node.
%% Unlimited means that there is no limit in the number of items that can
%% be stored.
--spec max_items(host(), [{atom(), any()}]) -> non_neg_integer().
+-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited.
max_items(Host, Options) ->
case get_option(Options, persist_items) of
true ->
case get_option(Options, max_items) of
I when is_integer(I), I < 0 -> 0;
I when is_integer(I) -> I;
- _ -> ?MAXITEMS
+ _ -> get_max_items_node(Host)
end;
false ->
case get_option(Options, send_last_published_item) of
@@ -3548,14 +3559,19 @@ decode_get_pending(#xdata{fields = Fs}, Lang) ->
{error, xmpp:err_resource_constraint(Txt, Lang)}
end.
--spec check_opt_range(atom(), [proplists:property()], non_neg_integer()) -> boolean().
+-spec check_opt_range(atom(), [proplists:property()],
+ non_neg_integer() | unlimited | undefined) -> boolean().
check_opt_range(_Opt, _Opts, undefined) ->
true;
+check_opt_range(_Opt, _Opts, unlimited) ->
+ true;
check_opt_range(Opt, Opts, Max) ->
- Val = proplists:get_value(Opt, Opts, Max),
- Val =< Max.
+ case proplists:get_value(Opt, Opts, Max) of
+ max -> true;
+ Val -> Val =< Max
+ end.
--spec get_max_items_node(host()) -> undefined | non_neg_integer().
+-spec get_max_items_node(host()) -> undefined | unlimited | non_neg_integer().
get_max_items_node(Host) ->
config(Host, max_items_node, undefined).
@@ -3707,6 +3723,7 @@ features() ->
<<"access-whitelist">>, % OPTIONAL
<<"collections">>, % RECOMMENDED
<<"config-node">>, % RECOMMENDED
+ <<"config-node-max">>,
<<"create-and-configure">>, % RECOMMENDED
<<"item-ids">>, % RECOMMENDED
<<"last-published">>, % RECOMMENDED
@@ -4136,6 +4153,46 @@ purge_offline(Host, LJID, Node) ->
{error, xmpp:err_internal_server_error(Txt, Lang)}
end.
+-spec delete_old_items(non_neg_integer()) -> ok | error.
+delete_old_items(N) ->
+ 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}) ->
+ case node_action(Host, Type,
+ remove_extra_items,
+ [Nidx , N]) of
+ {result, _} ->
+ ok;
+ {error, _} ->
+ error
+ 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",
+ 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}].
+
-spec mod_opt_type(atom()) -> econf:validator().
mod_opt_type(access_createnode) ->
econf:acl();
@@ -4146,7 +4203,7 @@ mod_opt_type(ignore_pep_from_offline) ->
mod_opt_type(last_item_cache) ->
econf:bool();
mod_opt_type(max_items_node) ->
- econf:non_neg_int();
+ econf:non_neg_int(unlimited);
mod_opt_type(max_nodes_discoitems) ->
econf:non_neg_int(infinity);
mod_opt_type(max_subscriptions_node) ->
@@ -4212,7 +4269,7 @@ mod_doc() ->
"(https://xmpp.org/extensions/xep-0163.html"
"[XEP-0163: Personal Eventing via Pubsub]) "
"is enabled in the default ejabberd configuration file, "
- "and it requires 'mod_caps'.")],
+ "and it requires _`mod_caps`_.")],
opts =>
[{access_createnode,
#{value => "AccessName",
@@ -4225,7 +4282,7 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to "
+ ?T("Same as top-level _`default_db`_ option, but applied to "
"this module only.")}},
{default_node_config,
#{value => "List of Key:Value",
@@ -4273,7 +4330,7 @@ mod_doc() ->
"and allows to raise user connection rate. The cost "
"is memory usage, as every item is stored in memory.")}},
{max_items_node,
- #{value => "MaxItems",
+ #{value => "non_neg_integer() | infinity",
desc =>
?T("Define the maximum number of items that can be "
"stored in a node. Default value is: '10'.")}},
diff --git a/src/mod_push.erl b/src/mod_push.erl
index 14ee02910..5477c5792 100644
--- a/src/mod_push.erl
+++ b/src/mod_push.erl
@@ -98,7 +98,7 @@ start(Host, Opts) ->
init_cache(Mod, Host, Opts),
register_iq_handlers(Host),
register_hooks(Host),
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
-spec stop(binary()) -> ok.
stop(Host) ->
@@ -191,23 +191,23 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
%%--------------------------------------------------------------------
%% ejabberd command callback.
@@ -405,7 +405,7 @@ c2s_stanza(State, #stream_error{}, _SendResult) ->
c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State,
Pkt, _SendResult) ->
?DEBUG("Notifying client of stanza", []),
- notify(State, unwrap_message(Pkt), get_direction(Pkt)),
+ notify(State, Pkt, get_direction(Pkt)),
State;
c2s_stanza(State, _Pkt, _SendResult) ->
State.
@@ -454,7 +454,7 @@ c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) ->
{Pkt, Dir} = case mod_stream_mgmt:queue_find(
fun is_incoming_chat_msg/1, Queue) of
none -> {none, undefined};
- Pkt0 -> {unwrap_message(Pkt0), get_direction(Pkt0)}
+ Pkt0 -> {Pkt0, get_direction(Pkt0)}
end,
notify(State, Pkt, Dir),
State;
@@ -536,7 +536,8 @@ notify(LUser, LServer, Clients, Pkt, Dir) ->
-spec notify(binary(), ljid(), binary(), xdata(),
xmpp_element() | xmlel() | none, direction(),
fun((iq() | timeout) -> any())) -> ok.
-notify(LServer, PushLJID, Node, XData, Pkt, Dir, HandleResponse) ->
+notify(LServer, PushLJID, Node, XData, Pkt0, Dir, HandleResponse) ->
+ Pkt = unwrap_message(Pkt0),
From = jid:make(LServer),
Summary = make_summary(LServer, Pkt, Dir),
Item = #ps_item{sub_els = [#push_notification{xdata = Summary}]},
@@ -714,7 +715,7 @@ make_summary(Host, #message{from = From} = Pkt, recv) ->
make_summary(_Host, _Pkt, _Dir) ->
undefined.
--spec unwrap_message(stanza()) -> stanza().
+-spec unwrap_message(Stanza) -> Stanza when Stanza :: stanza() | none.
unwrap_message(#message{meta = #{carbon_copy := true}} = Msg) ->
misc:unwrap_carbon(Msg);
unwrap_message(#message{type = normal} = Msg) ->
diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl
index 83b89ff50..e0e83f1e1 100644
--- a/src/mod_push_keepalive.erl
+++ b/src/mod_push_keepalive.erl
@@ -87,20 +87,20 @@ mod_opt_type(wake_on_timeout) ->
econf:bool().
mod_options(_Host) ->
- [{resume_timeout, timer:seconds(259200)},
+ [{resume_timeout, timer:hours(72)},
{wake_on_start, false},
{wake_on_timeout, true}].
mod_doc() ->
#{desc =>
[?T("This module tries to keep the stream management "
- "session (see 'mod_stream_mgmt') of a disconnected "
+ "session (see _`mod_stream_mgmt`_) of a disconnected "
"mobile client alive if the client enabled push "
"notifications for that session. However, the normal "
"session resumption timeout is restored once a push "
"notification is issued, so the session will be closed "
"if the client doesn't respond to push notifications."), "",
- ?T("The module depends on 'mod_push'.")],
+ ?T("The module depends on _`mod_push`_.")],
opts =>
[{resume_timeout,
#{value => "timeout()",
@@ -109,9 +109,9 @@ mod_doc() ->
"the session of a disconnected push client times out. "
"This timeout is only in effect as long as no push "
"notification is issued. Once that happened, the "
- "resumption timeout configured for the 'mod_stream_mgmt' "
- "module is restored. "
- "The default value is '72' minutes.")}},
+ "resumption timeout configured for _`mod_stream_mgmt`_ "
+ "is restored. "
+ "The default value is '72' hours.")}},
{wake_on_start,
#{value => "true | false",
desc =>
diff --git a/src/mod_register.erl b/src/mod_register.erl
index a864c0df2..379318da6 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -638,9 +638,9 @@ mod_doc() ->
?T("* Register a new account on the server."), "",
?T("* Change the password from an existing account on the server."), "",
?T("* Delete an existing account on the server."), "",
- ?T("This module reads also another option defined globally for the "
- "server: 'registration_timeout'. Please check that option "
- "documentation in the section with top-level options.")],
+ ?T("This module reads also the top-level _`registration_timeout`_ "
+ "option defined globally for the server, "
+ "so please check that option documentation too.")],
opts =>
[{access,
#{value => ?T("AccessName"),
@@ -664,9 +664,8 @@ mod_doc() ->
{captcha_protected,
#{value => "true | false",
desc =>
- ?T("Protect registrations with CAPTCHA (see section "
- "https://docs.ejabberd.im/admin/configuration/basic/#captcha[CAPTCHA] "
- "of the Configuration Guide). The default is 'false'.")}},
+ ?T("Protect registrations with http://../basic/#captcha[CAPTCHA]. "
+ "The default is 'false'.")}},
{ip_access,
#{value => ?T("AccessName"),
desc =>
@@ -680,7 +679,7 @@ mod_doc() ->
"https://en.wikipedia.org/wiki/Entropy_(information_theory)"
"[Shannon entropy] for passwords. The value 'Entropy' is a "
"number of bits of entropy. The recommended minimum is 32 bits. "
- "The default is 0, i.e. no checks are performed.")}},
+ "The default is '0', i.e. no checks are performed.")}},
{registration_watchers,
#{value => "[JID, ...]",
desc =>
diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl
index 9c2179302..0e216c81c 100644
--- a/src/mod_register_web.erl
+++ b/src/mod_register_web.erl
@@ -23,32 +23,6 @@
%%%
%%%----------------------------------------------------------------------
-%%% IDEAS:
-%%%
-%%% * Implement those options, already present in mod_register:
-%%% + access
-%%% + captcha_protected
-%%% + password_strength
-%%% + welcome_message
-%%% + registration_timeout
-%%%
-%%% * Improve this module to allow each virtual host to have different
-%%% options. See http://support.process-one.net/browse/EJAB-561
-%%%
-%%% * Check that all the text is translatable.
-%%%
-%%% * Add option to use a custom CSS file, or custom CSS lines.
-%%%
-%%% * Don't hardcode the "register" path in URL.
-%%%
-%%% * Allow private email during register, and store in custom table.
-%%% * Optionally require private email to register.
-%%% * Optionally require email confirmation to register.
-%%% * Allow to set a private email address anytime.
-%%% * Allow to recover password using private email to confirm (mod_passrecover)
-%%% * Optionally require invitation
-%%% * Optionally register request is forwarded to admin, no account created.
-
-module(mod_register_web).
-author('badlop@process-one.net').
@@ -533,14 +507,18 @@ form_del_get(Host, Lang) ->
%% {error, not_allowed} |
%% {error, invalid_jid}
register_account(Username, Host, Password) ->
- Access = mod_register_opt:access(Host),
- case jid:make(Username, Host) of
- error -> {error, invalid_jid};
- JID ->
- case acl:match_rule(Host, Access, JID) of
- deny -> {error, not_allowed};
- allow -> register_account2(Username, Host, Password)
- end
+ try mod_register_opt:access(Host) of
+ Access ->
+ case jid:make(Username, Host) of
+ error -> {error, invalid_jid};
+ JID ->
+ case acl:match_rule(Host, Access, JID) of
+ deny -> {error, not_allowed};
+ allow -> register_account2(Username, Host, Password)
+ end
+ end
+ catch _:{module_not_loaded, mod_register, _Host} ->
+ {error, host_unknown}
end.
register_account2(Username, Host, Password) ->
@@ -603,6 +581,8 @@ 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}) ->
@@ -625,13 +605,27 @@ mod_doc() ->
?T("- Register a new account on the server."), "",
?T("- Change the password from an existing account on the server."), "",
?T("- Unregister an existing account on the server."), "",
- ?T("This module supports CAPTCHA image to register a new account. "
- "To enable this feature, configure the options 'captcha\_cmd' "
- "and 'captcha\_url', which are documented in the section with "
- "top-level options."), "",
- ?T("As an example usage, the users of the host 'example.org' can "
- "visit the page: 'https://example.org:5281/register/' It is "
+ ?T("This module supports http://../basic/#captcha[CAPTCHA] "
+ "to register a new account. "
+ "To enable this feature, configure the "
+ "top-level _`captcha_cmd`_ and "
+ "top-level _`captcha_url`_ options."), "",
+ ?T("As an example usage, the users of the host 'localhost' can "
+ "visit the page: 'https://localhost:5280/register/' It is "
"important to include the last / character in the URL, "
"otherwise the subpages URL will be incorrect."), "",
- ?T("The module depends on 'mod_register' where all the configuration "
- "is performed.")]}.
+ ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers], "
+ "no need to enable in 'modules'."),
+ ?T("The module depends on _`mod_register`_ where all the "
+ "configuration is performed.")],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /register: mod_register_web",
+ "",
+ "modules:",
+ " mod_register: {}"]}.
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index f204c9211..94cae4950 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -1345,29 +1345,29 @@ mod_doc() ->
"This option does not affect the client in any way. "
"This option is only useful if option 'versioning' is "
"set to 'true'. The default value is 'false'. "
- "IMPORTANT: if you use 'mod_shared_roster' or "
- "'mod_shared_roster_ldap', you must set the value "
+ "IMPORTANT: if you use _`mod_shared_roster`_ or "
+ " _`mod_shared_roster_ldap`_, you must set the value "
"of the option to 'false'.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
["modules:",
" ...",
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 16cc96a75..13ff90466 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -1266,7 +1266,7 @@ mod_doc() ->
?T("- Displayed: A list of groups that will be in the "
"rosters of this group's members. A group of other vhost can "
"be identified with 'groupid@vhost'."), "",
- ?T("This module depends on 'mod_roster'. "
+ ?T("This module depends on _`mod_roster`_. "
"If not enabled, roster queries will return 503 errors.")],
opts =>
[{db_type,
@@ -1274,25 +1274,25 @@ mod_doc() ->
desc =>
?T("Define the type of storage where the module will create "
"the tables and store user information. The default is "
- "the storage defined by the global option 'default_db', "
+ "the storage defined by the top-level _`default_db`_ option, "
"or 'mnesia' if omitted. If 'sql' value is defined, "
"make sure you have defined the database.")}},
- {use_cache,
+ {use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
[{?T("Take the case of a computer club that wants all its members "
"seeing each other in their rosters. To achieve this, they "
diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl
index 93c08e0c3..08fbe8793 100644
--- a/src/mod_shared_roster_ldap.erl
+++ b/src/mod_shared_roster_ldap.erl
@@ -796,7 +796,7 @@ mod_doc() ->
"disable the check. Default value is 'true'.")}}] ++
[{Opt,
#{desc =>
- {?T("Same as top-level '~s' option, but "
+ {?T("Same as top-level _`~s`_ option, but "
"applied to this module only."), [Opt]}}}
|| Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases,
ldap_encrypt, ldap_password, ldap_port, ldap_rootdn,
diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl
index b9443e5d2..f60f6722b 100644
--- a/src/mod_stream_mgmt.erl
+++ b/src/mod_stream_mgmt.erl
@@ -962,12 +962,14 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
+ ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, "
+ "but applied to this module only. "
+ "The default value is '48 hours'.")}}]}.
diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl
index bb701b96b..6e7592453 100644
--- a/src/mod_stun_disco.erl
+++ b/src/mod_stun_disco.erl
@@ -176,7 +176,7 @@ mod_doc() ->
"clients. If ejabberd's built-in TURN service is used, "
"TURN relays allocated using temporary credentials will "
"be terminated shortly after the credentials expired. The "
- "default value is '12' hours. Note that restarting the "
+ "default value is '12 hours'. Note that restarting the "
"ejabberd node invalidates any temporary credentials "
"offered before the restart unless a 'secret' is "
"specified (see below).")}},
diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl
index e7cfff819..8e0d32a4a 100644
--- a/src/mod_vcard.erl
+++ b/src/mod_vcard.erl
@@ -640,23 +640,23 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql | ldap",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
{vcard,
#{value => ?T("vCard"),
desc =>
diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl
index c81c058f5..bc6e7ebca 100644
--- a/src/mod_vcard_ldap.erl
+++ b/src/mod_vcard_ldap.erl
@@ -571,7 +571,7 @@ mod_doc() ->
}]}}] ++
[{Opt,
#{desc =>
- {?T("Same as top-level '~s' option, but "
+ {?T("Same as top-level _`~s`_ option, but "
"applied to this module only."), [Opt]}}}
|| Opt <- [ldap_base, ldap_servers, ldap_uids,
ldap_deref_aliases, ldap_encrypt, ldap_password,
diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl
index ab8df2c60..59ebc7f71 100644
--- a/src/mod_vcard_xupdate.erl
+++ b/src/mod_vcard_xupdate.erl
@@ -228,26 +228,26 @@ mod_doc() ->
"frequently their presence. However, the overhead is significantly "
"reduced by the use of caching, so you probably don't want "
"to set 'use_cache' to 'false'."), "",
- ?T("The module depends on 'mod_vcard'."), "",
+ ?T("The module depends on _`mod_vcard`_."), "",
?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html"
"[XEP-0153] is used mostly as \"read-only\", i.e. modern "
"clients don't publish their avatars inside vCards. Thus "
"in the majority of cases the module is only used along "
- "with 'mod_avatar' module for providing backward compatibility.")],
+ "with _`mod_avatar`_ for providing backward compatibility.")],
opts =>
[{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/node_flat.erl b/src/node_flat.erl
index 4a2a60971..c597b9ce9 100644
--- a/src/node_flat.erl
+++ b/src/node_flat.erl
@@ -39,7 +39,8 @@
-export([init/3, terminate/2, options/0, features/0,
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/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -375,7 +376,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
or (Subscribed == true)) ->
{error, xmpp:err_forbidden()};
true ->
- if MaxItems > 0 ->
+ if MaxItems > 0;
+ MaxItems == unlimited ->
Now = erlang:timestamp(),
case get_item(Nidx, ItemId) of
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
@@ -402,6 +404,16 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
end
end.
+remove_extra_items(Nidx, MaxItems) ->
+ {result, States} = get_states(Nidx),
+ Records = States ++ mnesia:read({pubsub_orphan, Nidx}),
+ ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) ->
+ Is;
+ (#pubsub_orphan{items = Is}) ->
+ Is
+ end, Records),
+ remove_extra_items(Nidx, MaxItems, ItemIds).
+
%% @doc This function is used to remove extra items, most notably when the
%% maximum number of items has been reached.
%% This function is used internally by the core PubSub module, as no
@@ -945,15 +957,12 @@ rsm_page(Count, Index, Offset, Items) ->
last = Last}.
encode_stamp(Stamp) ->
- case catch xmpp_util:decode_timestamp(Stamp) of
- {MS,S,US} -> {MS,S,US};
- _ -> Stamp
+ try xmpp_util:decode_timestamp(Stamp)
+ catch _:{bad_timestamp, _} ->
+ Stamp % We should return a proper error to the client instead.
end.
decode_stamp(Stamp) ->
- case catch xmpp_util:encode_timestamp(Stamp) of
- TimeStamp when is_binary(TimeStamp) -> TimeStamp;
- _ -> Stamp
- end.
+ xmpp_util:encode_timestamp(Stamp).
transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) ->
{pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss};
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index 1e197a51d..240dc3760 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -40,9 +40,10 @@
-include("translate.hrl").
-export([init/3, terminate/2, options/0, features/0,
- 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/3,
+ 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,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -247,7 +248,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
or (Subscribed == true)) ->
{error, xmpp:err_forbidden()};
true ->
- if MaxItems > 0 ->
+ if MaxItems > 0;
+ MaxItems == unlimited ->
Now = erlang:timestamp(),
case get_item(Nidx, ItemId) of
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
@@ -258,20 +260,23 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
{result, _} ->
{error, xmpp:err_forbidden()};
_ ->
- Items = [ItemId | itemids(Nidx, GenKey)],
- {result, {_NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
+ OldIds = maybe_remove_extra_items(Nidx, MaxItems,
+ GenKey, ItemId),
set_item(#pubsub_item{
itemid = {ItemId, Nidx},
creation = {Now, GenKey},
modification = {Now, SubKey},
payload = Payload}),
- {result, {default, broadcast, OI}}
+ {result, {default, broadcast, OldIds}}
end;
true ->
{result, {default, broadcast, []}}
end
end.
+remove_extra_items(Nidx, MaxItems) ->
+ remove_extra_items(Nidx, MaxItems, itemids(Nidx)).
+
remove_extra_items(_Nidx, unlimited, ItemIds) ->
{result, {ItemIds, []}};
remove_extra_items(Nidx, MaxItems, ItemIds) ->
@@ -862,6 +867,18 @@ first_in_list(Pred, [H | T]) ->
_ -> first_in_list(Pred, T)
end.
+itemids(Nidx) ->
+ case catch
+ ejabberd_sql:sql_query_t(
+ ?SQL("select @(itemid)s from pubsub_item where "
+ "nodeid=%(Nidx)d order by modification desc"))
+ of
+ {selected, RItems} ->
+ [ItemId || {ItemId} <- RItems];
+ _ ->
+ []
+ end.
+
itemids(Nidx, {_U, _S, _R} = JID) ->
SJID = encode_jid(JID),
SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>,
@@ -933,6 +950,16 @@ update_subscription(Nidx, JID, Subscription) ->
"-affiliation='n'"
]).
+-spec maybe_remove_extra_items(mod_pubsub:nodeIdx(),
+ non_neg_integer() | unlimited, ljid(),
+ mod_pubsub:itemId()) -> [mod_pubsub:itemId()].
+maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) ->
+ [];
+maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) ->
+ ItemIds = [ItemId | itemids(Nidx, GenKey)],
+ {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds),
+ OldIds.
+
-spec decode_jid(SJID :: binary()) -> ljid().
decode_jid(SJID) ->
jid:tolower(jid:decode(SJID)).
@@ -1037,15 +1064,14 @@ rsm_page(Count, Index, Offset, Items) ->
last = Last}.
encode_stamp(Stamp) ->
- case catch xmpp_util:decode_timestamp(Stamp) of
- {MS,S,US} -> encode_now({MS,S,US});
- _ -> Stamp
+ try xmpp_util:decode_timestamp(Stamp) of
+ Now ->
+ encode_now(Now)
+ catch _:{bad_timestamp, _} ->
+ Stamp % We should return a proper error to the client instead.
end.
decode_stamp(Stamp) ->
- case catch xmpp_util:encode_timestamp(decode_now(Stamp)) of
- TimeStamp when is_binary(TimeStamp) -> TimeStamp;
- _ -> Stamp
- end.
+ xmpp_util:encode_timestamp(decode_now(Stamp)).
encode_now({T1, T2, T3}) ->
<<(misc:i2l(T1, 6))/binary, ":",
diff --git a/src/node_pep.erl b/src/node_pep.erl
index 58c3050a0..44388ca31 100644
--- a/src/node_pep.erl
+++ b/src/node_pep.erl
@@ -35,7 +35,8 @@
-export([init/3, terminate/2, options/0, features/0,
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/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -135,6 +136,9 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
Payload, PubOpts).
+remove_extra_items(Nidx, MaxItems) ->
+ node_flat:remove_extra_items(Nidx, MaxItems).
+
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat:remove_extra_items(Nidx, MaxItems, ItemIds).
diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl
index 7b21aa901..c0cf2b166 100644
--- a/src/node_pep_sql.erl
+++ b/src/node_pep_sql.erl
@@ -37,7 +37,8 @@
-export([init/3, terminate/2, options/0, features/0,
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/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -92,6 +93,9 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
Payload, PubOpts).
+remove_extra_items(Nidx, MaxItems) ->
+ node_flat_sql:remove_extra_items(Nidx, MaxItems).
+
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl
index fe15f3323..853c1fb93 100644
--- a/src/nodetree_tree.erl
+++ b/src/nodetree_tree.erl
@@ -46,7 +46,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -98,6 +99,14 @@ get_nodes(Host, Limit) ->
{Nodes, _} -> Nodes
end.
+get_all_nodes({_U, _S, _R} = Owner) ->
+ Host = jid:tolower(jid:remove_resource(Owner)),
+ mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'});
+get_all_nodes(Host) ->
+ mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'})
+ ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'},
+ _ = '_'}).
+
get_parentnodes(Host, Node, _From) ->
case catch mnesia:read({pubsub_node, {Host, Node}}) of
[Record] when is_record(Record, pubsub_node) ->
diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl
index d68355202..402c50901 100644
--- a/src/nodetree_tree_sql.erl
+++ b/src/nodetree_tree_sql.erl
@@ -45,7 +45,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -165,6 +166,34 @@ get_nodes(Host, Limit) ->
[]
end.
+get_all_nodes({_U, _S, _R} = JID) ->
+ SubKey = jid:tolower(JID),
+ GenKey = jid:remove_resource(SubKey),
+ EncKey = node_flat_sql:encode_jid(GenKey),
+ Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>,
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
+ "from pubsub_node where host=%(EncKey)s "
+ "or host like %(Pattern)s %ESCAPE")) of
+ {selected, RItems} ->
+ [raw_to_node(GenKey, Item) || Item <- RItems];
+ _ ->
+ []
+ end;
+get_all_nodes(Host) ->
+ Pattern1 = <<"%@", Host/binary>>,
+ Pattern2 = <<"%@", Host/binary, "/%">>,
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
+ "from pubsub_node where host=%(Host)s "
+ "or host like %(Pattern1)s "
+ "or host like %(Pattern2)s %ESCAPE")) of
+ {selected, RItems} ->
+ [raw_to_node(Host, Item) || Item <- RItems];
+ _ ->
+ []
+ end.
+
get_parentnodes(Host, Node, _From) ->
case get_node(Host, Node) of
Record when is_record(Record, pubsub_node) ->
diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl
index 9cf7a80ca..c0274a795 100644
--- a/src/nodetree_virtual.erl
+++ b/src/nodetree_virtual.erl
@@ -38,7 +38,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -71,6 +72,9 @@ get_nodes(Host) ->
get_nodes(_Host, _Limit) ->
[].
+get_all_nodes(_Host) ->
+ [].
+
get_parentnodes(_Host, _Node, _From) ->
[].
diff --git a/test/offline_tests.erl b/test/offline_tests.erl
index 1021c86e8..b5a90e7ee 100644
--- a/test/offline_tests.erl
+++ b/test/offline_tests.erl
@@ -489,6 +489,14 @@ wait_for_complete(Config, N) ->
end
end, error, [0, 100, 200, 2000, 5000, 10000]).
+xevent_stored(#message{body = [], subject = []}, _) -> false;
+xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false;
+xevent_stored(_, #xevent{id = undefined}) -> true;
+xevent_stored(_, #xevent{offline = true}) -> true;
+xevent_stored(_, #xevent{delivered = true}) -> true;
+xevent_stored(_, #xevent{displayed = true}) -> true;
+xevent_stored(_, _) -> false.
+
message_iterator(Config) ->
ServerJID = server_jid(Config),
ChatStates = [[#chatstate{type = composing}]],
@@ -511,8 +519,14 @@ message_iterator(Config) ->
fun(#message{type = error}) -> true;
(#message{type = groupchat}) -> false;
(#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true;
+ (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false;
(#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false;
- (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined, not MamEnabled -> false;
+ (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg#message{body = body, type = chat}, Event);
+ (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg, Event);
+ (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg, Event);
(#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false;
(#message{sub_els = [#hint{type = store}|_]}) -> true;
(#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false;
diff --git a/tools/captcha-ng.sh b/tools/captcha-ng.sh
index cbcb95407..bb57385c4 100755
--- a/tools/captcha-ng.sh
+++ b/tools/captcha-ng.sh
@@ -42,7 +42,8 @@ INTRUDER()
{
NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ')
SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ')
-RANDOM_DIGITS=$(echo 123456789 | grep -o . | sort -R | tr '\n' ' ')
+SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)"
+RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ')
INTRUDER=-1
for i in $RANDOM_DIGITS