aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.in3
-rw-r--r--ejabberd.yml.example33
-rw-r--r--rebar.config2
-rw-r--r--src/ejabberd_config.erl7
-rw-r--r--src/ejabberd_ctl.erl11
-rw-r--r--src/ejabberd_http.erl44
-rw-r--r--src/ejabberd_http_ws.erl12
-rw-r--r--src/ejabberd_websocket.erl98
-rw-r--r--src/gen_mod.erl28
-rw-r--r--src/misc.erl32
-rw-r--r--src/mod_http_upload.erl8
-rw-r--r--src/mod_mam.erl51
-rw-r--r--src/mod_mam_sql.erl42
-rw-r--r--src/mod_mqtt_ws.erl2
-rw-r--r--src/mod_muc.erl47
-rw-r--r--src/mod_muc_room.erl72
-rw-r--r--src/mod_offline.erl364
-rw-r--r--src/mod_stream_mgmt.erl38
-rw-r--r--src/rest.erl37
-rw-r--r--test/README-quicktest.md33
-rw-r--r--test/ejabberd_SUITE.erl8
-rw-r--r--test/mam_tests.erl117
-rw-r--r--test/offline_tests.erl44
-rw-r--r--test/suite.hrl23
24 files changed, 835 insertions, 321 deletions
diff --git a/Makefile.in b/Makefile.in
index d3bcb282d..1c3fbc204 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -375,9 +375,6 @@ test:
@cd priv && ln -sf ../sql
$(REBAR) skip_deps=true ct
-quicktest:
- $(REBAR) skip_deps=true ct suites=elixir
-
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \
install uninstall uninstall-binary uninstall-all translations deps test \
quicktest erlang_plt deps_plt ejabberd_plt
diff --git a/ejabberd.yml.example b/ejabberd.yml.example
index 2d2a89185..e62dd8bc2 100644
--- a/ejabberd.yml.example
+++ b/ejabberd.yml.example
@@ -39,24 +39,6 @@ certfiles:
- "/etc/letsencrypt/live/localhost/fullchain.pem"
- "/etc/letsencrypt/live/localhost/privkey.pem"
-define_macro:
- # TLS options for client not being able to use modern ciphers (Windows XP+, Android 3.0+)
- CIPHERS_INTERMEDIATE: "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"
- PROTOCOL_OPTIONS_INTERMEDIATE:
- - "no_sslv2"
- - "no_sslv3"
-
- # TLS options for client able to use modern ciphers (Windows 7+, Android 5.0+)
- CIPHERS_MODERN: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
- PROTOCOL_OPTIONS_MODERN:
- - "no_sslv2"
- - "no_sslv3"
- - "no_tlsv1"
- - "no_tlsv1_1"
-
-c2s_ciphers: CIPHERS_INTERMEDIATE
-c2s_protocol_options: PROTOCOL_OPTIONS_INTERMEDIATE
-
listen:
-
port: 5222
@@ -75,21 +57,20 @@ listen:
port: 5443
ip: "::"
module: ejabberd_http
+ tls: true
request_handlers:
+ "/admin": ejabberd_web_admin
"/api": mod_http_api
"/bosh": mod_bosh
+ "/captcha": ejabberd_captcha
"/upload": mod_http_upload
"/ws": ejabberd_http_ws
- web_admin: true
- captcha: true
- ciphers: CIPHERS_INTERMEDIATE
- protocol_options: PROTOCOL_OPTIONS_INTERMEDIATE
- tls: true
-
port: 5280
ip: "::"
module: ejabberd_http
- web_admin: true
+ request_handlers:
+ "/admin": ejabberd_web_admin
-
port: 1883
ip: "::"
@@ -220,10 +201,6 @@ modules:
- "flat"
- "pep"
force_node_config:
- ## Change from "whitelist" to "open" to enable OMEMO support
- ## See https://github.com/processone/ejabberd/issues/2425
- "eu.siacs.conversations.axolotl.*":
- access_model: whitelist
## Avoid buggy clients to make their bookmarks public
"storage:bookmarks":
access_model: whitelist
diff --git a/rebar.config b/rebar.config
index ffb2a94bd..583ca0b9b 100644
--- a/rebar.config
+++ b/rebar.config
@@ -24,7 +24,7 @@
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.0"}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.15"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.35"}}},
- {xmpp, ".*", {git, "https://github.com/processone/xmpp", "f3e953e8"}},
+ {xmpp, ".*", {git, "https://github.com/processone/xmpp", "d8e770495a42c22cd23536c85af5b1fd92a2139a"}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.18"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.4"}}},
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index b9b0f8ee7..f4abf8d55 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -38,7 +38,8 @@
default_db/1, default_db/2, default_ram_db/1, default_ram_db/2,
default_queue_type/1, queue_dir/0, fsm_limit_opts/1,
use_cache/1, cache_size/1, cache_missed/1, cache_life_time/1,
- codec_options/1, get_plain_terms_file/2, negotiation_timeout/0]).
+ codec_options/1, get_plain_terms_file/2, negotiation_timeout/0,
+ get_modules/0]).
-export([start/2]).
@@ -1099,7 +1100,9 @@ validate_opts(#state{opts = Opts} = State, ModOpts) ->
erlang:error(invalid_option)
end;
_ ->
- ?ERROR_MSG("Unknown option '~s'", [Opt]),
+ KnownOpts = dict:fetch_keys(ModOpts),
+ ?ERROR_MSG("Unknown option '~s', did you mean '~s'?",
+ [Opt, misc:best_match(Opt, KnownOpts)]),
erlang:error(unknown_option)
end
end, Opts),
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index 73a98827c..9cc4f83c0 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -319,13 +319,18 @@ try_run_ctp(Args, Auth, AccessCommands, Version) ->
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
try_call_command(Args, Auth, AccessCommands, Version) ->
try call_command(Args, Auth, AccessCommands, Version) of
- {error, command_unknown} ->
- {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
{error, wrong_command_arguments} ->
{"Error: wrong arguments", ?STATUS_ERROR};
Res ->
Res
catch
+ throw:{error, unknown_command} ->
+ KnownCommands = [Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version)],
+ UnknownCommand = list_to_atom(hd(Args)),
+ {io_lib:format(
+ "Error: unknown command '~s'. Did you mean '~s'?",
+ [hd(Args), misc:best_match(UnknownCommand, KnownCommands)]),
+ ?STATUS_ERROR};
throw:Error ->
{io_lib:format("~p", [Error]), ?STATUS_ERROR};
?EX_RULE(A, Why, Stack) ->
@@ -340,7 +345,7 @@ call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
Command = list_to_atom(binary_to_list(CmdStringU)),
case ejabberd_commands:get_command_format(Command, Auth, Version) of
{error, command_unknown} ->
- {error, command_unknown};
+ throw({error, unknown_command});
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index e37269659..29d23e082 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -971,15 +971,30 @@ prepare_request_module(Mod) when is_atom(Mod) ->
Mod;
Err ->
?ERROR_MSG(
- "Failed to load request handler ~s: "
+ "Failed to load request handler ~s, "
+ "did you mean ~s? Hint: "
"make sure there is no typo and file ~s.beam "
"exists inside either ~s or ~s directory",
- [Mod, Mod,
+ [Mod,
+ misc:best_match(Mod, ejabberd_config:get_modules()),
+ Mod,
filename:dirname(code:which(?MODULE)),
ext_mod:modules_dir()]),
erlang:error(Err)
end.
+emit_option_replacement(Option, Path, Handler) ->
+ ?WARNING_MSG(
+ "Listening option '~s' is deprecated, enable it via request handlers, e.g.:~n"
+ "listen:~n"
+ " ...~n"
+ " -~n"
+ " module: ~s~n"
+ " request_handlers:~n"
+ " ...~n"
+ " \"~s\": ~s~n",
+ [Option, ?MODULE, Path, Handler]).
+
-spec opt_type(atom()) -> fun((any()) -> any()) | [atom()].
opt_type(trusted_proxies) ->
fun (all) -> all;
@@ -1001,15 +1016,30 @@ listen_opt_type(certfile = Opt) ->
File
end;
listen_opt_type(captcha) ->
- fun(B) when is_boolean(B) -> B end;
+ fun(B) when is_boolean(B) ->
+ emit_option_replacement(captcha, "/captcha", ejabberd_captcha),
+ B
+ end;
listen_opt_type(register) ->
- fun(B) when is_boolean(B) -> B end;
+ fun(B) when is_boolean(B) ->
+ emit_option_replacement(register, "/register", mod_register_web),
+ B
+ end;
listen_opt_type(web_admin) ->
- fun(B) when is_boolean(B) -> B end;
+ fun(B) when is_boolean(B) ->
+ emit_option_replacement(web_admin, "/admin", ejabberd_web_admin),
+ B
+ end;
listen_opt_type(http_bind) ->
- fun(B) when is_boolean(B) -> B end;
+ fun(B) when is_boolean(B) ->
+ emit_option_replacement(http_bind, "/bosh", mod_bosh),
+ B
+ end;
listen_opt_type(xmlrpc) ->
- fun(B) when is_boolean(B) -> B end;
+ fun(B) when is_boolean(B) ->
+ emit_option_replacement(xmlrpc, "/", ejabberd_xmlrpc),
+ B
+ end;
listen_opt_type(tag) ->
fun(B) when is_binary(B) -> B end;
listen_opt_type(request_handlers) ->
diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl
index dbde28caa..26e68fdaa 100644
--- a/src/ejabberd_http_ws.erl
+++ b/src/ejabberd_http_ws.erl
@@ -201,15 +201,15 @@ handle_sync_event({send_xml, Packet}, _From, StateName,
case Packet2 of
{xmlstreamstart, Name, Attrs3} ->
B = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}),
- WsPid ! {send, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>};
+ WsPid ! {text, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>};
{xmlstreamend, Name} ->
- WsPid ! {send, <<"</", Name/binary, ">">>};
+ WsPid ! {text, <<"</", Name/binary, ">">>};
{xmlstreamelement, El} ->
- WsPid ! {send, fxml:element_to_binary(El)};
+ WsPid ! {text, fxml:element_to_binary(El)};
{xmlstreamraw, Bin} ->
- WsPid ! {send, Bin};
+ WsPid ! {text, Bin};
{xmlstreamcdata, Bin2} ->
- WsPid ! {send, Bin2};
+ WsPid ! {text, Bin2};
skip ->
ok
end,
@@ -224,7 +224,7 @@ handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}, rfc_compilant
when StateName /= stream_end_sent ->
Close = #xmlel{name = <<"close">>,
attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]},
- WsPid ! {send, fxml:element_to_binary(Close)},
+ WsPid ! {text, fxml:element_to_binary(Close)},
{stop, normal, StateData};
handle_sync_event(close, _From, _StateName, StateData) ->
{stop, normal, StateData}.
diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl
index edc602f55..e954b42c2 100644
--- a/src/ejabberd_websocket.erl
+++ b/src/ejabberd_websocket.erl
@@ -42,7 +42,7 @@
-author('ecestari@process-one.net').
--export([check/2, socket_handoff/5, opt_type/1]).
+-export([socket_handoff/5, opt_type/1]).
-include("logger.hrl").
@@ -62,29 +62,39 @@
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
-define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).
-check(_Path, Headers) ->
- HeadersValidators = [{'Upgrade', <<"websocket">>, true},
- {'Connection', ignore, true}, {'Host', ignore, true},
- {<<"Sec-Websocket-Key">>, ignore, true},
- {<<"Sec-Websocket-Version">>, <<"13">>, true},
- {<<"Origin">>, get_origin(), false}],
-
- F = fun ({Tag, Val, Required}) ->
- case lists:keyfind(Tag, 1, Headers) of
- false -> Required; % header not found, keep in list if required
- {_, HVal} ->
- case Val of
- ignore -> false; % ignore value -> ok, remove from list
- _ ->
- % expected value -> ok, remove from list (false)
- % value is different, keep in list (true)
- str:to_lower(HVal) /= Val
- end
- end
- end,
- case lists:filter(F, HeadersValidators) of
- [] -> true;
- _InvalidHeaders -> false
+is_valid_websocket_upgrade(_Path, Headers) ->
+ HeadersToValidate = [{'Upgrade', <<"websocket">>},
+ {'Connection', ignore},
+ {'Host', ignore},
+ {<<"Sec-Websocket-Key">>, ignore},
+ {<<"Sec-Websocket-Version">>, <<"13">>}],
+ Res = lists:all(
+ fun({Tag, Val}) ->
+ case lists:keyfind(Tag, 1, Headers) of
+ false ->
+ false;
+ {_, _} when Val == ignore ->
+ true;
+ {_, HVal} ->
+ str:to_lower(HVal) == Val
+ end
+ end, HeadersToValidate),
+
+ case {Res, lists:keyfind(<<"Origin">>, 1, Headers), get_origin()} of
+ {false, _, _} ->
+ false;
+ {true, _, []} ->
+ true;
+ {true, {_, HVal}, Origins} ->
+ HValLow = str:to_lower(HVal),
+ case lists:any(fun(V) -> V == HValLow end, Origins) of
+ true ->
+ true;
+ _ ->
+ invalid_origin
+ end;
+ {true, false, _} ->
+ true
end.
socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
@@ -92,7 +102,7 @@ socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
socket = Socket, sockmod = SockMod,
data = Buf, opts = HOpts},
_Opts, HandlerModule, InfoMsgFun) ->
- case check(LocalPath, Headers) of
+ case is_valid_websocket_upgrade(LocalPath, Headers) of
true ->
WS = #ws{socket = Socket,
sockmod = SockMod,
@@ -107,8 +117,11 @@ socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
http_opts = HOpts},
connect(WS, HandlerModule);
- _ ->
- {200, ?HEADER, InfoMsgFun()}
+ false ->
+ {200, ?HEADER, InfoMsgFun()};
+ invalid_origin ->
+ {403, ?HEADER, #xmlel{name = <<"h1">>,
+ children = [{xmlcdata, <<"403 Bad Request - Invalid origin">>}]}}
end;
socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _) ->
{200, ?OPTIONS_HEADER, []};
@@ -202,10 +215,14 @@ ws_loop(FrameInfo, Socket, WsHandleLoopPid, SocketMode) ->
end,
erlang:demonitor(Ref),
websocket_close(Socket, WsHandleLoopPid, SocketMode, Code);
- {send, Data} ->
+ {text, Data} ->
SocketMode:send(Socket, encode_frame(Data, 1)),
ws_loop(FrameInfo, Socket, WsHandleLoopPid,
SocketMode);
+ {data, Data} ->
+ SocketMode:send(Socket, encode_frame(Data, 2)),
+ ws_loop(FrameInfo, Socket, WsHandleLoopPid,
+ SocketMode);
{ping, Data} ->
SocketMode:send(Socket, encode_frame(Data, 9)),
ws_loop(FrameInfo, Socket, WsHandleLoopPid,
@@ -409,22 +426,27 @@ websocket_close(Socket, WsHandleLoopPid, SocketMode, _CloseCode) ->
SocketMode:close(Socket).
get_origin() ->
- ejabberd_config:get_option(websocket_origin, ignore).
+ ejabberd_config:get_option(websocket_origin, []).
opt_type(websocket_ping_interval) ->
fun (I) when is_integer(I), I >= 0 -> I end;
opt_type(websocket_timeout) ->
fun (I) when is_integer(I), I > 0 -> I end;
opt_type(websocket_origin) ->
- %% Accept only values conforming to RFC6454 section 7.1
- fun (<<"null">>) -> <<"null">>;
- (null) -> <<"null">>;
- (Origin) ->
- URIs = [_|_] = lists:flatmap(
- fun(<<>>) -> [];
- (URI) -> [misc:try_url(URI)]
- end, re:split(Origin, "\\s")),
- str:join(URIs, <<" ">>)
+ fun Verify(V) when is_binary(V) ->
+ Verify([V]);
+ Verify([]) ->
+ [];
+ Verify([<<"null">> | R]) ->
+ [<<"null">> | Verify(R)];
+ Verify([null | R]) ->
+ [<<"null">> | Verify(R)];
+ Verify([V | R]) when is_binary(V) ->
+ URIs = [_|_] = lists:filtermap(
+ fun(<<>>) -> false;
+ (URI) -> {true, misc:try_url(URI)}
+ end, re:split(V, "\\s+")),
+ [str:join(URIs, <<" ">>) | Verify(R)]
end;
opt_type(_) ->
[websocket_ping_interval, websocket_timeout, websocket_origin].
diff --git a/src/gen_mod.erl b/src/gen_mod.erl
index 4dc972fd9..b972285f5 100644
--- a/src/gen_mod.erl
+++ b/src/gen_mod.erl
@@ -44,7 +44,7 @@
%% Deprecated functions
-export([get_opt/3, get_opt/4, get_module_opt/4, get_module_opt/5,
get_opt_host/3, get_opt_hosts/3, db_type/2, db_type/3,
- ram_db_type/2, ram_db_type/3]).
+ ram_db_type/2, ram_db_type/3, update_module_opts/3]).
-deprecated([{get_opt, 3},
{get_opt, 4},
{get_opt_host, 3},
@@ -305,6 +305,20 @@ store_options(Host, Module, Opts, Order) ->
#ejabberd_module{module_host = {Module, Host},
opts = Opts, order = Order}).
+-spec update_module_opts(binary(), module(), opts()) -> ok | {ok, pid()} | error.
+update_module_opts(Host, Module, NewValues) ->
+ case ets:lookup(ejabberd_modules, {Module, Host}) of
+ [#ejabberd_module{opts = Opts, order = Order}] ->
+ NewOpts = lists:foldl(
+ fun({K, _} = KV, Acc) ->
+ lists:keystore(K, 1, Acc, KV)
+ end, Opts, NewValues),
+ reload_module(Host, Module, NewOpts, Opts, Order);
+ Other ->
+ ?WARNING_MSG("Unable to update module opts: (~p, ~p) -> ~p",
+ [Host, Module, Other])
+ end.
+
maybe_halt_ejabberd() ->
case is_app_running(ejabberd) of
false ->
@@ -568,8 +582,10 @@ validate_opts(Host, Module, Opts0) ->
module_error(ErrTxt);
_:{unknown_option, Opt, KnownOpts} ->
ErrTxt = io_lib:format("Unknown option '~s' of module '~s',"
- " available options are: ~s",
+ " did you mean '~s'?"
+ " Available options are: ~s",
[Opt, Module,
+ misc:best_match(Opt, KnownOpts),
misc:join_atoms(KnownOpts, <<", ">>)]),
module_error(ErrTxt)
end.
@@ -724,11 +740,15 @@ format_module_error(Module, Fun, Arity, Opts, Class, Reason, St) ->
IsCallbackExported = erlang:function_exported(Module, Fun, Arity),
case {Class, Reason} of
{error, undef} when not IsLoaded ->
- io_lib:format("Failed to ~s unknown module ~s: "
+ io_lib:format("Failed to ~s unknown module ~s, "
+ "did you mean ~s? Hint: "
"make sure there is no typo and ~s.beam "
"exists inside either ~s or ~s "
"directory",
- [Fun, Module, Module,
+ [Fun, Module,
+ misc:best_match(
+ Module, ejabberd_config:get_modules()),
+ Module,
filename:dirname(code:which(?MODULE)),
ext_mod:modules_dir()]);
{error, undef} when not IsCallbackExported ->
diff --git a/src/misc.erl b/src/misc.erl
index 73d05ff24..4f683a431 100644
--- a/src/misc.erl
+++ b/src/misc.erl
@@ -39,7 +39,7 @@
css_dir/0, img_dir/0, js_dir/0, msgs_dir/0, sql_dir/0, lua_dir/0,
read_css/1, read_img/1, read_js/1, read_lua/1, try_url/1,
intersection/2, format_val/1, cancel_timer/1, unique_timestamp/0,
- is_mucsub_message/1]).
+ is_mucsub_message/1, best_match/2]).
%% Deprecated functions
-export([decode_base64/1, encode_base64/1]).
@@ -425,6 +425,18 @@ cancel_timer(TRef) when is_reference(TRef) ->
cancel_timer(_) ->
ok.
+-spec best_match(atom(), [atom()]) -> atom().
+best_match(Pattern, []) ->
+ Pattern;
+best_match(Pattern, Opts) ->
+ String = atom_to_list(Pattern),
+ {Ds, _} = lists:mapfoldl(
+ fun(Opt, Cache) ->
+ {Distance, Cache1} = ld(String, atom_to_list(Opt), Cache),
+ {{Distance, Opt}, Cache1}
+ end, #{}, Opts),
+ element(2, lists:min(Ds)).
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
@@ -485,3 +497,21 @@ get_dir(Type) ->
unique_timestamp() ->
{MS, S, _} = erlang:timestamp(),
{MS, S, erlang:unique_integer([positive, monotonic]) rem 1000000}.
+
+%% Levenshtein distance
+-spec ld(string(), string(), map()) -> {non_neg_integer(), map()}.
+ld([] = S, T, Cache) ->
+ {length(T), maps:put({S, T}, length(T), Cache)};
+ld(S, [] = T, Cache) ->
+ {length(S), maps:put({S, T}, length(S), Cache)};
+ld([X|S], [X|T], Cache) ->
+ ld(S, T, Cache);
+ld([_|ST] = S, [_|TT] = T, Cache) ->
+ try {maps:get({S, T}, Cache), Cache}
+ catch _:{badkey, _} ->
+ {L1, C1} = ld(S, TT, Cache),
+ {L2, C2} = ld(ST, T, C1),
+ {L3, C3} = ld(ST, TT, C2),
+ L = 1 + lists:min([L1, L2, L3]),
+ {L, maps:put({S, T}, L, C3)}
+ end.
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
index 1d9582961..b3600e709 100644
--- a/src/mod_http_upload.erl
+++ b/src/mod_http_upload.erl
@@ -975,9 +975,7 @@ remove_user(User, Server) ->
end,
ok.
--spec del_tree(file:filename_all()) -> ok | {error, term()}.
-del_tree(Dir) when is_binary(Dir) ->
- del_tree(binary_to_list(Dir));
+-spec del_tree(file:filename_all()) -> ok | {error, file:posix()}.
del_tree(Dir) ->
try
{ok, Entries} = file:list_dir(Dir),
@@ -988,11 +986,9 @@ del_tree(Dir) ->
false ->
ok = file:delete(Path)
end
- end, [Dir ++ "/" ++ Entry || Entry <- Entries]),
+ end, [filename:join(Dir, Entry) || Entry <- Entries]),
ok = file:del_dir(Dir)
catch
_:{badmatch, {error, Error}} ->
- {error, Error};
- _:Error ->
{error, Error}
end.
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 5e20184fa..ba00d74e5 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -42,7 +42,7 @@
get_room_config/4, set_room_option/3, offline_message/1, export/1,
mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2,
is_empty_for_user/2, is_empty_for_room/3, check_create_room/4,
- process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2]).
+ process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7]).
-include("xmpp.hrl").
-include("logger.hrl").
@@ -71,17 +71,22 @@
#rsm_set{} | undefined, chat | groupchat) ->
{[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
{error, db_failure}.
+-callback select(binary(), jid(), jid(), mam_query:result(),
+ #rsm_set{} | undefined, chat | groupchat,
+ all | only_count | only_messages) ->
+ {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
+ {error, db_failure}.
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].
-callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}.
-callback is_empty_for_user(binary(), binary()) -> boolean().
-callback is_empty_for_room(binary(), binary(), binary()) -> boolean().
-callback select_with_mucsub(binary(), jid(), jid(), mam_query:result(),
- #rsm_set{} | undefined) ->
+ #rsm_set{} | undefined, all | only_count | only_messages) ->
{[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
{error, db_failure}.
--optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/5]).
+-optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7]).
%%%===================================================================
%%% API
@@ -112,7 +117,7 @@ start(Host, Opts) ->
ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
user_send_packet_strip_tag, 500),
ejabberd_hooks:add(offline_message_hook, Host, ?MODULE,
- offline_message, 50),
+ offline_message, 49),
ejabberd_hooks:add(muc_filter_message, Host, ?MODULE,
muc_filter_message, 50),
ejabberd_hooks:add(muc_process_iq, Host, ?MODULE,
@@ -188,7 +193,7 @@ stop(Host) ->
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
user_send_packet_strip_tag, 500),
ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE,
- offline_message, 50),
+ offline_message, 49),
ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE,
muc_filter_message, 50),
ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE,
@@ -1038,9 +1043,12 @@ select_and_send(LServer, Query, RSM, #iq{from = From, to = To} = IQ, MsgType) ->
xmpp:make_error(IQ, Err)
end.
+select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) ->
+ select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, all).
+
select(_LServer, JidRequestor, JidArchive, Query, RSM,
{groupchat, _Role, #state{config = #config{mam = false},
- history = History}} = MsgType) ->
+ history = History}} = MsgType, _Flags) ->
Start = proplists:get_value(start, Query),
End = proplists:get_value('end', Query),
#lqueue{queue = Q} = History,
@@ -1079,21 +1087,20 @@ select(_LServer, JidRequestor, JidArchive, Query, RSM,
_ ->
{Msgs, true, L}
end;
-select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) ->
+select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) ->
case might_expose_jid(Query, MsgType) of
true ->
{[], true, 0};
false ->
case {MsgType, gen_mod:get_module_opt(LServer, ?MODULE, user_mucsub_from_muc_archive)} of
{chat, true} ->
- select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM);
+ select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags);
_ ->
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType)
+ db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags)
end
end.
-select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM) ->
+select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags) ->
MucHosts = mod_muc_admin:find_hosts(LServer),
Mod = gen_mod:db_mod(LServer, ?MODULE),
case proplists:get_value(with, Query) of
@@ -1103,20 +1110,19 @@ select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM) ->
select(LServer, JidRequestor, MucJid, Query, RSM,
{groupchat, member, #state{config = #config{mam = true}}});
_ ->
- Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, chat)
+ db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags)
end;
_ ->
- case erlang:function_exported(Mod, select_with_mucsub, 5) of
+ case erlang:function_exported(Mod, select_with_mucsub, 6) of
true ->
- Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM);
+ Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags);
false ->
- select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM)
+ select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags)
end
end.
-select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM) ->
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- case Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, chat) of
+select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) ->
+ case db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) of
{error, _} = Err ->
Err;
{Entries, All, Count} ->
@@ -1166,6 +1172,15 @@ select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM) ->
end
end.
+db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ case erlang:function_exported(Mod, select, 7) of
+ true ->
+ Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags);
+ _ ->
+ Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType)
+ end.
+
wrap_as_mucsub(Messages, #jid{lserver = LServer} = Requester) ->
ReqBare = jid:remove_resource(Requester),
ReqServer = jid:make(<<>>, LServer, <<>>),
diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl
index 386110817..be87e64da 100644
--- a/src/mod_mam_sql.erl
+++ b/src/mod_mam_sql.erl
@@ -30,8 +30,8 @@
%% API
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
- extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/6, export/1, remove_from_archive/3,
- is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/5]).
+ extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3,
+ is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6]).
-include_lib("stdlib/include/ms_transform.hrl").
-include("xmpp.hrl").
@@ -174,20 +174,20 @@ get_prefs(LUser, LServer) ->
end.
select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
- MAMQuery, RSM, MsgType) ->
+ MAMQuery, RSM, MsgType, Flags) ->
User = case MsgType of
chat -> LUser;
_ -> jid:encode(JidArchive)
end,
{Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none),
- do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery).
+ do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags).
-spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(),
- #rsm_set{} | undefined) ->
+ #rsm_set{} | undefined, all | only_count | only_messages) ->
{[{binary(), non_neg_integer(), xmlel()}], boolean(), integer()} |
{error, db_failure}.
select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
- MAMQuery, RSM) ->
+ MAMQuery, RSM, Flags) ->
Extra = case gen_mod:db_mod(LServer, mod_muc) of
mod_muc_sql ->
subscribers_table;
@@ -204,17 +204,25 @@ select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
[jid:encode(Jid) || {Jid, _} <- SubRooms]
end,
{Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra),
- do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery).
+ do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags).
-do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM, MsgType, Query, CountQuery) ->
+do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM,
+ MsgType, Query, CountQuery, Flags) ->
% TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a
% reasonable limit on how many stanzas may be pushed to a client in one
% request. If a query returns a number of stanzas greater than this limit
% and the client did not specify a limit using RSM then the server should
% return a policy-violation error to the client." We currently don't do this
% for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer.
- case {ejabberd_sql:sql_query(LServer, Query),
- ejabberd_sql:sql_query(LServer, CountQuery)} of
+ QRes = case Flags of
+ all ->
+ {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)};
+ only_messages ->
+ {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}};
+ only_count ->
+ {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)}
+ end,
+ case QRes of
{{selected, _, Res}, {selected, _, [[Count]]}} ->
{Max, Direction, _} = get_max_direction_id(RSM),
{Res1, IsComplete} =
@@ -420,15 +428,21 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
{UserSel, UserWhere} = case ExtraUsernames of
Users when is_list(Users) ->
- EscUsers = [<<"'", (Escape(U))/binary, "'">> || U <- [SUser | Users]],
+ EscUsers = [<<"'", (Escape(U))/binary, "'">> || U <- [User | Users]],
{<<" username,">>,
[<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]};
subscribers_table ->
- SJid = jid:encode({User, LServer, <<>>}),
+ SJid = Escape(jid:encode({User, LServer, <<>>})),
+ RoomName = case ODBCType of
+ sqlite ->
+ <<"room || '@' || host">>;
+ _ ->
+ <<"concat(room, '@', host)">>
+ end,
{<<" username,">>,
[<<" (username = '">>, SUser, <<"'">>,
- <<" or username in (select concat(room, '@', host) ",
- "from muc_room_subscribers where jid='">>, SJid, <<"'">>, HostMatch, <<"))">>]};
+ <<" or username in (select ">>, RoomName,
+ <<" from muc_room_subscribers where jid='">>, SJid, <<"'">>, HostMatch, <<"))">>]};
_ ->
{<<>>, [<<" username='">>, SUser, <<"'">>]}
end,
diff --git a/src/mod_mqtt_ws.erl b/src/mod_mqtt_ws.erl
index 872553445..820b09d62 100644
--- a/src/mod_mqtt_ws.erl
+++ b/src/mod_mqtt_ws.erl
@@ -98,7 +98,7 @@ init([{#ws{ip = IP, http_opts = ListenOpts}, WsPid}]) ->
end.
handle_call({send, Data}, _From, #state{ws_pid = WsPid} = State) ->
- WsPid ! {send, Data},
+ WsPid ! {data, Data},
{reply, ok, State};
handle_call(Request, From, State) ->
?WARNING_MSG("Got unexpected call from ~p: ~p", [From, Request]),
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 6420ced31..76fce1f8e 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -654,31 +654,46 @@ load_permanent_rooms(Host, ServerHost, Access,
HistorySize, RoomShaper, QueueType) ->
RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),
lists:foreach(
- fun(R) ->
- {Room, Host} = R#muc_room.name_host,
- case RMod:find_online_room(ServerHost, Room, Host) of
- error ->
- {ok, Pid} = mod_muc_room:start(Host,
- ServerHost, Access, Room,
- HistorySize, RoomShaper,
- R#muc_room.opts, QueueType),
- RMod:register_online_room(ServerHost, Room, Host, Pid);
- {ok, _} ->
- ok
- end
- end,
- get_rooms(ServerHost, Host)).
+ fun(R) ->
+ {Room, Host} = R#muc_room.name_host,
+ case proplists:get_bool(persistent, R#muc_room.opts) of
+ true ->
+ case RMod:find_online_room(ServerHost, Room, Host) of
+ error ->
+ {ok, Pid} = mod_muc_room:start(Host,
+ ServerHost, Access, Room,
+ HistorySize, RoomShaper,
+ R#muc_room.opts, QueueType),
+ RMod:register_online_room(ServerHost, Room, Host, Pid);
+ {ok, _} ->
+ ok
+ end;
+ _ ->
+ forget_room(ServerHost, Host, Room)
+ end
+ end, get_rooms(ServerHost, Host)).
start_new_room(Host, ServerHost, Access, Room,
HistorySize, RoomShaper, From,
Nick, DefRoomOpts, QueueType) ->
- case restore_room(ServerHost, Host, Room) of
+ Opts = case restore_room(ServerHost, Host, Room) of
+ error ->
+ error;
+ Opts0 ->
+ case proplists:get_bool(persistent, Opts0) of
+ true ->
+ Opts0;
+ _ ->
+ error
+ end
+ end,
+ case Opts of
error ->
?DEBUG("MUC: open new room '~s'~n", [Room]),
mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper,
From, Nick, DefRoomOpts, QueueType);
- Opts ->
+ _ ->
?DEBUG("MUC: restore room '~s'~n", [Room]),
mod_muc_room:start(Host, ServerHost, Access, Room,
HistorySize, RoomShaper, Opts, QueueType)
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index f016f696a..f972a5feb 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -1140,6 +1140,7 @@ close_room_if_temporary_and_empty(StateData1) ->
"and empty",
[jid:encode(StateData1#state.jid)]),
add_to_log(room_existence, destroyed, StateData1),
+ maybe_forget_room(StateData1),
{stop, normal, StateData1};
_ -> {next_state, normal_state, StateData1}
end.
@@ -3486,13 +3487,12 @@ change_config(Config, StateData) ->
store_room(StateData1),
StateData1;
{true, false} ->
- Affiliations = get_affiliations(StateData),
- mod_muc:forget_room(StateData1#state.server_host,
- StateData1#state.host,
- StateData1#state.room),
- StateData1#state{affiliations = Affiliations};
- {false, false} ->
- StateData1
+ Affiliations = get_affiliations(StateData),
+ maybe_forget_room(StateData),
+ StateData1#state{affiliations = Affiliations};
+ _ ->
+ maybe_forget_room(StateData),
+ StateData1
end,
case {(StateData#state.config)#config.members_only,
Config#config.members_only} of
@@ -3822,14 +3822,27 @@ destroy_room(DEl, StateData) ->
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
end, ok, get_users_and_subscribers(StateData)),
- case (StateData#state.config)#config.persistent of
- true ->
- mod_muc:forget_room(StateData#state.server_host,
- StateData#state.host, StateData#state.room);
- false -> ok
- end,
+ maybe_forget_room(StateData),
{result, undefined, stop}.
+maybe_forget_room(StateData) ->
+ Forget = case (StateData#state.config)#config.persistent of
+ true ->
+ true;
+ _ ->
+ Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc),
+ erlang:function_exported(Mod, get_subscribed_rooms, 3)
+ end,
+ case Forget of
+ true ->
+ mod_muc:forget_room(StateData#state.server_host,
+ StateData#state.host,
+ StateData#state.room),
+ StateData;
+ _ ->
+ StateData
+ end.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Disco
@@ -4089,13 +4102,23 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang,
StateData) ->
FAffiliation = get_affiliation(From, StateData),
FRole = get_role(From, StateData),
- if FRole == moderator; FAffiliation == owner; FAffiliation == admin ->
+ IsModerator = FRole == moderator orelse FAffiliation == owner orelse
+ FAffiliation == admin,
+ case IsModerator orelse is_subscriber(From, StateData) of
+ true ->
+ ShowJid = IsModerator orelse
+ (StateData#state.config)#config.anonymous == false,
Subs = maps:fold(
- fun(_, #subscriber{jid = J, nodes = Nodes}, Acc) ->
- [#muc_subscription{jid = J, events = Nodes}|Acc]
+ fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) ->
+ case ShowJid of
+ true ->
+ [#muc_subscription{jid = J, events = Nodes}|Acc];
+ _ ->
+ [#muc_subscription{nick = N, events = Nodes}|Acc]
+ end
end, [], StateData#state.subscribers),
{result, #muc_subscriptions{list = Subs}, StateData};
- true ->
+ _ ->
Txt = <<"Moderator privileges required">>,
{error, xmpp:err_forbidden(Txt, Lang)}
end;
@@ -4349,7 +4372,20 @@ element_size(El) ->
store_room(StateData) ->
store_room(StateData, []).
store_room(StateData, ChangesHints) ->
- if (StateData#state.config)#config.persistent ->
+ % Let store persistent rooms or on those backends that have get_subscribed_rooms
+ ShouldStore = case (StateData#state.config)#config.persistent of
+ true ->
+ true;
+ _ ->
+ case ChangesHints of
+ [] ->
+ false;
+ _ ->
+ Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc),
+ erlang:function_exported(Mod, get_subscribed_rooms, 3)
+ end
+ end,
+ if ShouldStore ->
mod_muc:store_room(StateData#state.server_host,
StateData#state.host, StateData#state.room,
make_opts(StateData),
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 4a1a4cca2..f935bd1b6 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -61,7 +61,8 @@
c2s_copy_session/2,
webadmin_page/3,
webadmin_user/4,
- webadmin_user_parse_query/5]).
+ webadmin_user_parse_query/5,
+ user_unset_presence/4]).
-export([mod_opt_type/1, mod_options/1, depends/2]).
@@ -131,6 +132,8 @@ start(Host, Opts) ->
?MODULE, webadmin_user, 50),
ejabberd_hooks:add(webadmin_user_parse_query, Host,
?MODULE, webadmin_user_parse_query, 50),
+ ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE,
+ user_unset_presence, 50),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE,
?MODULE, handle_offline_query).
@@ -153,6 +156,8 @@ stop(Host) ->
?MODULE, webadmin_user, 50),
ejabberd_hooks:delete(webadmin_user_parse_query, Host,
?MODULE, webadmin_user_parse_query, 50),
+ ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE,
+ user_unset_presence, 50),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE).
reload(Host, NewOpts, OldOpts) ->
@@ -165,17 +170,37 @@ reload(Host, NewOpts, OldOpts) ->
end.
-spec store_offline_msg(#offline_msg{}) -> ok | {error, full | any()}.
-store_offline_msg(#offline_msg{us = {User, Server}} = Msg) ->
- Mod = gen_mod:db_mod(Server, ?MODULE),
- case get_max_user_messages(User, Server) of
- infinity ->
+store_offline_msg(#offline_msg{us = {User, Server}, packet = Pkt} = Msg) ->
+ {UseMam, ActivityMarker} = case use_mam_for_user(User, Server) of
+ true ->
+ {true, xmpp:get_meta(Pkt, activity_marker, false)};
+ _ ->
+ {false, false}
+ end,
+ case UseMam andalso (not ActivityMarker) andalso
+ xmpp:get_meta(Pkt, mam_archived, false) of
+ true ->
+ case xmpp:get_meta(Pkt, first_from_queue, false) of
+ true ->
+ store_last_activity_marker(User, Server, xmpp:get_meta(Pkt, stanza_id));
+ _ ->
+ ok
+ end;
+ false when ActivityMarker ->
+ Mod = gen_mod:db_mod(Server, ?MODULE),
Mod:store_message(Msg);
- Limit ->
- Num = count_offline_messages(User, Server),
- if Num < Limit ->
+ false ->
+ Mod = gen_mod:db_mod(Server, ?MODULE),
+ case get_max_user_messages(User, Server) of
+ infinity ->
Mod:store_message(Msg);
- true ->
- {error, full}
+ Limit ->
+ Num = count_offline_messages(User, Server),
+ if Num < Limit ->
+ Mod:store_message(Msg);
+ true ->
+ {error, full}
+ end
end
end.
@@ -298,34 +323,44 @@ handle_offline_query(#iq{lang = Lang} = IQ) ->
-spec handle_offline_items_view(jid(), [offline_item()]) -> boolean().
handle_offline_items_view(JID, Items) ->
{U, S, R} = jid:tolower(JID),
- lists:foldl(
- fun(#offline_item{node = Node, action = view}, Acc) ->
- case fetch_msg_by_node(JID, Node) of
- {ok, OfflineMsg} ->
- case offline_msg_to_route(S, OfflineMsg) of
- {route, El} ->
- NewEl = set_offline_tag(El, Node),
- case ejabberd_sm:get_session_pid(U, S, R) of
- Pid when is_pid(Pid) ->
- Pid ! {route, NewEl};
- none ->
- ok
- end,
- Acc or true;
- error ->
- Acc or false
- end;
- error ->
- Acc or false
- end
- end, false, Items).
+ case use_mam_for_user(U, S) of
+ true ->
+ false;
+ _ ->
+ lists:foldl(
+ fun(#offline_item{node = Node, action = view}, Acc) ->
+ case fetch_msg_by_node(JID, Node) of
+ {ok, OfflineMsg} ->
+ case offline_msg_to_route(S, OfflineMsg) of
+ {route, El} ->
+ NewEl = set_offline_tag(El, Node),
+ case ejabberd_sm:get_session_pid(U, S, R) of
+ Pid when is_pid(Pid) ->
+ Pid ! {route, NewEl};
+ none ->
+ ok
+ end,
+ Acc or true;
+ error ->
+ Acc or false
+ end;
+ error ->
+ Acc or false
+ end
+ end, false, Items) end.
-spec handle_offline_items_remove(jid(), [offline_item()]) -> boolean().
handle_offline_items_remove(JID, Items) ->
- lists:foldl(
- fun(#offline_item{node = Node, action = remove}, Acc) ->
- Acc or remove_msg_by_node(JID, Node)
- end, false, Items).
+ {U, S, _R} = jid:tolower(JID),
+ case use_mam_for_user(U, S) of
+ true ->
+ false;
+ _ ->
+ lists:foldl(
+ fun(#offline_item{node = Node, action = remove}, Acc) ->
+ Acc or remove_msg_by_node(JID, Node)
+ end, false, Items)
+ end.
-spec set_offline_tag(message(), binary()) -> message().
set_offline_tag(Msg, Node) ->
@@ -334,11 +369,11 @@ set_offline_tag(Msg, Node) ->
-spec handle_offline_fetch(jid()) -> ok.
handle_offline_fetch(#jid{luser = U, lserver = S} = JID) ->
ejabberd_sm:route(JID, {resend_offline, false}),
- lists:foreach(
- fun({Node, El}) ->
- El1 = set_offline_tag(El, Node),
- ejabberd_router:route(El1)
- end, read_messages(U, S)).
+ lists:foreach(
+ fun({Node, El}) ->
+ El1 = set_offline_tag(El, Node),
+ ejabberd_router:route(El1)
+ end, read_messages(U, S)).
-spec fetch_msg_by_node(jid(), binary()) -> error | {ok, #offline_msg{}}.
fetch_msg_by_node(To, Seq) ->
@@ -508,15 +543,26 @@ c2s_self_presence(Acc) ->
-spec route_offline_messages(c2s_state()) -> ok.
route_offline_messages(#{jid := #jid{luser = LUser, lserver = LServer}} = State) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
- case Mod:pop_messages(LUser, LServer) of
- {ok, OffMsgs} ->
- lists:foreach(
- fun(OffMsg) ->
- route_offline_message(State, OffMsg)
- end, OffMsgs);
- _ ->
- ok
- end.
+ Msgs = case Mod:pop_messages(LUser, LServer) of
+ {ok, OffMsgs} ->
+ case use_mam_for_user(LUser, LServer) of
+ true ->
+ lists:map(
+ fun({_, #message{from = From, to = To} = Msg}) ->
+ #offline_msg{from = From, to = To,
+ us = {LUser, LServer},
+ packet = Msg}
+ end, read_mam_messages(LUser, LServer, OffMsgs));
+ _ ->
+ OffMsgs
+ end;
+ _ ->
+ []
+ end,
+ lists:foreach(
+ fun(OffMsg) ->
+ route_offline_message(State, OffMsg)
+ end, Msgs).
-spec route_offline_message(c2s_state(), #offline_msg{}) -> ok.
route_offline_message(#{lserver := LServer} = State,
@@ -574,6 +620,31 @@ remove_user(User, Server) ->
Mod:remove_user(LUser, LServer),
ok.
+-spec user_unset_presence(binary(), binary(), binary(), binary()) -> any().
+user_unset_presence(User, Server, _Resource, _Status) ->
+ case use_mam_for_user(User, Server) of
+ true ->
+ case ejabberd_sm:get_user_present_resources(User, Server) of
+ [] ->
+ TimeStamp = erlang:system_time(microsecond),
+ store_last_activity_marker(User, Server, TimeStamp);
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end.
+
+store_last_activity_marker(User, Server, Timestamp) ->
+ Jid = jid:make(User, Server, <<>>),
+ Pkt = xmpp:put_meta(#message{id = <<"ActivityMarker">>, type = error},
+ activity_marker, true),
+
+ Msg = #offline_msg{us = {User, Server}, from = Jid, to = Jid,
+ timestamp = misc:usec_to_now(Timestamp),
+ packet = Pkt},
+ store_offline_msg(Msg).
+
%% Helper functions:
-spec check_if_message_should_be_bounced(message()) -> boolean().
@@ -641,25 +712,172 @@ offline_msg_to_route(LServer, #offline_msg{from = From, to = To} = R) ->
-spec read_messages(binary(), binary()) -> [{binary(), message()}].
read_messages(LUser, LServer) ->
+ Res = read_db_messages(LUser, LServer),
+ case use_mam_for_user(LUser, LServer) of
+ true ->
+ read_mam_messages(LUser, LServer, Res);
+ _ ->
+ Res
+ end.
+
+-spec read_db_messages(binary(), binary()) -> [{binary(), message()}].
+read_db_messages(LUser, LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
CodecOpts = ejabberd_config:codec_options(LServer),
lists:flatmap(
- fun({Seq, From, To, TS, El}) ->
- Node = integer_to_binary(Seq),
- try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of
- Pkt ->
- Node = integer_to_binary(Seq),
- Pkt1 = add_delay_info(Pkt, LServer, TS),
- Pkt2 = xmpp:set_from_to(Pkt1, From, To),
- [{Node, Pkt2}]
- catch _:{xmpp_codec, Why} ->
- ?ERROR_MSG("failed to decode packet ~p "
- "of user ~s: ~s",
- [El, jid:encode(To),
- xmpp:format_error(Why)]),
- []
- end
- end, Mod:read_message_headers(LUser, LServer)).
+ fun({Seq, From, To, TS, El}) ->
+ Node = integer_to_binary(Seq),
+ try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of
+ Pkt ->
+ Node = integer_to_binary(Seq),
+ Pkt1 = add_delay_info(Pkt, LServer, TS),
+ Pkt2 = xmpp:set_from_to(Pkt1, From, To),
+ [{Node, Pkt2}]
+ catch _:{xmpp_codec, Why} ->
+ ?ERROR_MSG("failed to decode packet ~p "
+ "of user ~s: ~s",
+ [El, jid:encode(To),
+ xmpp:format_error(Why)]),
+ []
+ end
+ end, Mod:read_message_headers(LUser, LServer)).
+
+-spec parse_marker_messages(binary(), [#offline_msg{} | {any(), message()}]) ->
+ {integer() | none, [message()]}.
+parse_marker_messages(LServer, ReadMsgs) ->
+ {Timestamp, ExtraMsgs} = lists:foldl(
+ fun({_Node, #message{id = <<"ActivityMarker">>,
+ body = [], type = error} = Msg}, {T, E}) ->
+ case xmpp:get_subtag(Msg, #delay{}) of
+ #delay{stamp = Time} ->
+ if T == none orelse T > Time ->
+ {Time, E};
+ true ->
+ {T, E}
+ end
+ end;
+ (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt},
+ {T, E}) ->
+ try xmpp:decode(Pkt) of
+ #message{id = <<"ActivityMarker">>,
+ body = [], type = error} = Msg ->
+ TS2 = case TS of
+ undefined ->
+ case xmpp:get_subtag(Msg, #delay{}) of
+ #delay{stamp = TS0} ->
+ TS0;
+ _ ->
+ erlang:timestamp()
+ end;
+ _ ->
+ TS
+ end,
+ if T == none orelse T > TS2 ->
+ {TS2, E};
+ true ->
+ {T, E}
+ end;
+ Decoded ->
+ Pkt1 = add_delay_info(Decoded, LServer, TS),
+ {T, [xmpp:set_from_to(Pkt1, From, To) | E]}
+ catch _:{xmpp_codec, _Why} ->
+ {T, E}
+ end;
+ ({_Node, Msg}, {T, E}) ->
+ {T, [Msg | E]}
+ end, {none, []}, ReadMsgs),
+ Start = case {Timestamp, ExtraMsgs} of
+ {none, [First|_]} ->
+ case xmpp:get_subtag(First, #delay{}) of
+ #delay{stamp = {Mega, Sec, Micro}} ->
+ {Mega, Sec, Micro+1};
+ _ ->
+ none
+ end;
+ {none, _} ->
+ none;
+ _ ->
+ Timestamp
+ end,
+ {Start, ExtraMsgs}.
+
+-spec read_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) ->
+ [{integer(), message()}].
+read_mam_messages(LUser, LServer, ReadMsgs) ->
+ {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs),
+ AllMsgs = case Start of
+ none ->
+ ExtraMsgs;
+ _ ->
+ MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of
+ Number when is_integer(Number) -> Number - length(ExtraMsgs);
+ infinity -> undefined;
+ _ -> 100 - length(ExtraMsgs)
+ end,
+ JID = jid:make(LUser, LServer, <<>>),
+ {MamMsgs, _, _} = mod_mam:select(LServer, JID, JID,
+ [{start, Start}],
+ #rsm_set{max = MaxOfflineMsgs,
+ before = <<"9999999999999999">>},
+ chat, only_messages),
+ MamMsgs2 = lists:map(
+ fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) ->
+ add_delay_info(MM, LServer, MMT)
+ end, MamMsgs),
+
+ ExtraMsgs ++ MamMsgs2
+ end,
+ AllMsgs2 = lists:sort(
+ fun(A, B) ->
+ DA = case xmpp:get_subtag(A, #stanza_id{}) of
+ #stanza_id{id = IDA} ->
+ IDA;
+ _ -> case xmpp:get_subtag(A, #delay{}) of
+ #delay{stamp = STA} ->
+ integer_to_binary(misc:now_to_usec(STA));
+ _ ->
+ <<"unknown">>
+ end
+ end,
+ DB = case xmpp:get_subtag(B, #stanza_id{}) of
+ #stanza_id{id = IDB} ->
+ IDB;
+ _ -> case xmpp:get_subtag(B, #delay{}) of
+ #delay{stamp = STB} ->
+ integer_to_binary(misc:now_to_usec(STB));
+ _ ->
+ <<"unknown">>
+ end
+ end,
+ DA < DB
+ end, AllMsgs),
+ {AllMsgs3, _} = lists:mapfoldl(
+ fun(Msg, Counter) ->
+ {{Counter, Msg}, Counter + 1}
+ end, 1, AllMsgs2),
+ AllMsgs3.
+
+-spec count_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) ->
+ integer().
+count_mam_messages(LUser, LServer, ReadMsgs) ->
+ {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs),
+ case Start of
+ none ->
+ length(ExtraMsgs);
+ _ ->
+ MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of
+ Number when is_integer(Number) -> Number - length(ExtraMsgs);
+ infinity -> undefined;
+ _ -> 100 - length(ExtraMsgs)
+ end,
+ JID = jid:make(LUser, LServer, <<>>),
+ {_, _, Count} = mod_mam:select(LServer, JID, JID,
+ [{start, Start}],
+ #rsm_set{max = MaxOfflineMsgs,
+ before = <<"9999999999999999">>},
+ chat, only_count),
+ Count + length(ExtraMsgs)
+ end.
format_user_queue(Hdrs) ->
lists:map(
@@ -823,8 +1041,14 @@ webadmin_user_parse_query(Acc, _Action, _User, _Server,
count_offline_messages(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- Mod = gen_mod:db_mod(LServer, ?MODULE),
- Mod:count_messages(LUser, LServer).
+ case use_mam_for_user(User, Server) of
+ true ->
+ Res = read_db_messages(LUser, LServer),
+ count_mam_messages(LUser, LServer, Res);
+ _ ->
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ Mod:count_messages(LUser, LServer)
+ end.
-spec add_delay_info(message(), binary(),
undefined | erlang:timestamp()) -> message().
@@ -873,6 +1097,9 @@ import(LServer, {sql, _}, DBType, <<"spool">>,
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(OffMsg).
+use_mam_for_user(_User, Server) ->
+ gen_mod:get_module_opt(Server, ?MODULE, use_mam_for_storage).
+
mod_opt_type(access_max_user_messages) ->
fun acl:shaper_rules_validator/1;
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
@@ -880,6 +1107,8 @@ mod_opt_type(store_groupchat) ->
fun(V) when is_boolean(V) -> V end;
mod_opt_type(bounce_groupchat) ->
fun(V) when is_boolean(V) -> V end;
+mod_opt_type(use_mam_for_storage) ->
+ fun(V) when is_boolean(V) -> V end;
mod_opt_type(store_empty_body) ->
fun (V) when is_boolean(V) -> V;
(unless_chat_state) -> unless_chat_state
@@ -889,5 +1118,6 @@ mod_options(Host) ->
[{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{access_max_user_messages, max_user_offline_messages},
{store_empty_body, unless_chat_state},
+ {use_mam_for_storage, false},
{bounce_groupchat, false},
{store_groupchat, false}].
diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl
index 1a4308c58..34ce4e53d 100644
--- a/src/mod_stream_mgmt.erl
+++ b/src/mod_stream_mgmt.erl
@@ -591,22 +591,25 @@ route_unacked_stanzas(#{mgmt_state := MgmtState,
end,
?DEBUG("Re-routing ~B unacknowledged stanza(s) to ~s",
[p1_queue:len(Queue), jid:encode(JID)]),
- p1_queue:foreach(
- fun({_, _Time, #presence{from = From}}) ->
- ?DEBUG("Dropping presence stanza from ~s", [jid:encode(From)]);
- ({_, _Time, #iq{} = El}) ->
+ p1_queue:foldl(
+ fun({_, _Time, #presence{from = From}}, Acc) ->
+ ?DEBUG("Dropping presence stanza from ~s", [jid:encode(From)]),
+ Acc;
+ ({_, _Time, #iq{} = El}, Acc) ->
Txt = <<"User session terminated">>,
ejabberd_router:route_error(
- El, xmpp:err_service_unavailable(Txt, Lang));
- ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}) ->
+ El, xmpp:err_service_unavailable(Txt, Lang)),
+ Acc;
+ ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}, Acc) ->
%% XEP-0280 says: "When a receiving server attempts to deliver a
%% forked message, and that message bounces with an error for
%% any reason, the receiving server MUST NOT forward that error
%% back to the original sender." Resending such a stanza could
%% easily lead to unexpected results as well.
?DEBUG("Dropping forwarded message stanza from ~s",
- [jid:encode(From)]);
- ({_, Time, #message{} = Msg}) ->
+ [jid:encode(From)]),
+ Acc;
+ ({_, Time, #message{} = Msg}, Acc) ->
case ejabberd_hooks:run_fold(message_is_archived,
LServer, false,
[State, Msg]) of
@@ -615,17 +618,26 @@ route_unacked_stanzas(#{mgmt_state := MgmtState,
[jid:encode(xmpp:get_from(Msg))]);
false when ResendOnTimeout ->
NewEl = add_resent_delay_info(State, Msg, Time),
- ejabberd_router:route(NewEl);
+ NewEl2 = case Acc of
+ first_resend ->
+ xmpp:put_meta(NewEl, first_from_queue, true);
+ _ ->
+ NewEl
+ end,
+ ejabberd_router:route(NewEl2),
+ false;
false ->
Txt = <<"User session terminated">>,
ejabberd_router:route_error(
- Msg, xmpp:err_service_unavailable(Txt, Lang))
+ Msg, xmpp:err_service_unavailable(Txt, Lang)),
+ Acc
end;
- ({_, _Time, El}) ->
+ ({_, _Time, El}, Acc) ->
%% Raw element of type 'error' resulting from a validation error
%% We cannot pass it to the router, it will generate an error
- ?DEBUG("Do not route raw element from ack queue: ~p", [El])
- end, Queue);
+ ?DEBUG("Do not route raw element from ack queue: ~p", [El]),
+ Acc
+ end, first_resend, Queue);
route_unacked_stanzas(_State) ->
ok.
diff --git a/src/rest.erl b/src/rest.erl
index b7aa88562..b8e3c3f6d 100644
--- a/src/rest.erl
+++ b/src/rest.erl
@@ -29,7 +29,7 @@
-export([start/1, stop/1, get/2, get/3, post/4, delete/2,
put/4, patch/4, request/6, with_retry/4,
- encode_json/1, opt_type/1]).
+ opt_type/1]).
-include("logger.hrl").
@@ -79,6 +79,10 @@ patch(Server, Path, Params, Content) ->
Data = encode_json(Content),
request(Server, patch, Path, Params, ?CONTENT_TYPE, Data).
+request(Server, Method, Path, _Params, _Mime, {error, Error}) ->
+ ejabberd_hooks:run(backend_api_error, Server,
+ [Server, Method, Path, Error]);
+ {error, Error};
request(Server, Method, Path, Params, Mime, Data) ->
{Query, Opts} = case Params of
{_, _} -> Params;
@@ -107,41 +111,28 @@ request(Server, Method, Path, Params, Mime, Data) ->
false -> {ok, Code, JSon}
end
catch
- _:Error ->
- ?ERROR_MSG("HTTP response decode failed:~n"
- "** URI = ~s~n"
- "** Body = ~p~n"
- "** Err = ~p",
- [URI, Body, Error]),
- {error, {invalid_json, Body}}
+ _:Reason ->
+ {error, {invalid_json, Body, Reason}}
end;
{error, Reason} ->
- ?ERROR_MSG("HTTP request failed:~n"
- "** URI = ~s~n"
- "** Err = ~p",
- [URI, Reason]),
{error, {http_error, {error, Reason}}}
catch
exit:Reason ->
- ?ERROR_MSG("HTTP request failed:~n"
- "** URI = ~s~n"
- "** Err = ~p",
- [URI, Reason]),
{error, {http_error, {error, Reason}}}
end,
ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]),
case Result of
- {error, {http_error,{error,timeout}}} ->
+ {error, {http_error, {error, timeout}}} ->
ejabberd_hooks:run(backend_api_timeout, Server,
[Server, Method, Path]);
- {error, {http_error,{error,connect_timeout}}} ->
+ {error, {http_error, {error, connect_timeout}}} ->
ejabberd_hooks:run(backend_api_timeout, Server,
[Server, Method, Path]);
- {error, _} ->
+ {error, Error} ->
ejabberd_hooks:run(backend_api_error, Server,
- [Server, Method, Path]);
- _ ->
- End = os:timestamp(),
+ [Server, Method, Path, Error]);
+ _ ->
+ End = os:timestamp(),
Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms
ejabberd_hooks:run(backend_api_response_time, Server,
[Server, Method, Path, Elapsed])
@@ -164,7 +155,7 @@ encode_json(Content) ->
"** Content = ~p~n"
"** Err = ~p",
[Content, Reason]),
- <<>>;
+ {error, {invalid_payload, Content, Reason}};
Encoded ->
Encoded
end.
diff --git a/test/README-quicktest.md b/test/README-quicktest.md
deleted file mode 100644
index 43c71e86b..000000000
--- a/test/README-quicktest.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Elixir unit tests
-
-## Running Elixir unit tests
-
-You can run Elixir unit tests with command:
-
-make quicktest
-
-You need to have ejabberd compile with Elixir and tools enabled.
-
-## Troubleshooting test
-
-To help with troubleshooting Elixir tests, we have added a special macro in ejabberd `logger.hrl` include file: ?EXUNIT_LOG
-
-To use this, in test file:
-
-1. in `setup_all, add:
-
- ```
- Application.start(:logger)
- ```
-
-2. Enable log capture for the test you want to analyse by adding
- `capture_log` tag before test implementation:
-
- ```
- @tag capture_log: true
- ```
-
-In the ejabberd code, if `logger.hrl` is included, you can code adds a
-EXUNIT_LOG macro:
-
- ?EXUNIT_LOG("My debug log:~p ~p", [Arg1, Arg2])
diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl
index 77e6ebdfb..6fa424e76 100644
--- a/test/ejabberd_SUITE.erl
+++ b/test/ejabberd_SUITE.erl
@@ -411,7 +411,7 @@ db_tests(riak) ->
muc_tests:master_slave_cases(),
privacy_tests:master_slave_cases(),
roster_tests:master_slave_cases(),
- offline_tests:master_slave_cases(),
+ offline_tests:master_slave_cases(riak),
vcard_tests:master_slave_cases(),
announce_tests:master_slave_cases(),
carbons_tests:master_slave_cases()];
@@ -439,14 +439,14 @@ db_tests(DB) when DB == mnesia; DB == redis ->
privacy_tests:master_slave_cases(),
pubsub_tests:master_slave_cases(),
roster_tests:master_slave_cases(),
- offline_tests:master_slave_cases(),
+ offline_tests:master_slave_cases(DB),
mam_tests:master_slave_cases(),
vcard_tests:master_slave_cases(),
announce_tests:master_slave_cases(),
carbons_tests:master_slave_cases(),
csi_tests:master_slave_cases(),
push_tests:master_slave_cases()];
-db_tests(_) ->
+db_tests(DB) ->
[{single_user, [sequence],
[test_register,
legacy_auth_tests(),
@@ -468,7 +468,7 @@ db_tests(_) ->
privacy_tests:master_slave_cases(),
pubsub_tests:master_slave_cases(),
roster_tests:master_slave_cases(),
- offline_tests:master_slave_cases(),
+ offline_tests:master_slave_cases(DB),
mam_tests:master_slave_cases(),
vcard_tests:master_slave_cases(),
announce_tests:master_slave_cases(),
diff --git a/test/mam_tests.erl b/test/mam_tests.erl
index 128df2fe8..69afacd2e 100644
--- a/test/mam_tests.erl
+++ b/test/mam_tests.erl
@@ -138,7 +138,10 @@ master_slave_cases() ->
master_slave_test(query_rsm_max),
master_slave_test(query_rsm_after),
master_slave_test(query_rsm_before),
- master_slave_test(muc)]}.
+ master_slave_test(muc),
+ master_slave_test(mucsub),
+ master_slave_test(mucsub_from_muc),
+ master_slave_test(mucsub_from_muc_non_persistent)]}.
archived_and_stanza_id_master(Config) ->
#presence{} = send_recv(Config, #presence{}),
@@ -281,12 +284,124 @@ muc_master(Config) ->
%% And retrieve them via MAM again.
recv_messages_from_room(Config, lists:seq(1, 5)),
put_event(Config, disconnect),
+ muc_tests:leave(Config),
clean(disconnect(Config)).
muc_slave(Config) ->
disconnect = get_event(Config),
clean(disconnect(Config)).
+mucsub_master(Config) ->
+ Room = muc_room_jid(Config),
+ Peer = ?config(peer, Config),
+ wait_for_slave(Config),
+ ct:comment("Joining muc room"),
+ ok = muc_tests:join_new(Config),
+
+ ct:comment("Enabling mam in room"),
+ CfgOpts = muc_tests:get_config(Config),
+ %% Find the MAM field in the config
+ ?match(true, proplists:is_defined(mam, CfgOpts)),
+ ?match(true, proplists:is_defined(allow_subscription, CfgOpts)),
+ %% Enable MAM
+ [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]),
+
+ ct:comment("Subscribing peer to room"),
+ ?send_recv(#iq{to = Room, type = set, sub_els = [
+ #muc_subscribe{jid = Peer, nick = <<"peer">>,
+ events = [?NS_MUCSUB_NODES_MESSAGES]}
+ ]}, #iq{type = result}),
+
+ ct:comment("Sending messages to room"),
+ send_messages_to_room(Config, lists:seq(1, 5)),
+
+ ct:comment("Retrieving messages from room mam storage"),
+ recv_messages_from_room(Config, lists:seq(1, 5)),
+
+ ct:comment("Cleaning up"),
+ put_event(Config, ready),
+ ready = get_event(Config),
+ muc_tests:leave(Config),
+ clean(disconnect(Config)).
+
+mucsub_slave(Config) ->
+ Room = muc_room_jid(Config),
+ MyJID = my_jid(Config),
+ MyJIDBare = jid:remove_resource(MyJID),
+ ok = set_default(Config, always),
+ send_recv(Config, #presence{}),
+ wait_for_master(Config),
+
+ ct:comment("Receiving mucsub events"),
+ lists:foreach(
+ fun(N) ->
+ Body = xmpp:mk_text(integer_to_binary(N)),
+ Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg),
+ PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [
+ #ps_item{} = PS
+ ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS),
+ ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{}))
+ end, lists:seq(1, 5)),
+
+ ct:comment("Retrieving personal mam archive"),
+ QID = p1_rand:get_string(),
+ I = send(Config, #iq{type = set,
+ sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}),
+ lists:foreach(
+ fun(N) ->
+ Body = xmpp:mk_text(integer_to_binary(N)),
+ Forw = ?match(#message{
+ to = MyJID, from = MyJIDBare,
+ sub_els = [#mam_result{
+ xmlns = ?NS_MAM_2,
+ queryid = QID,
+ sub_els = [#forwarded{
+ delay = #delay{}} = Forw]}]},
+ recv_message(Config), Forw),
+ IMsg = ?match(#message{
+ to = MyJIDBare, from = Room} = IMsg, xmpp:get_subtag(Forw, #message{}), IMsg),
+
+ PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [
+ #ps_item{} = PS
+ ]}}, xmpp:get_subtag(IMsg, #ps_event{}), PS),
+ ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{}))
+ end, lists:seq(1, 5)),
+ RSM = ?match(#iq{from = MyJIDBare, id = I, type = result,
+ sub_els = [#mam_fin{xmlns = ?NS_MAM_2,
+ id = QID,
+ rsm = RSM,
+ complete = true}]}, recv_iq(Config), RSM),
+ match_rsm_count(RSM, 5),
+
+ % Wait for master exit
+ ready = get_event(Config),
+ % Unsubscribe yourself
+ ?send_recv(#iq{to = Room, type = set, sub_els = [
+ #muc_unsubscribe{}
+ ]}, #iq{type = result}),
+ put_event(Config, ready),
+ clean(disconnect(Config)).
+
+mucsub_from_muc_master(Config) ->
+ mucsub_master(Config).
+
+mucsub_from_muc_slave(Config) ->
+ Server = ?config(server, Config),
+ gen_mod:update_module_opts(Server, mod_mam, [{user_mucsub_from_muc_archive, true}]),
+ Config2 = mucsub_slave(Config),
+ gen_mod:update_module_opts(Server, mod_mam, [{user_mucsub_from_muc_archive, false}]),
+ Config2.
+
+mucsub_from_muc_non_persistent_master(Config) ->
+ Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}),
+ Config2 = mucsub_from_muc_master(Config1),
+ lists:keydelete(persistent_room, 1, Config2).
+
+mucsub_from_muc_non_persistent_slave(Config) ->
+ Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}),
+ Config2 = mucsub_from_muc_slave(Config1),
+ lists:keydelete(persistent_room, 1, Config2).
+
%%%===================================================================
%%% Internal functions
%%%===================================================================
diff --git a/test/offline_tests.erl b/test/offline_tests.erl
index fbf1fbf74..179a2415c 100644
--- a/test/offline_tests.erl
+++ b/test/offline_tests.erl
@@ -141,10 +141,15 @@ unsupported_iq(Config) ->
%%%===================================================================
%%% Master-slave tests
%%%===================================================================
-master_slave_cases() ->
+master_slave_cases(DB) ->
{offline_master_slave, [sequence],
[master_slave_test(flex),
- master_slave_test(send_all)]}.
+ master_slave_test(send_all)] ++
+ case DB of
+ riak -> [];
+ _ -> [master_slave_test(from_mam)]
+ end
+ }.
flex_master(Config) ->
send_messages(Config, 5),
@@ -174,6 +179,21 @@ flex_slave(Config) ->
0 = get_number(Config),
clean(disconnect(Config)).
+from_mam_master(Config) ->
+ C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}),
+ C3 = send_all_master(C2),
+ lists:keydelete(mam_enabled, 1, C3).
+
+from_mam_slave(Config) ->
+ Server = ?config(server, Config),
+ gen_mod:update_module_opts(Server, mod_offline, [{use_mam_for_storage, true}]),
+ ok = mam_tests:set_default(Config, always),
+ C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}),
+ C3 = send_all_slave(C2),
+ gen_mod:update_module_opts(Server, mod_offline, [{use_mam_for_storage, false}]),
+ C4 = lists:keydelete(mam_enabled, 1, C3),
+ mam_tests:clean(C4).
+
send_all_master(Config) ->
wait_for_slave(Config),
Peer = ?config(peer, Config),
@@ -184,9 +204,11 @@ send_all_master(Config) ->
send(Config, Msg#message{to = BarePeer}),
Acc;
(Msg, Acc) ->
- I = send(Config, Msg#message{to = BarePeer}),
- case xmpp:get_subtag(Msg, #xevent{}) of
- #xevent{offline = true, id = undefined} ->
+ I = send(Config, Msg#message{to = BarePeer}),
+ case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of
+ {#offline{}, _} ->
+ ok;
+ {_, #xevent{offline = true, id = undefined}} ->
ct:comment("Receiving event-reply for:~n~s",
[xmpp:pp(Msg)]),
#message{} = Reply = recv_message(Config),
@@ -210,6 +232,8 @@ send_all_master(Config) ->
send_all_slave(Config) ->
ServerJID = server_jid(Config),
Peer = ?config(peer, Config),
+ #presence{} = send_recv(Config, #presence{}),
+ send(Config, #presence{type = unavailable}),
wait_for_master(Config),
peer_down = get_event(Config),
#presence{} = send_recv(Config, #presence{}),
@@ -298,7 +322,7 @@ get_nodes(Config) ->
MyBareJID = jid:remove_resource(MyJID),
Peer = ?config(peer, Config),
Peer_s = jid:encode(Peer),
- ct:comment("Getting headers"),
+ ct:comment("Getting headers"),
#iq{type = result,
sub_els = [#disco_items{
node = ?NS_FLEX_OFFLINE,
@@ -410,12 +434,14 @@ message_iterator(Config) ->
Body <- [[], xmpp:mk_text(<<"body">>)],
Subject <- [[], xmpp:mk_text(<<"subject">>)],
Els <- AllEls],
+ MamEnabled = ?config(mam_enabled, Config) == true,
lists:partition(
fun(#message{type = error}) -> true;
(#message{type = groupchat}) -> false;
- (#message{sub_els = [#offline{}|_]}) -> false;
- (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined -> false;
- (#message{sub_els = [#xevent{id = I}]}) when I /= undefined -> false;
+ (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true;
+ (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false;
+ (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined, not MamEnabled -> false;
+ (#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;
(#message{body = [], subject = []}) -> false;
diff --git a/test/suite.hrl b/test/suite.hrl
index b2515d8ce..b48932848 100644
--- a/test/suite.hrl
+++ b/test/suite.hrl
@@ -12,7 +12,7 @@
-define(recv1(P1),
P1 = (fun() ->
- V = recv(Config),
+ V = suite:recv(Config),
case V of
P1 -> V;
_ -> suite:match_failure([V], [??P1])
@@ -21,7 +21,7 @@
-define(recv2(P1, P2),
(fun() ->
- case {R1 = recv(Config), R2 = recv(Config)} of
+ case {R1 = suite:recv(Config), R2 = suite:recv(Config)} of
{P1, P2} -> {R1, R2};
{P2, P1} -> {R2, R1};
{P1, V1} -> suite:match_failure([V1], [P2]);
@@ -34,7 +34,7 @@
-define(recv3(P1, P2, P3),
(fun() ->
- case R3 = recv(Config) of
+ case R3 = suite:recv(Config) of
P1 -> insert(R3, 1, ?recv2(P2, P3));
P2 -> insert(R3, 2, ?recv2(P1, P3));
P3 -> insert(R3, 3, ?recv2(P1, P2));
@@ -44,7 +44,7 @@
-define(recv4(P1, P2, P3, P4),
(fun() ->
- case R4 = recv(Config) of
+ case R4 = suite:recv(Config) of
P1 -> insert(R4, 1, ?recv3(P2, P3, P4));
P2 -> insert(R4, 2, ?recv3(P1, P3, P4));
P3 -> insert(R4, 3, ?recv3(P1, P2, P4));
@@ -55,7 +55,7 @@
-define(recv5(P1, P2, P3, P4, P5),
(fun() ->
- case R5 = recv(Config) of
+ case R5 = suite:recv(Config) of
P1 -> insert(R5, 1, ?recv4(P2, P3, P4, P5));
P2 -> insert(R5, 2, ?recv4(P1, P3, P4, P5));
P3 -> insert(R5, 3, ?recv4(P1, P2, P4, P5));
@@ -75,6 +75,19 @@
end
end)()).
+-define(match(Pattern, Result, PatternRes),
+ (fun() ->
+ case Result of
+ Pattern ->
+ PatternRes;
+ Mismatch ->
+ suite:match_failure([Mismatch], [??Pattern])
+ end
+ end)()).
+
+-define(send_recv(Send, Recv),
+ ?match(Recv, suite:send_recv(Config, Send))).
+
-define(COMMON_VHOST, <<"localhost">>).
-define(MNESIA_VHOST, <<"mnesia.localhost">>).
-define(REDIS_VHOST, <<"redis.localhost">>).