Skip to content

Commit

Permalink
Merge pull request #3688 from esl/graphql/offline
Browse files Browse the repository at this point in the history
Mod offline graphql
  • Loading branch information
JanuszJakubiec authored Jun 22, 2022
2 parents 7ff9d24 + dd97292 commit 8f8f488
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 23 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
{suites, "tests", graphql_last_SUITE}.
{suites, "tests", graphql_muc_SUITE}.
{suites, "tests", graphql_muc_light_SUITE}.
{suites, "tests", graphql_offline_SUITE}.
{suites, "tests", graphql_private_SUITE}.
{suites, "tests", graphql_roster_SUITE}.
{suites, "tests", graphql_session_SUITE}.
Expand Down
1 change: 1 addition & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
{suites, "tests", graphql_session_SUITE}.
{suites, "tests", graphql_stanza_SUITE}.
{suites, "tests", graphql_vcard_SUITE}.
{suites, "tests", graphql_offline_SUITE}.
{suites, "tests", graphql_http_upload_SUITE}.
{suites, "tests", graphql_metric_SUITE}.

Expand Down
182 changes: 182 additions & 0 deletions big_tests/tests/graphql_offline_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
-module(graphql_offline_SUITE).

-compile([export_all, nowarn_export_all]).

-import(distributed_helper, [require_rpc_nodes/1]).
-import(domain_helper, [host_type/0, domain/0]).
-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1]).
-import(config_parser_helper, [mod_config/2]).
-import(mongooseimctl_helper, [mongooseimctl/3, rpc_call/3]).

-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("exml/include/exml.hrl").
-include_lib("escalus/include/escalus.hrl").
-include("../../include/mod_roster.hrl").

-record(offline_msg, {us, timestamp, expire, from, to, packet, permanent_fields = []}).

suite() ->
require_rpc_nodes([mim]) ++ escalus:suite().

all() ->
[{group, admin_offline},
{group, admin_offline_not_configured}].

groups() ->
[{admin_offline, [], admin_offline_handler()},
{admin_offline_not_configured, [], admin_offline_not_configured_handler()}].

admin_offline_handler() ->
[admin_delete_expired_messages_test,
admin_delete_old_messages_test,
admin_delete_expired_messages2_test,
admin_delete_old_messages2_test,
admin_delete_expired_messages_no_domain_test,
admin_delete_old_messages_no_domain_test].

admin_offline_not_configured_handler() ->
[admin_delete_expired_messages_offline_not_configured_test,
admin_delete_old_messages_offline_not_configured_test].

init_per_suite(Config) ->
Config1 = dynamic_modules:save_modules(host_type(), Config),
escalus:init_per_suite(Config1).

