diff options
-rw-r--r-- | Makefile.in | 3 | ||||
-rw-r--r-- | ejabberd.yml.example | 33 | ||||
-rw-r--r-- | rebar.config | 2 | ||||
-rw-r--r-- | src/ejabberd_config.erl | 7 | ||||
-rw-r--r-- | src/ejabberd_ctl.erl | 11 | ||||
-rw-r--r-- | src/ejabberd_http.erl | 44 | ||||
-rw-r--r-- | src/ejabberd_http_ws.erl | 12 | ||||
-rw-r--r-- | src/ejabberd_websocket.erl | 98 | ||||
-rw-r--r-- | src/gen_mod.erl | 28 | ||||
-rw-r--r-- | src/misc.erl | 32 | ||||
-rw-r--r-- | src/mod_http_upload.erl | 8 | ||||
-rw-r--r-- | src/mod_mam.erl | 51 | ||||
-rw-r--r-- | src/mod_mam_sql.erl | 42 | ||||
-rw-r--r-- | src/mod_mqtt_ws.erl | 2 | ||||
-rw-r--r-- | src/mod_muc.erl | 47 | ||||
-rw-r--r-- | src/mod_muc_room.erl | 72 | ||||
-rw-r--r-- | src/mod_offline.erl | 364 | ||||
-rw-r--r-- | src/mod_stream_mgmt.erl | 38 | ||||
-rw-r--r-- | src/rest.erl | 37 | ||||
-rw-r--r-- | test/README-quicktest.md | 33 | ||||
-rw-r--r-- | test/ejabberd_SUITE.erl | 8 | ||||
-rw-r--r-- | test/mam_tests.erl | 117 | ||||
-rw-r--r-- | test/offline_tests.erl | 44 | ||||
-rw-r--r-- | test/suite.hrl | 23 |
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">>). |