-spec create_config(atom()) -> [{mod_offline, gen_mod:module_opts()}].
create_config(riak) ->
[{mod_offline, mod_config(mod_offline, #{backend => riak,
riak => #{bucket_type => <<"offline">>}})}];
create_config(Backend) ->
[{mod_offline, mod_config(mod_offline, #{backend => Backend})}].


end_per_suite(Config) ->
dynamic_modules:restore_modules(Config),
escalus:end_per_suite(Config).

init_per_group(admin_offline, Config) ->
HostType = host_type(),
Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType),
ModConfig = create_config(Backend),
dynamic_modules:ensure_modules(HostType, ModConfig),
Config1 = [{backend, Backend} | escalus:init_per_suite(Config)],
graphql_helper:init_admin_handler(Config1);
init_per_group(admin_offline_not_configured, Config) ->
dynamic_modules:ensure_modules(host_type(), [{mod_offline, stopped}]),
graphql_helper:init_admin_handler(Config).

end_per_group(_, _Config) ->
escalus_fresh:clean().

init_per_testcase(CaseName, Config) ->
escalus:init_per_testcase(CaseName, Config).

end_per_testcase(CaseName, Config) ->
escalus:end_per_testcase(CaseName, Config).

% Admin test cases

admin_delete_expired_messages_test(Config) ->
Vars = #{<<"domain">> => domain()},
GraphQlRequest = admin_delete_expired_messages_mutation(Config, Vars),
Message = ok_result(<<"offline">>, <<"deleteExpiredMessages">>, GraphQlRequest),
?assertEqual(<<"Removed 0 messages">>, Message).

admin_delete_old_messages_test(Config) ->
Vars = #{<<"domain">> => domain(), <<"days">> => 2},
GraphQlRequest = admin_delete_old_messages_mutation(Config, Vars),
Message = ok_result(<<"offline">>, <<"deleteOldMessages">>, GraphQlRequest),
?assertEqual(<<"Removed 0 messages">>, Message).

admin_delete_expired_messages2_test(Config) ->
escalus:fresh_story_with_config(Config, [{mike, 1}, {kate, 1}], fun admin_delete_expired_messages2_test/3).

admin_delete_expired_messages2_test(Config, JidMike, JidKate) ->
generate_message(JidMike, JidKate, 10, 2),
generate_message(JidMike, JidKate, 10, 2),
Vars = #{<<"domain">> => domain()},
GraphQlRequest = admin_delete_expired_messages_mutation(Config, Vars),
Message = ok_result(<<"offline">>, <<"deleteExpiredMessages">>, GraphQlRequest),
?assertEqual(<<"Removed 2 messages">>, Message).

admin_delete_old_messages2_test(Config) ->
escalus:fresh_story_with_config(Config, [{mike, 1}, {kate, 1}], fun admin_delete_old_messages2_test/3).

admin_delete_old_messages2_test(Config, JidMike, JidKate) ->
generate_message(JidMike, JidKate, 2, 10),
generate_message(JidMike, JidKate, 2, 10),
Vars = #{<<"domain">> => domain(), <<"days">> => 2},
GraphQlRequest = admin_delete_old_messages_mutation(Config, Vars),
Message = ok_result(<<"offline">>, <<"deleteOldMessages">>, GraphQlRequest),
?assertEqual(<<"Removed 2 messages">>, Message).

admin_delete_expired_messages_no_domain_test(Config) ->
Vars = #{<<"domain">> => <<"AAAA">>},
GraphQlRequest = admin_delete_expired_messages_mutation(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"domain_not_found">>, ParsedResult).

admin_delete_old_messages_no_domain_test(Config) ->
Vars = #{<<"domain">> => <<"AAAA">>, <<"days">> => 2},
GraphQlRequest = admin_delete_old_messages_mutation(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"domain_not_found">>, ParsedResult).

admin_delete_expired_messages_offline_not_configured_test(Config) ->
Vars = #{<<"domain">> => domain()},
GraphQlRequest = admin_delete_expired_messages_mutation(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"module_not_loaded_error">>, ParsedResult).

admin_delete_old_messages_offline_not_configured_test(Config) ->
Vars = #{<<"domain">> => domain(), <<"days">> => 2},
GraphQlRequest = admin_delete_old_messages_mutation(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"module_not_loaded_error">>, ParsedResult).

% Helpers

admin_delete_expired_messages_mutation(Config, Vars) ->
Mutation = <<"mutation M1($domain: String!)
{offline{deleteExpiredMessages(domain: $domain)}}">>,
admin_send_mutation(Config, Vars, Mutation).

admin_delete_old_messages_mutation(Config, Vars) ->
Mutation = <<"mutation M1($domain: String!, $days: Int!)
{offline{deleteOldMessages(domain: $domain, days: $days)}}">>,
admin_send_mutation(Config, Vars, Mutation).

admin_send_mutation(Config, Vars, Mutation) ->
Body = #{query => Mutation, operationName => <<"M1">>, variables => Vars},
execute_auth(Body, Config).

error_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"errors">> := [Data]}}) ->
maps:get(What2, maps:get(What1, Data)).

ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) ->
maps:get(What2, maps:get(What1, Data)).

generate_message(JidMike, JidKate, TimestampDaysAgo, TimestampExpiringDaysAgo) ->
JidRecordMike = jid:from_binary(user_to_bin(JidMike)),
JidRecordKate = jid:from_binary(user_to_bin(JidKate)),
Domain = domain(),
Msg1 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "Rolling stones"),
OldTimestamp = fallback_timestamp(TimestampDaysAgo, os:system_time(microsecond)),
ExpirationTime = fallback_timestamp(TimestampExpiringDaysAgo, os:system_time(microsecond)),
OfflineOld = generate_offline_expired_message(JidRecordMike,
JidRecordKate, Msg1,
OldTimestamp,
ExpirationTime),
{LUser, LServer} = jid:to_lus(JidRecordKate),
rpc_call(mod_offline_backend, write_messages, [host_type(), LUser, LServer, [OfflineOld]]).

generate_offline_expired_message(From, To, Msg, TimeStamp, ExpirationTime) ->
{LUser, LServer} = jid:to_lus(To),
#offline_msg{us = {LUser, LServer}, timestamp = TimeStamp,
expire = ExpirationTime, from = From, to = To, packet = Msg}.

fallback_timestamp(HowManyDays, TS_MicroSeconds) ->
HowManySeconds = HowManyDays * 86400,
HowManyMicroSeconds = erlang:convert_time_unit(HowManySeconds, second, microsecond),
TS_MicroSeconds - HowManyMicroSeconds.
2 changes: 2 additions & 0 deletions priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ type AdminMutation @protected{
private: PrivateAdminMutation
"Http upload"
httpUpload: HttpUploadAdminMutation
"Offline deleting old messages"
offline: OfflineAdminMutation
}
11 changes: 11 additions & 0 deletions priv/graphql/schemas/admin/offline.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Allow admin to delete offline messages from specified domain
"""
type OfflineAdminMutation @protected{
"Allow admin to delete offline messages whose date has expired"
deleteExpiredMessages(domain: String!): String
@protected(type: DOMAIN, args: ["domain"])
"Allow the admin to delete messages at least as old as the number of days specified in the parameter"
deleteOldMessages(domain: String!, days: Int!): String
@protected(type: DOMAIN, args: ["domain"])
}
31 changes: 8 additions & 23 deletions src/ejabberd_admin.erl
Original file line number Diff line number Diff line change
Expand Up @@ -414,34 +414,19 @@ get_loglevel() ->
%%%

-spec delete_expired_messages(jid:lserver()) -> {ok, iolist()} | {error, iolist()}.
delete_expired_messages(LServer) ->
case mongoose_domain_api:get_domain_host_type(LServer) of
{ok, HostType} ->
case mod_offline:remove_expired_messages(HostType, LServer) of
{ok, C} ->
{ok, io_lib:format("Removed ~p messages", [C])};
{error, Reason} ->
{error, io_lib:format("Can't delete expired messages: ~n~p", [Reason])}
end;
{error, not_found} ->
{error, "Unknown domain"}
delete_expired_messages(Domain) ->
case mod_offline_api:delete_expired_messages(Domain) of
{ok, _} = Result -> Result;
{_, Message} -> {error, Message}
end.

-spec delete_old_messages(jid:lserver(), Days :: integer()) -> {ok, iolist()} | {error, iolist()}.
delete_old_messages(LServer, Days) ->
case mongoose_domain_api:get_domain_host_type(LServer) of
{ok, HostType} ->
case mod_offline:remove_old_messages(HostType, LServer, Days) of
{ok, C} ->
{ok, io_lib:format("Removed ~p messages", [C])};
{error, Reason} ->
{error, io_lib:format("Can't remove old messages: ~n~p", [Reason])}
end;
{error, not_found} ->
{error, "Unknown domain"}
delete_old_messages(Domain, Days) ->
case mod_offline_api:delete_old_messages(Domain, Days) of
{ok, _} = Result -> Result;
{_, Message} -> {error, Message}
end.


%%%
%%% Mnesia management
%%%
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/admin/mongoose_graphql_admin_mutation.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ execute(_Ctx, _Obj, <<"httpUpload">>, _Args) ->
{ok, httpUpload};
execute(_Ctx, _Obj, <<"muc_light">>, _Args) ->
{ok, muc_light};
execute(_Ctx, _Obj, <<"offline">>, _Args) ->
{ok, offline};
execute(_Ctx, _Obj, <<"private">>, _Args) ->
{ok, private};
execute(_Ctx, _Obj, <<"roster">>, _Args) ->
Expand Down
24 changes: 24 additions & 0 deletions src/graphql/admin/mongoose_graphql_offline_admin_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-module(mongoose_graphql_offline_admin_mutation).
-behaviour(mongoose_graphql).

-export([execute/4]).

-import(mongoose_graphql_helper, [make_error/2]).

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").
-include("mongoose.hrl").
-include("jlib.hrl").

execute(_Ctx, offline, <<"deleteExpiredMessages">>, #{<<"domain">> := Domain} = Input) ->
case mod_offline_api:delete_expired_messages(Domain) of
{ok, _} = Result -> Result;
Error -> make_error(Error, Input)
end;

execute(_Ctx, offline, <<"deleteOldMessages">>, #{<<"domain">> := Domain, <<"days">> := Days} = Input) ->
case mod_offline_api:delete_old_messages(Domain, Days) of
{ok, _} = Result -> Result;
Error -> make_error(Error, Input)
end.
1 change: 1 addition & 0 deletions src/graphql/mongoose_graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ admin_mapping_rules() ->
'MUCAdminQuery' => mongoose_graphql_muc_admin_query,
'MUCLightAdminMutation' => mongoose_graphql_muc_light_admin_mutation,
'MUCLightAdminQuery' => mongoose_graphql_muc_light_admin_query,
'OfflineAdminMutation' => mongoose_graphql_offline_admin_mutation,
'PrivateAdminMutation' => mongoose_graphql_private_admin_mutation,
'PrivateAdminQuery' => mongoose_graphql_private_admin_query,
'RosterAdminQuery' => mongoose_graphql_roster_admin_query,
Expand Down
42 changes: 42 additions & 0 deletions src/offline/mod_offline_api.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-module(mod_offline_api).

-export([delete_expired_messages/1, delete_old_messages/2]).

-spec delete_expired_messages(jid:lserver()) ->
{ok | domain_not_found | server_error | module_not_loaded_error, iolist()}.
delete_expired_messages(Domain) ->
call_for_loaded_module(Domain, fun remove_expired_messages/2, {Domain}).

-spec delete_old_messages(jid:lserver(), Days :: integer()) ->
{ok | domain_not_found | server_error | module_not_loaded_error, iolist()}.
delete_old_messages(Domain, Days) ->
call_for_loaded_module(Domain, fun remove_old_messages/2, {Domain, Days}).

call_for_loaded_module(Domain, Function, Args) ->
case mongoose_domain_api:get_domain_host_type(Domain) of
{ok, HostType} ->
case gen_mod:is_loaded(HostType, mod_offline) of
true ->
Function(Args, HostType);
false ->
{module_not_loaded_error, "mod_offline is not loaded for this host"}
end;
{error, not_found} ->
{domain_not_found, "Unknown domain"}
end.

remove_old_messages({Domain, Days}, HostType) ->
case mod_offline:remove_old_messages(HostType, Domain, Days) of
{ok, C} ->
{ok, io_lib:format("Removed ~p messages", [C])};
{error, Reason} ->
{server_error, io_lib:format("Can't remove old messages: ~n~p", [Reason])}
end.

remove_expired_messages({Domain}, HostType) ->
case mod_offline:remove_expired_messages(HostType, Domain) of
{ok, C} ->
{ok, io_lib:format("Removed ~p messages", [C])};
{error, Reason} ->
{server_error, io_lib:format("Can't remove old messages: ~n~p", [Reason])}
end.

0 comments on commit 8f8f488

Please sign in to comment.