mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(push): stop pushes for read, silent and muted messages (#3033)
This commit is contained in:
1 parent
9908518f5b
commit
b30ea361d3
45 files changed
+3393
-268
No files matched your search
@@ -28,12 +28,18 @@ do_sync_user_guild_settings(#{
|
||||
<<"user_guild_settings">> := Settings
|
||||
}) ->
|
||||
UserId = validation:snowflake_or_throw(<<"user_id">>, UBin),
|
||||
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GBin),
|
||||
GuildId = settings_guild_id(GBin),
|
||||
fanout_local_cache_mutation(push, sync_user_guild_settings_local, [
|
||||
UserId, GuildId, Settings
|
||||
]),
|
||||
true.
|
||||
|
||||
-spec settings_guild_id(term()) -> non_neg_integer().
|
||||
settings_guild_id(<<"0">>) ->
|
||||
0;
|
||||
settings_guild_id(GBin) ->
|
||||
validation:snowflake_or_throw(<<"guild_id">>, GBin).
|
||||
|
||||
-spec do_sync_user_blocked_ids(map()) -> true.
|
||||
do_sync_user_blocked_ids(#{<<"user_id">> := UBin, <<"blocked_user_ids">> := BlockedRaw}) ->
|
||||
UserId = validation:snowflake_or_throw(<<"user_id">>, UBin),
|
||||
@@ -137,6 +143,47 @@ invalidate_subscriptions_accepts_a_user_test() ->
|
||||
execute_method(<<"push.invalidate_subscriptions">>, #{<<"user_id">> => <<"1001">>})
|
||||
).
|
||||
|
||||
sync_user_guild_settings_caches_the_private_scope_test() ->
|
||||
push_ets_cache:init(),
|
||||
Settings = #{<<"channel_overrides">> => #{<<"5">> => #{<<"muted">> => true}}},
|
||||
try
|
||||
with_registered_push(fun() ->
|
||||
?assert(
|
||||
execute_method(<<"push.sync_user_guild_settings">>, #{
|
||||
<<"user_id">> => <<"1001">>,
|
||||
<<"guild_id">> => <<"0">>,
|
||||
<<"user_guild_settings">> => Settings
|
||||
})
|
||||
)
|
||||
end),
|
||||
?assertEqual(Settings, push_ets_cache:get_user_guild_settings(1001, 0))
|
||||
after
|
||||
push_ets_cache:delete_user_guild_settings(1001, 0)
|
||||
end.
|
||||
|
||||
sync_user_guild_settings_rejects_an_invalid_guild_id_test() ->
|
||||
?assertError(
|
||||
{validation, _},
|
||||
execute_method(<<"push.sync_user_guild_settings">>, #{
|
||||
<<"user_id">> => <<"1001">>,
|
||||
<<"guild_id">> => <<"nope">>,
|
||||
<<"user_guild_settings">> => #{}
|
||||
})
|
||||
).
|
||||
|
||||
with_registered_push(Fun) ->
|
||||
case whereis(push) of
|
||||
undefined ->
|
||||
register(push, self()),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
unregister(push)
|
||||
end;
|
||||
_Pid ->
|
||||
Fun()
|
||||
end.
|
||||
|
||||
invalidate_subscriptions_rejects_an_invalid_snowflake_test() ->
|
||||
?assertError(
|
||||
{validation, _},
|
||||
|
||||
@@ -302,6 +302,12 @@ render_push_outbox_queue_stats(Stats) ->
|
||||
<<"counter">>,
|
||||
<<"Queued recipients skipped because they became active">>,
|
||||
gate_counter(skipped_active, Stats)
|
||||
),
|
||||
format_metric(
|
||||
<<"fluxer_gateway_push_outbox_followup_clears_total">>,
|
||||
<<"counter">>,
|
||||
<<"Clears queued after an in-flight push job whose recipient read the channel">>,
|
||||
gate_counter(followup_clears, Stats)
|
||||
)
|
||||
].
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
-export([
|
||||
call/1,
|
||||
call/2,
|
||||
call_with_retry/2,
|
||||
handle_http_response/2,
|
||||
rpc_headers/1,
|
||||
@@ -42,9 +43,13 @@
|
||||
|
||||
-spec call(rpc_request()) -> rpc_response().
|
||||
call(Request) ->
|
||||
call(Request, request_timeout_ms()).
|
||||
|
||||
-spec call(rpc_request(), pos_integer()) -> rpc_response().
|
||||
call(Request, Timeout) ->
|
||||
Trace = build_request_trace(Request),
|
||||
maybe_log_voice_request_start(Trace),
|
||||
Result = do_request(Request),
|
||||
Result = do_request(Request, Timeout),
|
||||
maybe_log_voice_request_response(Trace, Result),
|
||||
Result.
|
||||
|
||||
@@ -132,10 +137,9 @@ backoff_delay(Attempt, {_MaxAttempts, BaseMs, MaxMs, JitterMs}) ->
|
||||
end,
|
||||
trunc(CappedDelay + Jitter).
|
||||
|
||||
-spec do_request(rpc_request()) -> rpc_response().
|
||||
do_request(Request) ->
|
||||
-spec do_request(rpc_request(), pos_integer()) -> rpc_response().
|
||||
do_request(Request, Timeout) ->
|
||||
{Url, HostKey, IsHttps} = rpc_url_meta(),
|
||||
Timeout = request_timeout_ms(),
|
||||
Payload = iolist_to_binary(json:encode(Request)),
|
||||
Headers = rpc_headers(Request),
|
||||
RequestOpts = #{
|
||||
|
||||
@@ -59,6 +59,8 @@ handle_call({reload, NewData}, _From, State) ->
|
||||
handle_reload_call(NewData, State);
|
||||
handle_call(get_voice_server_pid, _From, State) ->
|
||||
guild_voice_lifecycle:reply_voice_server_pid(State);
|
||||
handle_call({released_push_holds, SessionIds}, _From, State) when is_list(SessionIds) ->
|
||||
{reply, guild_sessions:released_push_holds(SessionIds, State), State};
|
||||
handle_call({terminate}, _From, State) ->
|
||||
{stop, normal, ok, State};
|
||||
handle_call(Msg, From, State) when is_tuple(Msg) ->
|
||||
@@ -158,6 +160,10 @@ handle_cast({drop_session_member_lists, SessionId}, State) when is_binary(Sessio
|
||||
{noreply, guild_member_list:unsubscribe_session(SessionId, State)};
|
||||
handle_cast({set_session_typing_override, SessionId, TypingFlag}, State) ->
|
||||
handle_set_session_typing_override_cast(SessionId, TypingFlag, State);
|
||||
handle_cast({set_session_push_hold, SessionId, Hold}, State) when
|
||||
is_binary(SessionId), is_boolean(Hold)
|
||||
->
|
||||
{noreply, guild_sessions:set_session_push_hold(SessionId, Hold, State)};
|
||||
handle_cast({send_guild_sync, SessionId}, State) ->
|
||||
handle_send_guild_sync_cast(SessionId, State);
|
||||
handle_cast({send_members_chunk, SessionId, ChunkData}, State) ->
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
-define(PUSH_WORKER_MAX_AGE_MS, 60000).
|
||||
-define(GRACE_RECHECK_KEY, guild_push_offline_grace_recheck_ms).
|
||||
-define(DEFAULT_GRACE_RECHECK_MS, 7000).
|
||||
-define(GRACE_HOLD_CALL_TIMEOUT_MS, 5000).
|
||||
-define(PUSH_COUNTERS, guild_push_counters).
|
||||
-define(PUSH_COUNTER_KEYS, [
|
||||
worker_started,
|
||||
@@ -40,14 +41,17 @@
|
||||
-type user_id() :: integer().
|
||||
-type push_worker() :: {integer(), pid(), integer()}.
|
||||
-type push_gate() :: push_worker() | undefined.
|
||||
-type grace_sessions() :: #{user_id() => [pid()]}.
|
||||
-type grace_sessions() :: #{user_id() => [{binary(), pid()}]}.
|
||||
-type grace_hold() :: {grace_sessions(), integer()} | none.
|
||||
-type held_push() :: {[user_id()], grace_hold(), term()}.
|
||||
-type held_push() :: {[user_id()], grace_hold(), term(), pid() | undefined}.
|
||||
-export_type([event/0, event_data/0, guild_state/0, guild_id/0]).
|
||||
|
||||
-spec maybe_send_push_notifications(event(), event_data(), guild_id(), guild_state()) -> ok.
|
||||
maybe_send_push_notifications(message_create, FinalData, GuildId, UpdatedState) ->
|
||||
case maps:get(disable_push_notifications, UpdatedState, false) of
|
||||
case
|
||||
maps:get(disable_push_notifications, UpdatedState, false) orelse
|
||||
push_message_params:suppresses_notifications(FinalData)
|
||||
of
|
||||
true -> ok;
|
||||
false -> maybe_spawn_push(FinalData, GuildId, UpdatedState)
|
||||
end;
|
||||
@@ -251,15 +255,16 @@ spawn_push(FinalData, GuildId, UpdatedState, Gate) ->
|
||||
-spec compact_push_state(ets:tid(), map(), guild_id(), guild_state()) -> guild_state().
|
||||
compact_push_state(MembersTab, Data, GuildId, UpdatedState) ->
|
||||
Sessions = maps:get(sessions, UpdatedState, #{}),
|
||||
SessionEligibility = build_push_session_eligibility(Sessions, UpdatedState),
|
||||
{SessionEligibility, Hold} = build_push_session_eligibility(Sessions, UpdatedState),
|
||||
#{
|
||||
id => maps:get(id, UpdatedState, GuildId),
|
||||
data => compact_push_data(Data),
|
||||
virtual_channel_access => maps:get(virtual_channel_access, UpdatedState, #{}),
|
||||
members_ets => MembersTab,
|
||||
member_presence => maps:get(member_presence, UpdatedState, undefined),
|
||||
guild_pid => grace_guild_pid(),
|
||||
session_eligibility => SessionEligibility,
|
||||
grace_hold => grace_hold(Sessions, SessionEligibility),
|
||||
grace_hold => Hold,
|
||||
member_count => maps:get(member_count, UpdatedState, undefined)
|
||||
}.
|
||||
|
||||
@@ -316,6 +321,7 @@ legacy_push_state(GuildId, UpdatedState) ->
|
||||
data => maps:get(data, UpdatedState, #{}),
|
||||
sessions => maps:get(sessions, UpdatedState, #{}),
|
||||
member_presence => maps:get(member_presence, UpdatedState, undefined),
|
||||
guild_pid => grace_guild_pid(),
|
||||
virtual_channel_access => maps:get(virtual_channel_access, UpdatedState, #{}),
|
||||
member_count => maps:get(member_count, UpdatedState, undefined)
|
||||
}.
|
||||
@@ -577,7 +583,8 @@ send_compact_scanned_push(MessageData, GuildId, Scan, State) ->
|
||||
{
|
||||
lists:reverse(maps:get(held_user_ids, Scan, [])),
|
||||
maps:get(grace_hold, State, none),
|
||||
maps:get(member_presence, State, undefined)
|
||||
maps:get(member_presence, State, undefined),
|
||||
maps:get(guild_pid, State, undefined)
|
||||
}
|
||||
).
|
||||
|
||||
@@ -654,7 +661,7 @@ send_push_notifications(MessageData, GuildId, State) ->
|
||||
Data = maps:get(data, State),
|
||||
Members = guild_data_index:member_map(Data),
|
||||
Sessions = maps:get(sessions, State, #{}),
|
||||
SessionEligibility = build_push_session_eligibility(Sessions, State),
|
||||
{SessionEligibility, Hold} = build_push_session_eligibility(Sessions, State),
|
||||
CandidateUserIds = push_candidate_user_ids(Members, SessionEligibility, MessageData),
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, MessageData, undefined),
|
||||
case guild_dispatch_decorate:parse_snowflake(<<"channel_id">>, ChannelIdBin) of
|
||||
@@ -668,7 +675,7 @@ send_push_notifications(MessageData, GuildId, State) ->
|
||||
CandidateUserIds,
|
||||
ChannelId,
|
||||
SessionEligibility,
|
||||
grace_hold(Sessions, SessionEligibility),
|
||||
Hold,
|
||||
Data,
|
||||
State
|
||||
)
|
||||
@@ -715,7 +722,12 @@ send_to_eligible(
|
||||
SessionEligibility,
|
||||
Data,
|
||||
large_guild_meta(State),
|
||||
{HeldUserIds, Hold, maps:get(member_presence, State, undefined)}
|
||||
{
|
||||
HeldUserIds,
|
||||
Hold,
|
||||
maps:get(member_presence, State, undefined),
|
||||
maps:get(guild_pid, State, undefined)
|
||||
}
|
||||
)
|
||||
end.
|
||||
|
||||
@@ -885,41 +897,81 @@ view_to_filtermap(UserId, ChannelId, Member, State) ->
|
||||
false -> false
|
||||
end.
|
||||
|
||||
-spec build_push_session_eligibility(map(), guild_state()) -> #{user_id() => boolean()}.
|
||||
-spec build_push_session_eligibility(map(), guild_state()) ->
|
||||
{#{user_id() => boolean()}, grace_hold()}.
|
||||
build_push_session_eligibility(Sessions, State) ->
|
||||
case presence_eligibility_enabled() of
|
||||
true -> build_push_presence_eligibility(Sessions, State);
|
||||
false -> build_push_session_eligibility(Sessions)
|
||||
false -> build_legacy_session_eligibility(Sessions)
|
||||
end.
|
||||
|
||||
-spec grace_guild_pid() -> pid() | undefined.
|
||||
grace_guild_pid() ->
|
||||
case presence_eligibility_enabled() of
|
||||
true -> self();
|
||||
false -> undefined
|
||||
end.
|
||||
|
||||
-spec presence_eligibility_enabled() -> boolean().
|
||||
presence_eligibility_enabled() ->
|
||||
application:get_env(fluxer_gateway, push_presence_eligibility, true) =:= true.
|
||||
|
||||
-spec build_push_presence_eligibility(map(), guild_state()) -> #{user_id() => boolean()}.
|
||||
-spec build_legacy_session_eligibility(map()) -> {#{user_id() => boolean()}, grace_hold()}.
|
||||
build_legacy_session_eligibility(Sessions) ->
|
||||
Eligibility = build_push_session_eligibility(Sessions),
|
||||
{Eligibility, grace_hold(suppressed_sessions(Sessions, Eligibility))}.
|
||||
|
||||
-spec build_push_presence_eligibility(map(), guild_state()) ->
|
||||
{#{user_id() => boolean()}, grace_hold()}.
|
||||
build_push_presence_eligibility(Sessions, State) ->
|
||||
Presences = maps:get(member_presence, State, undefined),
|
||||
Engaged = engaged_sessions(Sessions, maps:get(member_presence, State, undefined)),
|
||||
{engagement_eligibility(Sessions, Engaged), grace_hold(held_engaged_sessions(Engaged))}.
|
||||
|
||||
-spec engaged_sessions(map(), term()) -> grace_sessions().
|
||||
engaged_sessions(Sessions, Presences) ->
|
||||
maps:fold(
|
||||
fun(_Sid, Session, Acc) ->
|
||||
accumulate_presence_eligibility(Session, Presences, Acc)
|
||||
fun(Sid, Session, Acc) -> maybe_add_engaged_session(Sid, Session, Presences, Acc) end,
|
||||
#{},
|
||||
Sessions
|
||||
).
|
||||
|
||||
-spec maybe_add_engaged_session(term(), term(), term(), grace_sessions()) -> grace_sessions().
|
||||
maybe_add_engaged_session(Sid, #{user_id := UserId} = Session, Presences, Acc) when
|
||||
is_integer(UserId)
|
||||
->
|
||||
case session_engaged(Session, UserId, Presences) of
|
||||
true -> Acc#{UserId => session_pids(Sid, Session) ++ maps:get(UserId, Acc, [])};
|
||||
false -> Acc
|
||||
end;
|
||||
maybe_add_engaged_session(_Sid, _Session, _Presences, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec session_engaged(map(), user_id(), term()) -> boolean().
|
||||
session_engaged(#{push_hold := Hold}, _UserId, _Presences) ->
|
||||
Hold =:= true;
|
||||
session_engaged(_Session, UserId, Presences) ->
|
||||
actively_engaged(UserId, Presences).
|
||||
|
||||
-spec session_pids(term(), map()) -> [{binary(), pid()}].
|
||||
session_pids(Sid, #{pid := Pid}) when is_binary(Sid), is_pid(Pid) -> [{Sid, Pid}];
|
||||
session_pids(_Sid, _Session) -> [].
|
||||
|
||||
-spec engagement_eligibility(map(), grace_sessions()) -> #{user_id() => boolean()}.
|
||||
engagement_eligibility(Sessions, Engaged) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(_Sid, #{user_id := UserId}, Acc) when is_integer(UserId) ->
|
||||
Acc#{UserId => not maps:is_key(UserId, Engaged)};
|
||||
(_Sid, _Session, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
#{},
|
||||
Sessions
|
||||
).
|
||||
|
||||
-spec accumulate_presence_eligibility(map(), term(), #{user_id() => boolean()}) ->
|
||||
#{user_id() => boolean()}.
|
||||
accumulate_presence_eligibility(Session, Presences, Acc) ->
|
||||
case maps:get(user_id, Session, undefined) of
|
||||
UserId when is_integer(UserId) ->
|
||||
Acc#{
|
||||
UserId =>
|
||||
maps:get(UserId, Acc, true) andalso
|
||||
not actively_engaged(UserId, Presences)
|
||||
};
|
||||
_ ->
|
||||
Acc
|
||||
end.
|
||||
-spec held_engaged_sessions(grace_sessions()) -> grace_sessions().
|
||||
held_engaged_sessions(Engaged) ->
|
||||
maps:filter(fun(_UserId, Pids) -> Pids =/= [] end, Engaged).
|
||||
|
||||
-spec actively_engaged(user_id(), term()) -> boolean().
|
||||
actively_engaged(_UserId, undefined) ->
|
||||
@@ -1059,46 +1111,77 @@ send_push_now(UserIds, Params) ->
|
||||
push:handle_message_create(Params#{user_ids => UserIds}).
|
||||
|
||||
-spec hold_push_through_grace(held_push(), map()) -> ok.
|
||||
hold_push_through_grace({[_ | _] = UserIds, {Sessions, RecheckAt}, Presences}, Params) ->
|
||||
hold_push_through_grace(
|
||||
{[_ | _] = UserIds, {Sessions, RecheckAt}, Presences, GuildPid}, Params
|
||||
) ->
|
||||
HeldParams = Params#{
|
||||
user_roles => maps:with(UserIds, maps:get(user_roles, Params)),
|
||||
connected_users => maps:with(UserIds, maps:get(connected_users, Params))
|
||||
},
|
||||
HeldSessions = maps:with(UserIds, Sessions),
|
||||
_ = spawn(fun() ->
|
||||
deliver_after_grace(HeldParams, HeldSessions, RecheckAt, Presences)
|
||||
deliver_after_grace(HeldParams, HeldSessions, RecheckAt, {Presences, GuildPid})
|
||||
end),
|
||||
ok;
|
||||
hold_push_through_grace(_Held, _Params) ->
|
||||
ok.
|
||||
|
||||
-spec deliver_after_grace(map(), grace_sessions(), integer(), term()) -> ok.
|
||||
deliver_after_grace(Params, Sessions, RecheckAt, Presences) ->
|
||||
-spec deliver_after_grace(map(), grace_sessions(), integer(), {term(), pid() | undefined}) ->
|
||||
ok.
|
||||
deliver_after_grace(Params, Sessions, RecheckAt, {Presences, GuildPid}) ->
|
||||
ok = apply_push_worker_priority(),
|
||||
LiveSessions = await_grace_recheck(monitor_grace_sessions(Sessions), RecheckAt),
|
||||
send_push_now(
|
||||
grace_recipients(maps:keys(Sessions), maps:values(LiveSessions), Presences), Params
|
||||
).
|
||||
HeldUserIds = maps:keys(Sessions),
|
||||
AlreadyOffline = users_left_offline(HeldUserIds, Presences),
|
||||
LiveSessions = maps:values(
|
||||
await_grace_recheck(monitor_grace_sessions(Sessions), RecheckAt)
|
||||
),
|
||||
Holding = still_holding(LiveSessions, released_push_holds(GuildPid, LiveSessions)),
|
||||
send_push_now(grace_recipients(HeldUserIds, Holding, Presences, AlreadyOffline), Params).
|
||||
|
||||
-spec grace_recipients([user_id()], [user_id()], term()) -> [user_id()].
|
||||
grace_recipients(HeldUserIds, LiveUserIds, Presences) ->
|
||||
-spec still_holding([{user_id(), binary()}], [binary()]) -> [user_id()].
|
||||
still_holding(LiveSessions, Released) ->
|
||||
ReleasedSet = maps:from_keys(Released, true),
|
||||
[UserId || {UserId, Sid} <- LiveSessions, not maps:is_key(Sid, ReleasedSet)].
|
||||
|
||||
-spec released_push_holds(pid() | undefined, [{user_id(), binary()}]) -> [binary()].
|
||||
released_push_holds(GuildPid, [_ | _] = LiveSessions) when is_pid(GuildPid) ->
|
||||
SessionIds = [Sid || {_UserId, Sid} <- LiveSessions],
|
||||
try
|
||||
gen_server:call(
|
||||
GuildPid, {released_push_holds, SessionIds}, ?GRACE_HOLD_CALL_TIMEOUT_MS
|
||||
)
|
||||
of
|
||||
Released when is_list(Released) -> Released;
|
||||
_ -> []
|
||||
catch
|
||||
exit:_ -> []
|
||||
end;
|
||||
released_push_holds(_GuildPid, _LiveSessions) ->
|
||||
[].
|
||||
|
||||
-spec grace_recipients([user_id()], [user_id()], term(), [user_id()]) -> [user_id()].
|
||||
grace_recipients(HeldUserIds, LiveUserIds, Presences, AlreadyOffline) ->
|
||||
Live = lists:usort(LiveUserIds),
|
||||
Gone = lists:usort(HeldUserIds) -- Live,
|
||||
lists:usort(Gone ++ users_left_offline(Live, Presences)).
|
||||
lists:usort(Gone ++ (users_left_offline(Live, Presences) -- AlreadyOffline)).
|
||||
|
||||
-spec monitor_grace_sessions(grace_sessions()) -> #{reference() => user_id()}.
|
||||
-spec monitor_grace_sessions(grace_sessions()) -> #{reference() => {user_id(), binary()}}.
|
||||
monitor_grace_sessions(Sessions) ->
|
||||
maps:fold(fun monitor_user_sessions/3, #{}, Sessions).
|
||||
|
||||
-spec monitor_user_sessions(user_id(), [pid()], #{reference() => user_id()}) ->
|
||||
#{reference() => user_id()}.
|
||||
monitor_user_sessions(UserId, Pids, Monitors) ->
|
||||
-spec monitor_user_sessions(
|
||||
user_id(), [{binary(), pid()}], #{reference() => {user_id(), binary()}}
|
||||
) ->
|
||||
#{reference() => {user_id(), binary()}}.
|
||||
monitor_user_sessions(UserId, Held, Monitors) ->
|
||||
lists:foldl(
|
||||
fun(Pid, Acc) -> Acc#{erlang:monitor(process, Pid) => UserId} end, Monitors, Pids
|
||||
fun({Sid, Pid}, Acc) -> Acc#{erlang:monitor(process, Pid) => {UserId, Sid}} end,
|
||||
Monitors,
|
||||
Held
|
||||
).
|
||||
|
||||
-spec await_grace_recheck(#{reference() => user_id()}, integer()) ->
|
||||
#{reference() => user_id()}.
|
||||
-spec await_grace_recheck(#{reference() => {user_id(), binary()}}, integer()) ->
|
||||
#{reference() => {user_id(), binary()}}.
|
||||
await_grace_recheck(Monitors, RecheckAt) ->
|
||||
Remaining = max(0, RecheckAt - erlang:monotonic_time(millisecond)),
|
||||
receive
|
||||
@@ -1119,14 +1202,11 @@ presence_is_offline(UserId, Presences) ->
|
||||
Presence -> maps:get(<<"status">>, Presence, <<"offline">>) =:= <<"offline">>
|
||||
end.
|
||||
|
||||
-spec grace_hold(map(), #{user_id() => boolean()}) -> grace_hold().
|
||||
grace_hold(Sessions, SessionEligibility) ->
|
||||
case suppressed_sessions(Sessions, SessionEligibility) of
|
||||
Held when map_size(Held) =:= 0 ->
|
||||
none;
|
||||
Held ->
|
||||
{Held, erlang:monotonic_time(millisecond) + grace_recheck_ms()}
|
||||
end.
|
||||
-spec grace_hold(grace_sessions()) -> grace_hold().
|
||||
grace_hold(Held) when map_size(Held) =:= 0 ->
|
||||
none;
|
||||
grace_hold(Held) ->
|
||||
{Held, erlang:monotonic_time(millisecond) + grace_recheck_ms()}.
|
||||
|
||||
-spec held_sessions(grace_hold()) -> grace_sessions().
|
||||
held_sessions(none) -> #{};
|
||||
@@ -1135,35 +1215,37 @@ held_sessions({Held, _RecheckAt}) -> Held.
|
||||
-spec suppressed_sessions(map(), #{user_id() => boolean()}) -> grace_sessions().
|
||||
suppressed_sessions(Sessions, SessionEligibility) ->
|
||||
maps:fold(
|
||||
fun(_Sid, Session, Acc) -> maybe_hold_session(Session, SessionEligibility, Acc) end,
|
||||
fun(Sid, Session, Acc) -> maybe_hold_session(Sid, Session, SessionEligibility, Acc) end,
|
||||
#{},
|
||||
Sessions
|
||||
).
|
||||
|
||||
-spec maybe_hold_session(term(), #{user_id() => boolean()}, grace_sessions()) ->
|
||||
-spec maybe_hold_session(term(), term(), #{user_id() => boolean()}, grace_sessions()) ->
|
||||
grace_sessions().
|
||||
maybe_hold_session(Session, SessionEligibility, Acc) when is_map(Session) ->
|
||||
maybe_hold_session(Sid, Session, SessionEligibility, Acc) when is_map(Session) ->
|
||||
hold_suppressed_session(
|
||||
maps:get(user_id, Session, undefined),
|
||||
maps:get(pid, Session, undefined),
|
||||
session_pids(Sid, Session),
|
||||
SessionEligibility,
|
||||
Acc
|
||||
);
|
||||
maybe_hold_session(_Session, _SessionEligibility, Acc) ->
|
||||
maybe_hold_session(_Sid, _Session, _SessionEligibility, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec hold_suppressed_session(term(), term(), #{user_id() => boolean()}, grace_sessions()) ->
|
||||
-spec hold_suppressed_session(
|
||||
term(), [{binary(), pid()}], #{user_id() => boolean()}, grace_sessions()
|
||||
) ->
|
||||
grace_sessions().
|
||||
hold_suppressed_session(UserId, Pid, SessionEligibility, Acc) when
|
||||
is_integer(UserId), is_pid(Pid)
|
||||
hold_suppressed_session(UserId, [_ | _] = Held, SessionEligibility, Acc) when
|
||||
is_integer(UserId)
|
||||
->
|
||||
case maps:get(UserId, SessionEligibility, true) of
|
||||
false ->
|
||||
Acc#{UserId => [Pid | maps:get(UserId, Acc, [])]};
|
||||
Acc#{UserId => Held ++ maps:get(UserId, Acc, [])};
|
||||
true ->
|
||||
Acc
|
||||
end;
|
||||
hold_suppressed_session(_UserId, _Pid, _SessionEligibility, Acc) ->
|
||||
hold_suppressed_session(_UserId, _Held, _SessionEligibility, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec grace_recheck_ms() -> pos_integer().
|
||||
@@ -1258,7 +1340,7 @@ presence_eligibility_suppresses_active_desktop_test() ->
|
||||
),
|
||||
?assertEqual(
|
||||
#{1 => false},
|
||||
build_push_presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{
|
||||
presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{
|
||||
member_presence => Tab
|
||||
})
|
||||
)
|
||||
@@ -1274,7 +1356,7 @@ presence_eligibility_allows_mobile_only_session_test() ->
|
||||
),
|
||||
?assertEqual(
|
||||
#{1 => true},
|
||||
build_push_presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{
|
||||
presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{
|
||||
member_presence => Tab
|
||||
})
|
||||
)
|
||||
@@ -1293,7 +1375,7 @@ presence_eligibility_allows_idle_and_afk_test() ->
|
||||
),
|
||||
?assertEqual(
|
||||
#{1 => true, 2 => true},
|
||||
build_push_presence_eligibility(
|
||||
presence_eligibility(
|
||||
#{<<"s1">> => #{user_id => 1}, <<"s2">> => #{user_id => 2}},
|
||||
#{member_presence => Tab}
|
||||
)
|
||||
@@ -1307,7 +1389,7 @@ presence_eligibility_allows_unknown_presence_test() ->
|
||||
try
|
||||
?assertEqual(
|
||||
#{7 => true},
|
||||
build_push_presence_eligibility(#{<<"s1">> => #{user_id => 7}}, #{
|
||||
presence_eligibility(#{<<"s1">> => #{user_id => 7}}, #{
|
||||
member_presence => Tab
|
||||
})
|
||||
)
|
||||
@@ -1318,24 +1400,121 @@ presence_eligibility_allows_unknown_presence_test() ->
|
||||
presence_eligibility_allows_when_presence_table_missing_test() ->
|
||||
?assertEqual(
|
||||
#{1 => true},
|
||||
build_push_presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{})
|
||||
presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{})
|
||||
).
|
||||
|
||||
presence_eligibility(Sessions, State) ->
|
||||
{Eligibility, _Hold} = build_push_presence_eligibility(Sessions, State),
|
||||
Eligibility.
|
||||
|
||||
session_push_hold_keeps_an_active_desktop_engaged_beside_a_backgrounded_phone_test() ->
|
||||
Tab = ets:new(test_member_presence, [set, public]),
|
||||
Desktop = spawn(fun() -> ok end),
|
||||
Phone = spawn(fun() -> ok end),
|
||||
try
|
||||
ets:insert(
|
||||
Tab, {1, #{<<"status">> => <<"online">>, <<"mobile">> => true, <<"afk">> => false}}
|
||||
),
|
||||
Sessions = #{
|
||||
<<"desktop">> => #{user_id => 1, pid => Desktop, push_hold => true},
|
||||
<<"phone">> => #{user_id => 1, pid => Phone, push_hold => false}
|
||||
},
|
||||
{Eligibility, {Held, _RecheckAt}} =
|
||||
build_push_presence_eligibility(Sessions, #{member_presence => Tab}),
|
||||
?assertEqual(#{1 => false}, Eligibility),
|
||||
?assertEqual(#{1 => [{<<"desktop">>, Desktop}]}, Held)
|
||||
after
|
||||
ets:delete(Tab)
|
||||
end.
|
||||
|
||||
session_push_hold_engages_a_user_published_as_offline_test() ->
|
||||
Tab = ets:new(test_member_presence, [set, public]),
|
||||
try
|
||||
ets:insert(Tab, {1, #{<<"status">> => <<"offline">>}}),
|
||||
?assertEqual(
|
||||
#{1 => false},
|
||||
presence_eligibility(
|
||||
#{<<"s1">> => #{user_id => 1, push_hold => true}}, #{member_presence => Tab}
|
||||
)
|
||||
)
|
||||
after
|
||||
ets:delete(Tab)
|
||||
end.
|
||||
|
||||
session_push_hold_false_overrides_an_active_published_presence_test() ->
|
||||
Tab = ets:new(test_member_presence, [set, public]),
|
||||
try
|
||||
ets:insert(
|
||||
Tab, {1, #{<<"status">> => <<"online">>, <<"mobile">> => false, <<"afk">> => false}}
|
||||
),
|
||||
?assertEqual(
|
||||
{#{1 => true}, none},
|
||||
build_push_presence_eligibility(
|
||||
#{<<"s1">> => #{user_id => 1, pid => self(), push_hold => false}},
|
||||
#{member_presence => Tab}
|
||||
)
|
||||
)
|
||||
after
|
||||
ets:delete(Tab)
|
||||
end.
|
||||
|
||||
released_push_holds_treat_an_old_guild_reply_as_nothing_released_test() ->
|
||||
GuildPid = spawn(fun() ->
|
||||
receive
|
||||
{'$gen_call', From, {released_push_holds, _SessionIds}} ->
|
||||
gen_server:reply(From, ok)
|
||||
end
|
||||
end),
|
||||
?assertEqual([], released_push_holds(GuildPid, [{1, <<"s1">>}])).
|
||||
|
||||
released_push_holds_treat_a_dead_guild_as_nothing_released_test() ->
|
||||
{GuildPid, MRef} = spawn_monitor(fun() -> ok end),
|
||||
receive
|
||||
{'DOWN', MRef, process, GuildPid, _} -> ok
|
||||
end,
|
||||
?assertEqual([], released_push_holds(GuildPid, [{1, <<"s1">>}])).
|
||||
|
||||
push_states_carry_the_guild_pid_for_the_grace_recheck_test() ->
|
||||
?assertEqual({self(), self()}, push_state_guild_pids()).
|
||||
|
||||
push_states_skip_the_grace_recheck_while_presence_eligibility_is_off_test() ->
|
||||
ok = application:set_env(fluxer_gateway, push_presence_eligibility, false),
|
||||
try
|
||||
?assertEqual({undefined, undefined}, push_state_guild_pids())
|
||||
after
|
||||
ok = application:unset_env(fluxer_gateway, push_presence_eligibility)
|
||||
end.
|
||||
|
||||
push_state_guild_pids() ->
|
||||
Tab = ets:new(push_state_members, []),
|
||||
try
|
||||
Compact = compact_push_state(Tab, #{}, 7, #{sessions => #{}}),
|
||||
Legacy = legacy_push_state(7, #{}),
|
||||
{maps:get(guild_pid, Compact), maps:get(guild_pid, Legacy)}
|
||||
after
|
||||
ets:delete(Tab)
|
||||
end.
|
||||
|
||||
grace_recipients_keep_holding_a_user_already_offline_when_held_test() ->
|
||||
Presences = #{1 => #{<<"status">> => <<"offline">>}},
|
||||
?assertEqual([], grace_recipients([1], [1], Presences, [1])),
|
||||
?assertEqual([1], grace_recipients([1], [], Presences, [1])).
|
||||
|
||||
grace_recipients_push_a_user_whose_held_sessions_all_ended_test() ->
|
||||
Presences = #{1 => #{<<"status">> => <<"online">>}},
|
||||
?assertEqual([1], grace_recipients([1], [], Presences)).
|
||||
?assertEqual([1], grace_recipients([1], [], Presences, [])).
|
||||
|
||||
grace_recipients_skip_a_user_still_online_on_a_held_session_test() ->
|
||||
Presences = #{1 => #{<<"status">> => <<"online">>}},
|
||||
?assertEqual([], grace_recipients([1], [1], Presences)).
|
||||
?assertEqual([], grace_recipients([1], [1], Presences, [])).
|
||||
|
||||
grace_recipients_push_a_user_whose_live_session_went_offline_test() ->
|
||||
Presences = #{1 => #{<<"status">> => <<"offline">>}},
|
||||
?assertEqual([1], grace_recipients([1], [1], Presences)).
|
||||
?assertEqual([1], grace_recipients([1], [1], Presences, [])).
|
||||
|
||||
grace_recipients_skip_a_user_with_one_ended_and_one_live_online_session_test() ->
|
||||
Presences = #{1 => #{<<"status">> => <<"online">>}},
|
||||
?assertEqual([], grace_recipients([1], [1], Presences)).
|
||||
?assertEqual([], grace_recipients([1], [1], Presences, [])).
|
||||
|
||||
grace_recipients_mix_users_test() ->
|
||||
Presences = #{
|
||||
@@ -1343,7 +1522,7 @@ grace_recipients_mix_users_test() ->
|
||||
2 => #{<<"status">> => <<"online">>},
|
||||
3 => #{<<"status">> => <<"offline">>}
|
||||
},
|
||||
?assertEqual([1, 3], grace_recipients([1, 2, 3], [2, 3, 3], Presences)).
|
||||
?assertEqual([1, 3], grace_recipients([1, 2, 3], [2, 3, 3], Presences, [])).
|
||||
|
||||
deliver_after_grace_pushes_a_user_whose_session_exits_inside_the_window_test() ->
|
||||
Self = self(),
|
||||
@@ -1358,10 +1537,10 @@ deliver_after_grace_pushes_a_user_whose_session_exits_inside_the_window_test() -
|
||||
stop -> ok
|
||||
end
|
||||
end),
|
||||
Held = #{42 => [Session]},
|
||||
Held = #{42 => [{<<"s1">>, Session}]},
|
||||
Presences = #{42 => #{<<"status">> => <<"online">>}},
|
||||
RecheckAt = erlang:monotonic_time(millisecond) + 300,
|
||||
_ = spawn(fun() -> deliver_after_grace(#{}, Held, RecheckAt, Presences) end),
|
||||
_ = spawn(fun() -> deliver_after_grace(#{}, Held, RecheckAt, {Presences, undefined}) end),
|
||||
timer:sleep(50),
|
||||
Session ! stop,
|
||||
receive
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
refresh_user_session_cache/2,
|
||||
refresh_all_viewable_channels/1,
|
||||
handle_set_typing_override/3,
|
||||
set_session_push_hold/3,
|
||||
released_push_holds/2,
|
||||
handle_send_guild_sync/2,
|
||||
handle_send_members_chunk/3,
|
||||
build_viewable_channel_map/1
|
||||
@@ -214,6 +216,25 @@ is_session_active(SessionId, State) ->
|
||||
handle_set_typing_override(SessionId, TypingFlag, State) ->
|
||||
guild_sessions_passive:handle_set_typing_override(SessionId, TypingFlag, State).
|
||||
|
||||
-spec set_session_push_hold(session_id(), boolean(), guild_state()) -> guild_state().
|
||||
set_session_push_hold(SessionId, Hold, State) ->
|
||||
Sessions = maps:get(sessions, State, #{}),
|
||||
case maps:get(SessionId, Sessions, undefined) of
|
||||
SessionData when is_map(SessionData) ->
|
||||
State#{sessions => Sessions#{SessionId => SessionData#{push_hold => Hold}}};
|
||||
_ ->
|
||||
State
|
||||
end.
|
||||
|
||||
-spec released_push_holds([session_id()], guild_state()) -> [session_id()].
|
||||
released_push_holds(SessionIds, State) ->
|
||||
Sessions = maps:get(sessions, State, #{}),
|
||||
[
|
||||
SessionId
|
||||
|| SessionId <- SessionIds,
|
||||
maps:get(push_hold, maps:get(SessionId, Sessions, #{}), undefined) =:= false
|
||||
].
|
||||
|
||||
-spec handle_send_guild_sync(session_id(), guild_state()) -> guild_state().
|
||||
handle_send_guild_sync(SessionId, State) ->
|
||||
guild_sessions_passive:handle_send_guild_sync(SessionId, State).
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}.
|
||||
-type sessions() :: #{session_id() => session_entry()}.
|
||||
-type push_buffer_entry() :: #{
|
||||
channel_id := integer(), message_id := integer(), params := map()
|
||||
channel_id := integer(), message_id := integer(), params := map(), buffered_at => integer()
|
||||
}.
|
||||
-type state() :: #{
|
||||
user_id := user_id(),
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
-type user_id() :: integer().
|
||||
-type state() :: map().
|
||||
-type push_buffer_entry() :: #{
|
||||
channel_id := integer(), message_id := integer(), params := map()
|
||||
channel_id := integer(), message_id := integer(), params := map(), buffered_at => integer()
|
||||
}.
|
||||
|
||||
-define(DEFAULT_PUSH_BUFFER_MAX_ENTRIES, 128).
|
||||
-define(DEFAULT_PUSH_BUFFER_MAX_BYTES, 1048576).
|
||||
-define(PUSH_READ_MARKS_MAX_CHANNELS, 32).
|
||||
-define(PUSH_BUFFER_MAX_ENTRIES_CONFIG_KEY, presence_push_buffer_max_entries).
|
||||
-define(PUSH_BUFFER_MAX_BYTES_CONFIG_KEY, presence_push_buffer_max_bytes).
|
||||
-define(PUSH_BUFFER_COUNTERS, presence_push_buffer_counters).
|
||||
@@ -133,9 +134,27 @@ handle_message_create_event(Data, State) ->
|
||||
UserId = maps:get(user_id, State),
|
||||
case build_push_create_params(UserId, Data) of
|
||||
undefined -> State;
|
||||
Params -> route_push_notification(Params, State)
|
||||
#{author_id := UserId} -> ack_own_message(Data, State);
|
||||
Params -> route_unread_push_notification(Data, Params, State)
|
||||
end.
|
||||
|
||||
-spec route_unread_push_notification(map(), map(), state()) -> state().
|
||||
route_unread_push_notification(Data, Params, State) ->
|
||||
ChannelId = extract_snowflake(<<"channel_id">>, Data),
|
||||
MessageId = extract_snowflake(<<"id">>, Data),
|
||||
ReadMarks = maps:get(push_read_marks, State, #{}),
|
||||
Read = is_integer(MessageId) andalso MessageId =< maps:get(ChannelId, ReadMarks, 0),
|
||||
case Read orelse push_message_params:suppresses_notifications(Data) of
|
||||
true -> State;
|
||||
false -> route_push_notification(Params, State)
|
||||
end.
|
||||
|
||||
-spec ack_own_message(map(), state()) -> state().
|
||||
ack_own_message(Data, State) ->
|
||||
ChannelId = extract_snowflake(<<"channel_id">>, Data),
|
||||
MessageId = extract_snowflake(<<"id">>, Data),
|
||||
maybe_ack_push_buffer(ChannelId, MessageId, State).
|
||||
|
||||
-spec handle_message_ack_event(map(), state()) -> state().
|
||||
handle_message_ack_event(Data, State) ->
|
||||
ChannelId = extract_snowflake(<<"channel_id">>, Data),
|
||||
@@ -146,11 +165,10 @@ handle_message_ack_event(Data, State) ->
|
||||
flush_push_buffer(#{push_buffer := []} = State) ->
|
||||
State;
|
||||
flush_push_buffer(#{push_buffer := Buffer} = State) ->
|
||||
Entries = lists:reverse(Buffer),
|
||||
lists:foreach(
|
||||
fun(Entry) -> push:handle_message_create(maps:get(params, Entry)) end,
|
||||
Entries
|
||||
),
|
||||
ok = push:handle_buffered_message_creates([
|
||||
(maps:get(params, Entry))#{buffered_at => maps:get(buffered_at, Entry, undefined)}
|
||||
|| Entry <- lists:reverse(Buffer)
|
||||
]),
|
||||
State#{push_buffer := []}.
|
||||
|
||||
-spec maybe_update_push_eligibility(state()) -> state().
|
||||
@@ -362,10 +380,23 @@ maybe_ack_push_buffer(_, _, State) ->
|
||||
ack_push_buffer(ChannelId, MessageId, State) when ChannelId > 0, MessageId > 0 ->
|
||||
Buffer = maps:get(push_buffer, State, []),
|
||||
FilteredBuffer = [E || E <- Buffer, not should_drop_buffer_entry(E, ChannelId, MessageId)],
|
||||
State#{push_buffer := FilteredBuffer};
|
||||
record_read_mark(ChannelId, MessageId, State#{push_buffer := FilteredBuffer});
|
||||
ack_push_buffer(_, _, State) ->
|
||||
State.
|
||||
|
||||
-spec record_read_mark(integer(), integer(), state()) -> state().
|
||||
record_read_mark(ChannelId, MessageId, State) ->
|
||||
ReadMarks = maps:get(push_read_marks, State, #{}),
|
||||
ReadMark = max(MessageId, maps:get(ChannelId, ReadMarks, 0)),
|
||||
State#{push_read_marks => cap_read_marks(ReadMarks#{ChannelId => ReadMark})}.
|
||||
|
||||
-spec cap_read_marks(#{integer() => integer()}) -> #{integer() => integer()}.
|
||||
cap_read_marks(ReadMarks) when map_size(ReadMarks) > ?PUSH_READ_MARKS_MAX_CHANNELS ->
|
||||
{_, OldestChannelId} = lists:min([{Mark, Id} || {Id, Mark} <- maps:to_list(ReadMarks)]),
|
||||
maps:remove(OldestChannelId, ReadMarks);
|
||||
cap_read_marks(ReadMarks) ->
|
||||
ReadMarks.
|
||||
|
||||
-spec should_drop_buffer_entry(push_buffer_entry(), integer(), integer()) -> boolean().
|
||||
should_drop_buffer_entry(Entry, ChannelId, MessageId) ->
|
||||
maps:get(channel_id, Entry) =:= ChannelId andalso
|
||||
@@ -383,7 +414,12 @@ make_push_buffer_entry(Params) ->
|
||||
build_buffer_entry(ChannelId, MessageId, Params) when
|
||||
is_integer(ChannelId), is_integer(MessageId)
|
||||
->
|
||||
#{channel_id => ChannelId, message_id => MessageId, params => Params};
|
||||
#{
|
||||
channel_id => ChannelId,
|
||||
message_id => MessageId,
|
||||
params => Params,
|
||||
buffered_at => erlang:system_time(millisecond)
|
||||
};
|
||||
build_buffer_entry(_, _, _) ->
|
||||
undefined.
|
||||
|
||||
@@ -512,6 +548,61 @@ buffer_push_notification_caps_bytes_test() ->
|
||||
end
|
||||
).
|
||||
|
||||
private_message(MessageId, ChannelType, Flags) ->
|
||||
#{
|
||||
<<"id">> => integer_to_binary(MessageId),
|
||||
<<"channel_id">> => <<"5">>,
|
||||
<<"channel_type">> => ChannelType,
|
||||
<<"flags">> => Flags,
|
||||
<<"author">> => #{<<"id">> => <<"20">>}
|
||||
}.
|
||||
|
||||
pushed_private_message_ids(Sessions, Messages) ->
|
||||
Self = self(),
|
||||
ok = meck:new(push, [passthrough, no_link]),
|
||||
try
|
||||
ok = meck:expect(push, handle_message_create, fun(Params) ->
|
||||
Self ! {pushed, maps:get(<<"id">>, maps:get(message_data, Params))},
|
||||
ok
|
||||
end),
|
||||
State = lists:foldl(
|
||||
fun(Message, Acc) -> handle_message_create_event(Message, Acc) end,
|
||||
#{user_id => 10, sessions => Sessions, push_buffer => []},
|
||||
Messages
|
||||
),
|
||||
Buffered = [maps:get(message_id, Entry) || Entry <- maps:get(push_buffer, State)],
|
||||
{pushed_ids(), lists:sort(Buffered)}
|
||||
after
|
||||
meck:unload(push)
|
||||
end.
|
||||
|
||||
pushed_ids() ->
|
||||
receive
|
||||
{pushed, Id} -> [binary_to_integer(Id) | pushed_ids()]
|
||||
after 0 -> []
|
||||
end.
|
||||
|
||||
silent_dms_and_group_dms_are_not_pushed_test() ->
|
||||
Messages = [
|
||||
private_message(1, 1, 4096),
|
||||
private_message(2, 3, 4096 bor 4),
|
||||
private_message(3, 1, 0),
|
||||
private_message(4, 3, 4)
|
||||
],
|
||||
?assertEqual({[3, 4], []}, pushed_private_message_ids(#{}, Messages)).
|
||||
|
||||
silent_dms_and_group_dms_are_not_buffered_behind_an_active_desktop_test() ->
|
||||
Desktop = #{<<"desktop">> => #{status => online, afk => false, mobile => false}},
|
||||
Messages = [
|
||||
private_message(1, 1, 4096),
|
||||
private_message(2, 3, 4096),
|
||||
private_message(3, 1, 0),
|
||||
private_message(4, 3, 0)
|
||||
],
|
||||
with_gateway_config(#{}, fun() ->
|
||||
?assertEqual({[], [3, 4]}, pushed_private_message_ids(Desktop, Messages))
|
||||
end).
|
||||
|
||||
push_params(ChannelId, MessageId) ->
|
||||
#{
|
||||
message_data => #{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
-export([
|
||||
handle_message_create/1,
|
||||
handle_buffered_message_creates/1,
|
||||
sync_user_guild_settings/3,
|
||||
sync_user_guild_settings_local/3,
|
||||
sync_user_blocked_ids/2,
|
||||
@@ -15,7 +16,8 @@
|
||||
invalidate_user_subscriptions_local/1,
|
||||
invalidate_user_badge_count_local/1,
|
||||
invalidate_user_badge_counts_local/1,
|
||||
clear_channel_notifications/3
|
||||
clear_channel_notifications/3,
|
||||
clear_notifications_enabled/0
|
||||
]).
|
||||
-export([get_cache_stats/0]).
|
||||
-export([push_owner_key/1]).
|
||||
@@ -29,7 +31,14 @@
|
||||
-define(CNT_FETCH_FAILURES, push_blocked_ids_fetch_failures).
|
||||
-define(CNT_SUPPRESSED, push_blocked_ids_suppressed).
|
||||
-define(CNT_BUDGET_EXHAUSTED, push_blocked_ids_budget_exhausted).
|
||||
-define(CNT_READ_STATE_SUPPRESSED, push_read_state_suppressed).
|
||||
-define(CNT_READ_STATE_FAILURES, push_read_state_fetch_failures).
|
||||
-define(CNT_READ_STATE_SKIPPED, push_read_state_fetch_skipped).
|
||||
-define(MAX_FETCH_RPCS, 8).
|
||||
-define(READ_STATE_FETCH_TIMEOUT_MS, 2000).
|
||||
-define(READ_STATE_FETCH_MAX_POOL_PCT, 50).
|
||||
-define(READ_STATE_FETCH_SLOTS, read_state_fetches_in_flight).
|
||||
-define(MAX_READ_STATE_FETCHES, 64).
|
||||
-define(DEFAULT_FETCH_USERS, 2000).
|
||||
-define(MAX_FETCH_USERS, 5000).
|
||||
-define(DEFAULT_FETCH_CHUNK, 500).
|
||||
@@ -39,6 +48,8 @@
|
||||
-type state() :: #{
|
||||
max_entries := non_neg_integer()
|
||||
}.
|
||||
-type read_key() :: {integer(), integer(), integer()}.
|
||||
-type read_marks() :: #{{integer(), integer()} => non_neg_integer()}.
|
||||
|
||||
-spec start_link() -> {ok, pid()} | {error, term()} | ignore.
|
||||
start_link() ->
|
||||
@@ -115,6 +126,17 @@ handle_message_create(Params) ->
|
||||
false -> ok
|
||||
end.
|
||||
|
||||
-spec handle_buffered_message_creates([map()]) -> ok.
|
||||
handle_buffered_message_creates(ParamsList) ->
|
||||
case is_push_active() of
|
||||
true ->
|
||||
{Stale, Fresh} = split_stale_buffered(ParamsList),
|
||||
lists:foreach(fun handle_message_create/1, Fresh),
|
||||
spawn_read_state_check(Stale);
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec sync_user_guild_settings(integer(), integer(), map()) -> ok.
|
||||
sync_user_guild_settings(UserId, GuildId, Settings) ->
|
||||
maybe_cast(UserId, {sync_user_guild_settings, UserId, GuildId, Settings}).
|
||||
@@ -254,6 +276,149 @@ do_handle_message_create(Params) ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec split_stale_buffered([map()]) -> {[{read_key(), map()}], [map()]}.
|
||||
split_stale_buffered(ParamsList) ->
|
||||
lists:foldr(
|
||||
fun(Params, {Stale, Fresh}) ->
|
||||
Unbuffered = maps:remove(buffered_at, Params),
|
||||
case stale_read_key(Params) of
|
||||
{ok, Key} -> {[{Key, Unbuffered} | Stale], Fresh};
|
||||
none -> {Stale, [Unbuffered | Fresh]}
|
||||
end
|
||||
end,
|
||||
{[], []},
|
||||
ParamsList
|
||||
).
|
||||
|
||||
-spec stale_read_key(map()) -> {ok, read_key()} | none.
|
||||
stale_read_key(#{buffered_at := BufferedAt} = Params) when is_integer(BufferedAt) ->
|
||||
case
|
||||
erlang:system_time(millisecond) - BufferedAt >= push_outbox:max_age_ms() andalso
|
||||
push_message_params:context(Params)
|
||||
of
|
||||
{ok, #{user_ids := [UserId], channel_id := ChannelId, message_id := MessageId}} ->
|
||||
{ok, {UserId, ChannelId, MessageId}};
|
||||
_ ->
|
||||
none
|
||||
end;
|
||||
stale_read_key(_Params) ->
|
||||
none.
|
||||
|
||||
-spec spawn_read_state_check([{read_key(), map()}]) -> ok.
|
||||
spawn_read_state_check([]) ->
|
||||
ok;
|
||||
spawn_read_state_check(Stale) ->
|
||||
Channels = lists:usort([{UserId, ChannelId} || {{UserId, ChannelId, _}, _Params} <- Stale]),
|
||||
case read_state_fetch_allowed(length(Channels)) of
|
||||
true ->
|
||||
_ = spawn(fun() -> publish_unread(Channels, Stale) end),
|
||||
ok;
|
||||
false ->
|
||||
lists:foreach(fun(Entry) -> publish_unless_read(#{}, Entry) end, Stale)
|
||||
end.
|
||||
|
||||
-spec publish_unread([{integer(), integer()}], [{read_key(), map()}]) -> ok.
|
||||
publish_unread(Channels, Stale) ->
|
||||
try
|
||||
ReadMarks =
|
||||
try
|
||||
fetch_each_read_mark(Channels, #{})
|
||||
catch
|
||||
_:_ -> #{}
|
||||
end,
|
||||
lists:foreach(fun(Entry) -> publish_unless_read(ReadMarks, Entry) end, Stale)
|
||||
after
|
||||
release_read_state_fetch_slot()
|
||||
end.
|
||||
|
||||
-spec publish_unless_read(read_marks(), {read_key(), map()}) -> ok.
|
||||
publish_unless_read(ReadMarks, {{UserId, ChannelId, MessageId}, Params}) ->
|
||||
case MessageId =< maps:get({UserId, ChannelId}, ReadMarks, 0) of
|
||||
true -> bump_counter(?CNT_READ_STATE_SUPPRESSED);
|
||||
false -> handle_message_create(Params)
|
||||
end.
|
||||
|
||||
-spec read_state_fetch_allowed(non_neg_integer()) -> boolean().
|
||||
read_state_fetch_allowed(Fetches) ->
|
||||
case
|
||||
push_worker_pool:utilization_pct() < ?READ_STATE_FETCH_MAX_POOL_PCT andalso
|
||||
acquire_read_state_fetch_slot()
|
||||
of
|
||||
true ->
|
||||
true;
|
||||
false ->
|
||||
bump_counter(?CNT_READ_STATE_SKIPPED, Fetches),
|
||||
false
|
||||
end.
|
||||
|
||||
-spec acquire_read_state_fetch_slot() -> boolean().
|
||||
acquire_read_state_fetch_slot() ->
|
||||
try
|
||||
ets:update_counter(
|
||||
?PUSH_COUNTER_TABLE, ?READ_STATE_FETCH_SLOTS, {2, 1}, {?READ_STATE_FETCH_SLOTS, 0}
|
||||
)
|
||||
of
|
||||
InFlight when InFlight > ?MAX_READ_STATE_FETCHES ->
|
||||
release_read_state_fetch_slot(),
|
||||
false;
|
||||
_InFlight ->
|
||||
true
|
||||
catch
|
||||
error:badarg -> false
|
||||
end.
|
||||
|
||||
-spec release_read_state_fetch_slot() -> ok.
|
||||
release_read_state_fetch_slot() ->
|
||||
try ets:update_counter(?PUSH_COUNTER_TABLE, ?READ_STATE_FETCH_SLOTS, {2, -1, 0, 0}) of
|
||||
_InFlight -> ok
|
||||
catch
|
||||
error:badarg -> ok
|
||||
end.
|
||||
|
||||
-spec fetch_each_read_mark([{integer(), integer()}], read_marks()) -> read_marks().
|
||||
fetch_each_read_mark([], ReadMarks) ->
|
||||
ReadMarks;
|
||||
fetch_each_read_mark([{UserId, ChannelId} = Key | Rest], ReadMarks) ->
|
||||
case fetch_read_message_id(UserId, ChannelId) of
|
||||
{ok, ReadMessageId} ->
|
||||
fetch_each_read_mark(Rest, ReadMarks#{Key => ReadMessageId});
|
||||
error ->
|
||||
bump_counter(?CNT_READ_STATE_SKIPPED, length(Rest)),
|
||||
ReadMarks
|
||||
end.
|
||||
|
||||
-spec fetch_read_message_id(integer(), integer()) -> {ok, non_neg_integer()} | error.
|
||||
fetch_read_message_id(UserId, ChannelId) ->
|
||||
Request = #{
|
||||
<<"type">> => <<"get_read_state">>,
|
||||
<<"user_id">> => integer_to_binary(UserId),
|
||||
<<"channel_id">> => integer_to_binary(ChannelId)
|
||||
},
|
||||
case read_state_call(Request) of
|
||||
{ok, #{<<"last_message_id">> := LastMessageId}} ->
|
||||
{ok, read_message_id(snowflake_id:parse_maybe(LastMessageId))};
|
||||
{ok, _Data} ->
|
||||
{ok, 0};
|
||||
{error, Reason} ->
|
||||
bump_counter(?CNT_READ_STATE_FAILURES),
|
||||
logger:debug("Push: read state fetch failed", #{
|
||||
reason => Reason, user_id => UserId, channel_id => ChannelId
|
||||
}),
|
||||
error
|
||||
end.
|
||||
|
||||
-spec read_state_call(map()) -> {ok, term()} | {error, term()}.
|
||||
read_state_call(Request) ->
|
||||
try
|
||||
rpc_client:call(Request, ?READ_STATE_FETCH_TIMEOUT_MS)
|
||||
catch
|
||||
Class:Reason -> {error, {Class, Reason}}
|
||||
end.
|
||||
|
||||
-spec read_message_id(integer() | undefined) -> non_neg_integer().
|
||||
read_message_id(MessageId) when is_integer(MessageId), MessageId > 0 -> MessageId;
|
||||
read_message_id(_MessageId) -> 0.
|
||||
|
||||
-spec do_handle_message_create_context(push_message_params:context()) -> ok.
|
||||
do_handle_message_create_context(Context) ->
|
||||
#{
|
||||
@@ -625,7 +790,11 @@ cache_stats_with_counters() ->
|
||||
push_loss_counters() ->
|
||||
#{
|
||||
counters => counter_table_status(),
|
||||
worker_pool_dropped => read_counter(?CNT_WORKER_POOL)
|
||||
worker_pool_dropped => read_counter(?CNT_WORKER_POOL),
|
||||
read_state_suppressed => read_counter(?CNT_READ_STATE_SUPPRESSED),
|
||||
read_state_fetch_failures => read_counter(?CNT_READ_STATE_FAILURES),
|
||||
read_state_fetch_skipped => read_counter(?CNT_READ_STATE_SKIPPED),
|
||||
read_state_fetches_in_flight => read_counter(?READ_STATE_FETCH_SLOTS)
|
||||
}.
|
||||
|
||||
-spec counter_table_status() -> live | unavailable.
|
||||
@@ -826,6 +995,207 @@ filter_eligible_users_fetches_large_metadata_once_test() ->
|
||||
)
|
||||
end.
|
||||
|
||||
a_dm_buffered_past_the_outbox_window_and_read_since_is_not_published_test() ->
|
||||
Suppressed = read_counter_value(?CNT_READ_STATE_SUPPRESSED),
|
||||
Published = with_buffered_dms(
|
||||
[{stale_buffered_at(), 123, 456}], read_through(#{123 => <<"456">>})
|
||||
),
|
||||
?assertEqual({0, 1}, Published),
|
||||
?assertEqual(Suppressed + 1, read_counter_value(?CNT_READ_STATE_SUPPRESSED)).
|
||||
|
||||
a_dm_buffered_past_the_outbox_window_and_still_unread_is_published_test() ->
|
||||
Published = with_buffered_dms(
|
||||
[{stale_buffered_at(), 123, 456}], read_through(#{123 => <<"455">>})
|
||||
),
|
||||
?assertEqual({1, 1}, Published).
|
||||
|
||||
a_dm_buffered_past_the_outbox_window_without_a_read_state_is_published_test() ->
|
||||
Published = with_buffered_dms(
|
||||
[{stale_buffered_at(), 123, 456}], fun(_ChannelId) ->
|
||||
{ok, #{<<"last_message_id">> => null}}
|
||||
end
|
||||
),
|
||||
?assertEqual({1, 1}, Published).
|
||||
|
||||
a_dm_buffered_past_the_outbox_window_is_published_when_the_read_state_fetch_fails_test() ->
|
||||
Published = with_buffered_dms(
|
||||
[{stale_buffered_at(), 123, 456}], fun(_ChannelId) -> {error, timeout} end
|
||||
),
|
||||
?assertEqual({1, 1}, Published).
|
||||
|
||||
a_dm_buffered_within_the_outbox_window_is_published_without_a_read_state_fetch_test() ->
|
||||
Published = with_buffered_dms(
|
||||
[{erlang:system_time(millisecond), 123, 456}], fun(_ChannelId) ->
|
||||
{error, unexpected}
|
||||
end
|
||||
),
|
||||
?assertEqual({1, 0}, Published).
|
||||
|
||||
a_buffered_dm_without_a_buffer_time_is_published_without_a_read_state_fetch_test() ->
|
||||
Published = with_buffered_dms(
|
||||
[{undefined, 123, 456}], fun(_ChannelId) -> {error, unexpected} end
|
||||
),
|
||||
?assertEqual({1, 0}, Published).
|
||||
|
||||
a_dm_buffered_past_the_outbox_window_is_published_without_a_fetch_when_the_pool_is_busy_test() ->
|
||||
Skipped = read_counter_value(?CNT_READ_STATE_SKIPPED),
|
||||
ok = meck:new(push_worker_pool, [passthrough, no_link]),
|
||||
try
|
||||
ok = meck:expect(push_worker_pool, utilization_pct, fun() ->
|
||||
?READ_STATE_FETCH_MAX_POOL_PCT
|
||||
end),
|
||||
Published = with_buffered_dms(
|
||||
[{stale_buffered_at(), 123, 456}], read_through(#{123 => <<"456">>})
|
||||
),
|
||||
?assertEqual({1, 0}, Published)
|
||||
after
|
||||
meck:unload(push_worker_pool)
|
||||
end,
|
||||
?assertEqual(Skipped + 1, read_counter_value(?CNT_READ_STATE_SKIPPED)).
|
||||
|
||||
a_full_buffer_of_read_dms_is_not_published_by_its_own_flush_test() ->
|
||||
Entries = [{stale_buffered_at(), 123, 1000 + N} || N <- lists:seq(1, 128)],
|
||||
Published = with_buffered_dms(Entries, fun(123) ->
|
||||
timer:sleep(50),
|
||||
{ok, #{<<"last_message_id">> => <<"999999">>}}
|
||||
end),
|
||||
?assertEqual({0, 1}, Published).
|
||||
|
||||
a_flush_fetches_each_channel_read_state_once_test() ->
|
||||
Entries = [
|
||||
{stale_buffered_at(), 123, 456},
|
||||
{stale_buffered_at(), 124, 460},
|
||||
{stale_buffered_at(), 123, 470}
|
||||
],
|
||||
Published = with_buffered_dms(Entries, read_through(#{123 => <<"456">>, 124 => <<"459">>})),
|
||||
?assertEqual({2, 2}, Published).
|
||||
|
||||
a_failed_read_state_fetch_publishes_the_rest_of_the_flush_without_fetching_test() ->
|
||||
Entries = [
|
||||
{stale_buffered_at(), 123, 456},
|
||||
{stale_buffered_at(), 124, 460},
|
||||
{stale_buffered_at(), 125, 470}
|
||||
],
|
||||
Skipped = read_counter_value(?CNT_READ_STATE_SKIPPED),
|
||||
Published = with_buffered_dms(Entries, fun(_ChannelId) -> {error, timeout} end),
|
||||
?assertEqual({3, 1}, Published),
|
||||
?assertEqual(Skipped + 2, read_counter_value(?CNT_READ_STATE_SKIPPED)).
|
||||
|
||||
a_flush_publishes_fresh_and_unread_stale_dms_and_drops_read_ones_test() ->
|
||||
Stale = stale_buffered_at(),
|
||||
Params = [
|
||||
buffered_dm_params(erlang:system_time(millisecond), 123, 500),
|
||||
buffered_dm_params(Stale, 124, 456),
|
||||
buffered_dm_params(Stale, 125, 470)
|
||||
],
|
||||
{Published, Requests} = with_buffered_dm_push(
|
||||
read_through(#{124 => <<"456">>, 125 => <<"469">>}),
|
||||
fun() ->
|
||||
ok = handle_buffered_message_creates(Params),
|
||||
lists:sort(published_message_ids(1000))
|
||||
end
|
||||
),
|
||||
?assertEqual([470, 500], Published),
|
||||
?assertEqual(2, Requests).
|
||||
|
||||
stale_buffered_at() ->
|
||||
erlang:system_time(millisecond) - push_outbox:max_age_ms() - 1000.
|
||||
|
||||
read_through(ReadMessageIds) ->
|
||||
fun(ChannelId) -> {ok, #{<<"last_message_id">> => maps:get(ChannelId, ReadMessageIds)}} end.
|
||||
|
||||
read_counter_value(Key) ->
|
||||
ok = push_worker_pool:init_counter(),
|
||||
case read_counter(Key) of
|
||||
Value when is_integer(Value) -> Value;
|
||||
unavailable -> 0
|
||||
end.
|
||||
|
||||
with_buffered_dms(Entries, ReadStateReply) ->
|
||||
with_buffered_dm_push(ReadStateReply, fun() ->
|
||||
{Stale, Fresh} = split_stale_buffered([
|
||||
buffered_dm_params(BufferedAt, ChannelId, MessageId)
|
||||
|| {BufferedAt, ChannelId, MessageId} <- Entries
|
||||
]),
|
||||
lists:foreach(fun handle_message_create/1, Fresh),
|
||||
ok = spawn_read_state_check(Stale),
|
||||
ok = wait_for_read_state_fetches(100),
|
||||
length(published_message_ids(0))
|
||||
end).
|
||||
|
||||
wait_for_read_state_fetches(0) ->
|
||||
erlang:error(read_state_fetch_still_in_flight);
|
||||
wait_for_read_state_fetches(Attempts) ->
|
||||
case read_counter(?READ_STATE_FETCH_SLOTS) of
|
||||
InFlight when is_integer(InFlight), InFlight > 0 ->
|
||||
timer:sleep(20),
|
||||
wait_for_read_state_fetches(Attempts - 1);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
with_buffered_dm_push(ReadStateReply, Fun) ->
|
||||
ok = push_worker_pool:init_counter(),
|
||||
Self = self(),
|
||||
Modules = [fluxer_gateway_env, gateway_node_router, rpc_client],
|
||||
lists:foreach(fun(Module) -> ok = meck:new(Module, [passthrough, no_link]) end, Modules),
|
||||
try
|
||||
ok = meck:expect(fluxer_gateway_env, get, fun
|
||||
(push_enabled) -> true;
|
||||
(Key) -> meck:passthrough([Key])
|
||||
end),
|
||||
ok = meck:expect(gateway_node_router, owner_node_result, fun(_Key, push) ->
|
||||
{ok, node()}
|
||||
end),
|
||||
ok = meck:expect(rpc_client, call, fun(Request, ?READ_STATE_FETCH_TIMEOUT_MS) ->
|
||||
Self ! {read_state_request, Request},
|
||||
ReadStateReply(binary_to_integer(maps:get(<<"channel_id">>, Request)))
|
||||
end),
|
||||
Published = with_registered_push(Fun),
|
||||
Requests = meck:num_calls(rpc_client, call, ['_', '_']),
|
||||
assert_read_state_requests(Requests),
|
||||
{Published, Requests}
|
||||
after
|
||||
lists:foreach(fun meck:unload/1, Modules)
|
||||
end.
|
||||
|
||||
published_message_ids(Timeout) ->
|
||||
receive
|
||||
{'$gen_cast', {handle_message_create, Params}} ->
|
||||
?assertNot(maps:is_key(buffered_at, Params)),
|
||||
MessageData = maps:get(message_data, Params),
|
||||
[
|
||||
binary_to_integer(maps:get(<<"id">>, MessageData))
|
||||
| published_message_ids(Timeout)
|
||||
]
|
||||
after Timeout -> []
|
||||
end.
|
||||
|
||||
buffered_dm_params(BufferedAt, ChannelId, MessageId) ->
|
||||
#{
|
||||
message_data => #{
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"id">> => integer_to_binary(MessageId),
|
||||
<<"channel_type">> => 1
|
||||
},
|
||||
user_ids => [71],
|
||||
guild_id => 0,
|
||||
author_id => 7,
|
||||
buffered_at => BufferedAt
|
||||
}.
|
||||
|
||||
assert_read_state_requests(0) ->
|
||||
ok;
|
||||
assert_read_state_requests(Count) ->
|
||||
receive
|
||||
{read_state_request, Request} ->
|
||||
?assertMatch(
|
||||
#{<<"type">> := <<"get_read_state">>, <<"user_id">> := <<"71">>}, Request
|
||||
),
|
||||
assert_read_state_requests(Count - 1)
|
||||
after 0 -> erlang:error(no_read_state_request)
|
||||
end.
|
||||
|
||||
blocked_ids_fetch_defaults_are_bounded_test() ->
|
||||
?assertEqual(?DEFAULT_FETCH_USERS, blocked_ids_fetch_max_users()),
|
||||
?assertEqual(?DEFAULT_FETCH_CHUNK, blocked_ids_fetch_chunk()).
|
||||
|
||||
@@ -92,17 +92,6 @@ is_user_blocked(UserId, AuthorId) ->
|
||||
-spec check_user_guild_settings(
|
||||
integer(), integer(), integer(), map(), integer(), map(), map(), map() | undefined
|
||||
) -> boolean().
|
||||
check_user_guild_settings(
|
||||
_UserId,
|
||||
0,
|
||||
_ChannelId,
|
||||
_MessageData,
|
||||
_GuildDefaultNotifications,
|
||||
_UserRolesMap,
|
||||
_ConnectedUsers,
|
||||
_LargeGuildMetadata
|
||||
) ->
|
||||
true;
|
||||
check_user_guild_settings(
|
||||
UserId,
|
||||
GuildId,
|
||||
@@ -114,8 +103,7 @@ check_user_guild_settings(
|
||||
LargeGuildMetadata
|
||||
) ->
|
||||
Settings = fetch_settings(UserId, GuildId),
|
||||
MobilePush = get_boolean_setting(mobile_push, Settings, true),
|
||||
case MobilePush of
|
||||
case mobile_push_allowed(GuildId, Settings) of
|
||||
false ->
|
||||
false;
|
||||
true ->
|
||||
@@ -132,6 +120,12 @@ check_user_guild_settings(
|
||||
)
|
||||
end.
|
||||
|
||||
-spec mobile_push_allowed(integer(), map()) -> boolean().
|
||||
mobile_push_allowed(0, _Settings) ->
|
||||
true;
|
||||
mobile_push_allowed(_GuildId, Settings) ->
|
||||
get_boolean_setting(mobile_push, Settings, true).
|
||||
|
||||
-spec fetch_settings(integer(), integer()) -> map().
|
||||
fetch_settings(UserId, GuildId) ->
|
||||
case push_ets_cache:get_user_guild_settings(UserId, GuildId) of
|
||||
@@ -446,6 +440,110 @@ mention_here_requires_connected_user_test() ->
|
||||
?assertEqual(false, is_user_mentioned(123, MessageData, #{}, #{}, #{})),
|
||||
?assertEqual(true, is_user_mentioned(123, MessageData, #{}, #{}, #{123 => true})).
|
||||
|
||||
-define(DM_USER, 920001).
|
||||
-define(DM_AUTHOR, 920002).
|
||||
-define(DM_CHANNEL, 920005).
|
||||
|
||||
dm_eligible(ChannelType, PrivateSettings) ->
|
||||
push_ets_cache:init(),
|
||||
ok = push_ets_cache:put_user_guild_settings(?DM_USER, 0, PrivateSettings),
|
||||
try
|
||||
is_eligible_for_push(
|
||||
?DM_USER,
|
||||
?DM_AUTHOR,
|
||||
0,
|
||||
?DM_CHANNEL,
|
||||
#{<<"channel_type">> => ChannelType},
|
||||
0,
|
||||
#{},
|
||||
#{},
|
||||
undefined
|
||||
)
|
||||
after
|
||||
push_ets_cache:delete_user_guild_settings(?DM_USER, 0)
|
||||
end.
|
||||
|
||||
dm_override(Override) ->
|
||||
#{<<"channel_overrides">> => #{integer_to_binary(?DM_CHANNEL) => Override}}.
|
||||
|
||||
mute_ending_in(OffsetMs) ->
|
||||
Ms = erlang:system_time(millisecond) + OffsetMs,
|
||||
#{
|
||||
<<"end_time">> => list_to_binary(
|
||||
calendar:system_time_to_rfc3339(Ms, [{unit, millisecond}, {offset, "Z"}])
|
||||
)
|
||||
}.
|
||||
|
||||
a_muted_dm_is_not_pushed_test() ->
|
||||
Muted = dm_override(#{<<"muted">> => true, <<"mute_config">> => null}),
|
||||
?assertEqual(false, dm_eligible(1, Muted)).
|
||||
|
||||
a_muted_group_dm_is_not_pushed_test() ->
|
||||
Muted = dm_override(#{<<"muted">> => true, <<"mute_config">> => mute_ending_in(60000)}),
|
||||
?assertEqual(false, dm_eligible(3, Muted)).
|
||||
|
||||
a_dm_whose_mute_expired_is_pushed_test() ->
|
||||
Expired = dm_override(#{<<"muted">> => true, <<"mute_config">> => mute_ending_in(-60000)}),
|
||||
?assertEqual(true, dm_eligible(1, Expired)),
|
||||
?assertEqual(true, dm_eligible(3, Expired)).
|
||||
|
||||
an_unmuted_dm_is_pushed_test() ->
|
||||
?assertEqual(true, dm_eligible(1, #{})),
|
||||
?assertEqual(true, dm_eligible(3, #{})),
|
||||
Other = #{<<"channel_overrides">> => #{<<"1">> => #{<<"muted">> => true}}},
|
||||
?assertEqual(true, dm_eligible(1, Other)).
|
||||
|
||||
a_dm_set_to_no_messages_is_not_pushed_test() ->
|
||||
?assertEqual(false, dm_eligible(1, dm_override(#{<<"message_notifications">> => 2}))),
|
||||
?assertEqual(false, dm_eligible(3, #{<<"message_notifications">> => 2})).
|
||||
|
||||
a_dm_set_to_only_mentions_is_still_pushed_test() ->
|
||||
?assertEqual(true, dm_eligible(1, dm_override(#{<<"message_notifications">> => 1}))),
|
||||
?assertEqual(true, dm_eligible(3, #{<<"message_notifications">> => 1})).
|
||||
|
||||
all_dms_muted_are_not_pushed_test() ->
|
||||
?assertEqual(false, dm_eligible(1, #{<<"muted">> => true})).
|
||||
|
||||
dm_pushes_ignore_the_private_mobile_push_setting_test() ->
|
||||
?assertEqual(true, dm_eligible(1, #{<<"mobile_push">> => false})).
|
||||
|
||||
dm_settings_are_fetched_once_for_the_private_scope_and_cached_test() ->
|
||||
push_ets_cache:init(),
|
||||
push_ets_cache:delete_user_guild_settings(?DM_USER, 0),
|
||||
Self = self(),
|
||||
Muted = dm_override(#{<<"muted">> => true, <<"mute_config">> => null}),
|
||||
ok = meck:new(rpc_client, [passthrough, no_link]),
|
||||
try
|
||||
ok = meck:expect(rpc_client, call, fun(Request) ->
|
||||
Self ! {settings_request, Request},
|
||||
{ok, #{<<"user_guild_settings">> => [Muted]}}
|
||||
end),
|
||||
MessageData = #{<<"channel_type">> => 1},
|
||||
Check = fun() ->
|
||||
is_eligible_for_push(
|
||||
?DM_USER, ?DM_AUTHOR, 0, ?DM_CHANNEL, MessageData, 0, #{}, #{}, undefined
|
||||
)
|
||||
end,
|
||||
?assertEqual(false, Check()),
|
||||
?assertEqual(false, Check()),
|
||||
?assertEqual(1, meck:num_calls(rpc_client, call, '_')),
|
||||
receive
|
||||
{settings_request, Request} ->
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"type">> := <<"get_user_guild_settings">>,
|
||||
<<"guild_id">> := <<"0">>,
|
||||
<<"user_ids">> := [<<"920001">>]
|
||||
},
|
||||
Request
|
||||
)
|
||||
after 0 -> erlang:error(no_settings_request)
|
||||
end
|
||||
after
|
||||
meck:unload(rpc_client),
|
||||
push_ets_cache:delete_user_guild_settings(?DM_USER, 0)
|
||||
end.
|
||||
|
||||
mention_here_respects_suppress_everyone_test() ->
|
||||
MessageData = #{<<"mention_everyone">> => true, <<"mention_here">> => true},
|
||||
Settings = #{suppress_everyone => true},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
-module(push_job_publisher).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([publish_message/8, publish_clear/3]).
|
||||
-export([publish_message/8, publish_clear/3, clear_job/4]).
|
||||
-export([publish_ring/6, request/3]).
|
||||
|
||||
-define(SUBJECT_MESSAGE, <<"push.job.message">>).
|
||||
@@ -85,19 +85,37 @@ first_error({error, Reason}, _Second) ->
|
||||
|
||||
-spec publish_clear(integer(), integer(), integer()) -> ok | {error, term()}.
|
||||
publish_clear(UserId, ChannelId, MessageId) ->
|
||||
Job = #{
|
||||
publish(
|
||||
?SUBJECT_CLEAR,
|
||||
clear_fields(UserId, ChannelId, MessageId),
|
||||
clear_meta(UserId, ChannelId, MessageId)
|
||||
).
|
||||
|
||||
-spec clear_job(integer(), integer(), integer(), integer()) -> push_outbox:job().
|
||||
clear_job(UserId, ChannelId, MessageId, AfterMessageId) ->
|
||||
Job = (clear_fields(UserId, ChannelId, MessageId))#{
|
||||
<<"after_message_id">> => integer_to_binary(AfterMessageId)
|
||||
},
|
||||
outbox_job(
|
||||
?SUBJECT_CLEAR,
|
||||
Job,
|
||||
iolist_to_binary(json:encode(Job)),
|
||||
clear_meta(UserId, ChannelId, MessageId)
|
||||
).
|
||||
|
||||
-spec clear_fields(integer(), integer(), integer()) -> map().
|
||||
clear_fields(UserId, ChannelId, MessageId) ->
|
||||
#{
|
||||
<<"v">> => ?JOB_VERSION,
|
||||
<<"config_version">> => ?LEGACY_CONFIG_VERSION,
|
||||
<<"user_id">> => integer_to_binary(UserId),
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"message_id">> => integer_to_binary(MessageId)
|
||||
},
|
||||
publish(?SUBJECT_CLEAR, Job, #{
|
||||
kind => clear,
|
||||
user_ids => [UserId],
|
||||
channel_id => ChannelId,
|
||||
message_id => MessageId
|
||||
}).
|
||||
}.
|
||||
|
||||
-spec clear_meta(integer(), integer(), integer()) -> meta().
|
||||
clear_meta(UserId, ChannelId, MessageId) ->
|
||||
#{kind => clear, user_ids => [UserId], channel_id => ChannelId, message_id => MessageId}.
|
||||
|
||||
-spec publish_ring(integer(), integer(), integer(), integer(), integer(), map()) ->
|
||||
ok | {error, term()}.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
-module(push_message_params).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([context/1, owner_key/1]).
|
||||
-export([context/1, owner_key/1, suppresses_notifications/1]).
|
||||
|
||||
-export_type([context/0]).
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
-define(MESSAGE_TYPE_DEFAULT, 0).
|
||||
-define(MESSAGE_TYPE_REPLY, 19).
|
||||
-define(PUSHABLE_MESSAGE_TYPES, [?MESSAGE_TYPE_DEFAULT, ?MESSAGE_TYPE_REPLY]).
|
||||
-define(MESSAGE_FLAG_SUPPRESS_NOTIFICATIONS, 4096).
|
||||
|
||||
-type context() :: #{
|
||||
message_data := map(),
|
||||
@@ -118,10 +119,23 @@ validate(Context) ->
|
||||
-spec validate_message_type(integer(), context()) -> {ok, context()} | {error, term()}.
|
||||
validate_message_type(Type, Context) ->
|
||||
case lists:member(Type, ?PUSHABLE_MESSAGE_TYPES) of
|
||||
true -> {ok, Context};
|
||||
true -> validate_notifications(Context);
|
||||
false -> {error, {unpushable_message_type, Type}}
|
||||
end.
|
||||
|
||||
-spec validate_notifications(context()) -> {ok, context()} | {error, term()}.
|
||||
validate_notifications(#{message_data := MessageData} = Context) ->
|
||||
case suppresses_notifications(MessageData) of
|
||||
true -> {error, suppressed_notifications};
|
||||
false -> {ok, Context}
|
||||
end.
|
||||
|
||||
-spec suppresses_notifications(map()) -> boolean().
|
||||
suppresses_notifications(#{<<"flags">> := Flags}) when is_integer(Flags) ->
|
||||
Flags band ?MESSAGE_FLAG_SUPPRESS_NOTIFICATIONS =/= 0;
|
||||
suppresses_notifications(_MessageData) ->
|
||||
false.
|
||||
|
||||
-spec message_type(context()) -> integer().
|
||||
message_type(#{message_data := MessageData}) ->
|
||||
normalize_message_type(maps:get(<<"type">>, MessageData, ?MESSAGE_TYPE_DEFAULT)).
|
||||
@@ -230,4 +244,37 @@ context_treats_a_missing_message_type_as_pushable_test() ->
|
||||
context_treats_a_malformed_message_type_as_pushable_test() ->
|
||||
?assertMatch({ok, _}, context(params_with_message_type(<<"nonsense">>))).
|
||||
|
||||
params_with_flags(Flags, ChannelType, GuildId) ->
|
||||
Params = params_with_message_type(?MESSAGE_TYPE_DEFAULT),
|
||||
MessageData = maps:get(message_data, Params),
|
||||
Params#{
|
||||
message_data := MessageData#{<<"flags">> => Flags, <<"channel_type">> => ChannelType},
|
||||
guild_id := GuildId
|
||||
}.
|
||||
|
||||
context_rejects_a_silent_dm_test() ->
|
||||
?assertEqual(
|
||||
{error, suppressed_notifications}, context(params_with_flags(4096, 1, 0))
|
||||
).
|
||||
|
||||
context_rejects_a_silent_group_dm_test() ->
|
||||
?assertEqual(
|
||||
{error, suppressed_notifications}, context(params_with_flags(4096, 3, 0))
|
||||
).
|
||||
|
||||
context_rejects_a_silent_guild_message_that_mentions_the_recipient_test() ->
|
||||
Params = params_with_flags(4096 bor 4, 0, <<"1472200708085309475">>),
|
||||
MessageData = maps:get(message_data, Params),
|
||||
Mentioned = MessageData#{
|
||||
<<"mentions">> => [#{<<"id">> => <<"1474262819227156566">>}],
|
||||
<<"mention_everyone">> => true
|
||||
},
|
||||
?assertEqual(
|
||||
{error, suppressed_notifications}, context(Params#{message_data := Mentioned})
|
||||
).
|
||||
|
||||
context_allows_a_message_with_other_flags_test() ->
|
||||
?assertMatch({ok, _}, context(params_with_flags(4 bor 8192, 1, 0))),
|
||||
?assertMatch({ok, _}, context(params_with_flags(0, 3, 0))).
|
||||
|
||||
-endif.
|
||||
@@ -11,7 +11,8 @@
|
||||
note_session_active/1,
|
||||
record_dropped/2,
|
||||
stats/0,
|
||||
request_timeout_ms/0
|
||||
request_timeout_ms/0,
|
||||
max_age_ms/0
|
||||
]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
-export_type([job/0, kind/0]).
|
||||
@@ -54,11 +55,13 @@
|
||||
| sheds
|
||||
| truncations
|
||||
| skipped_active
|
||||
| followup_clears
|
||||
| enqueued.
|
||||
-type state() :: #{
|
||||
jobs := gb_trees:tree(non_neg_integer(), entry()),
|
||||
ready := queue:queue(non_neg_integer()),
|
||||
inflight := #{pid() => {reference(), reference(), entry()}},
|
||||
followups => #{pid() => #{integer() => integer()}},
|
||||
next_seq := non_neg_integer(),
|
||||
reads := #{{integer(), integer()} => {integer(), integer()}},
|
||||
active := #{integer() => {non_neg_integer(), integer()}},
|
||||
@@ -108,6 +111,10 @@ stats() ->
|
||||
request_timeout_ms() ->
|
||||
env_pos_integer(push_outbox_request_timeout_ms, ?DEFAULT_REQUEST_TIMEOUT_MS).
|
||||
|
||||
-spec max_age_ms() -> pos_integer().
|
||||
max_age_ms() ->
|
||||
env_pos_integer(push_outbox_max_age_ms, ?DEFAULT_MAX_AGE_MS).
|
||||
|
||||
-spec init([]) -> {ok, state()}.
|
||||
init([]) ->
|
||||
erlang:process_flag(fullsweep_after, 10),
|
||||
@@ -116,6 +123,7 @@ init([]) ->
|
||||
jobs => gb_trees:empty(),
|
||||
ready => queue:new(),
|
||||
inflight => #{},
|
||||
followups => #{},
|
||||
next_seq => 0,
|
||||
reads => #{},
|
||||
active => #{},
|
||||
@@ -124,7 +132,7 @@ init([]) ->
|
||||
max_queue => env_pos_integer(push_outbox_max_queue, ?DEFAULT_MAX_QUEUE),
|
||||
max_inflight => env_pos_integer(push_outbox_max_inflight, ?DEFAULT_MAX_INFLIGHT),
|
||||
request_timeout_ms => request_timeout_ms(),
|
||||
max_age_ms => env_pos_integer(push_outbox_max_age_ms, ?DEFAULT_MAX_AGE_MS),
|
||||
max_age_ms => max_age_ms(),
|
||||
retry_base_ms => app_pos_integer(push_outbox_retry_base_ms, ?DEFAULT_RETRY_BASE_MS)
|
||||
}}.
|
||||
|
||||
@@ -298,14 +306,53 @@ apply_read(UserId, ChannelId, MessageId, #{reads := Reads, jobs := Jobs} = State
|
||||
error -> MessageId
|
||||
end,
|
||||
State1 = State#{reads := Reads#{Key => {Watermark, now_ms()}}},
|
||||
lists:foldl(
|
||||
State2 = lists:foldl(
|
||||
fun({Seq, Entry}, Acc) ->
|
||||
truncate_entry(Seq, Entry, UserId, ChannelId, MessageId, Acc)
|
||||
end,
|
||||
State1,
|
||||
gb_trees:to_list(Jobs)
|
||||
),
|
||||
note_inflight_read(UserId, ChannelId, MessageId, State2).
|
||||
|
||||
-spec note_inflight_read(integer(), integer(), integer(), state()) -> state().
|
||||
note_inflight_read(UserId, ChannelId, MessageId, #{inflight := Inflight} = State) ->
|
||||
maps:fold(
|
||||
fun(Pid, {_MRef, _TRef, Entry}, Acc) ->
|
||||
note_followup(Pid, Entry, UserId, ChannelId, MessageId, Acc)
|
||||
end,
|
||||
State,
|
||||
Inflight
|
||||
).
|
||||
|
||||
-spec note_followup(pid(), entry(), integer(), integer(), integer(), state()) -> state().
|
||||
note_followup(
|
||||
Pid,
|
||||
#{
|
||||
kind := message,
|
||||
channel_id := ChannelId,
|
||||
message_id := JobMessageId,
|
||||
user_ids := UserIds
|
||||
},
|
||||
UserId,
|
||||
ChannelId,
|
||||
MessageId,
|
||||
State
|
||||
) when JobMessageId =< MessageId ->
|
||||
case lists:member(UserId, UserIds) of
|
||||
false -> State;
|
||||
true -> record_followup(Pid, UserId, MessageId, State)
|
||||
end;
|
||||
note_followup(_Pid, _Entry, _UserId, _ChannelId, _MessageId, State) ->
|
||||
State.
|
||||
|
||||
-spec record_followup(pid(), integer(), integer(), state()) -> state().
|
||||
record_followup(Pid, UserId, MessageId, State) ->
|
||||
Followups = maps:get(followups, State, #{}),
|
||||
Reads = maps:get(Pid, Followups, #{}),
|
||||
Watermark = max(MessageId, maps:get(UserId, Reads, MessageId)),
|
||||
State#{followups => Followups#{Pid => Reads#{UserId => Watermark}}}.
|
||||
|
||||
-spec truncate_entry(non_neg_integer(), entry(), integer(), integer(), integer(), state()) ->
|
||||
state().
|
||||
truncate_entry(
|
||||
@@ -374,11 +421,63 @@ finish_worker(Pid, Result, #{inflight := Inflight} = State) ->
|
||||
{{MRef, TRef, Entry}, Rest} ->
|
||||
erlang:demonitor(MRef, [flush]),
|
||||
_ = erlang:cancel_timer(TRef, [{async, true}, {info, false}]),
|
||||
handle_result(Result, Entry, State#{inflight := Rest});
|
||||
Followups = maps:get(followups, State, #{}),
|
||||
Reads = maps:get(Pid, Followups, #{}),
|
||||
State1 = State#{inflight := Rest, followups => maps:remove(Pid, Followups)},
|
||||
follow_up_clears(Entry, Reads, handle_result(Result, Entry, State1));
|
||||
error ->
|
||||
State
|
||||
end.
|
||||
|
||||
-spec follow_up_clears(entry(), #{integer() => integer()}, state()) -> state().
|
||||
follow_up_clears(_Entry, Reads, State) when map_size(Reads) =:= 0 ->
|
||||
State;
|
||||
follow_up_clears(#{channel_id := ChannelId, message_id := AfterMessageId}, Reads, State) ->
|
||||
case push:clear_notifications_enabled() of
|
||||
true ->
|
||||
maps:fold(
|
||||
fun(UserId, MessageId, Acc) ->
|
||||
follow_up_clear(UserId, ChannelId, MessageId, AfterMessageId, Acc)
|
||||
end,
|
||||
State,
|
||||
Reads
|
||||
);
|
||||
false ->
|
||||
State
|
||||
end.
|
||||
|
||||
-spec follow_up_clear(integer(), integer(), integer(), integer(), state()) -> state().
|
||||
follow_up_clear(UserId, ChannelId, MessageId, AfterMessageId, State) ->
|
||||
case pending_followup(UserId, ChannelId, State) of
|
||||
{ok, Pid} ->
|
||||
record_followup(Pid, UserId, MessageId, State);
|
||||
none ->
|
||||
Job = push_job_publisher:clear_job(UserId, ChannelId, MessageId, AfterMessageId),
|
||||
bump(followup_clears, 1, admit(Job, State))
|
||||
end.
|
||||
|
||||
-spec pending_followup(integer(), integer(), state()) -> {ok, pid()} | none.
|
||||
pending_followup(UserId, ChannelId, #{inflight := Inflight} = State) ->
|
||||
Pending = maps:filter(
|
||||
fun(Pid, Reads) ->
|
||||
is_map_key(UserId, Reads) andalso
|
||||
inflight_channel(Pid, Inflight) =:= {ok, ChannelId}
|
||||
end,
|
||||
maps:get(followups, State, #{})
|
||||
),
|
||||
case maps:keys(Pending) of
|
||||
[Pid | _] -> {ok, Pid};
|
||||
[] -> none
|
||||
end.
|
||||
|
||||
-spec inflight_channel(pid(), #{pid() => {reference(), reference(), entry()}}) ->
|
||||
{ok, integer()} | error.
|
||||
inflight_channel(Pid, Inflight) ->
|
||||
case maps:find(Pid, Inflight) of
|
||||
{ok, {_MRef, _TRef, #{channel_id := ChannelId}}} -> {ok, ChannelId};
|
||||
error -> error
|
||||
end.
|
||||
|
||||
-spec reply_result(term()) -> ok | {error, term()}.
|
||||
reply_result(ok) -> ok;
|
||||
reply_result({error, Reason}) -> {error, Reason};
|
||||
@@ -466,6 +565,7 @@ build_stats(#{jobs := Jobs, inflight := Inflight, counters := Counters, dropped
|
||||
sheds => 0,
|
||||
truncations => 0,
|
||||
skipped_active => 0,
|
||||
followup_clears => 0,
|
||||
enqueued => 0
|
||||
},
|
||||
Counters#{
|
||||
@@ -569,6 +669,7 @@ test_state(Reads) ->
|
||||
jobs => gb_trees:empty(),
|
||||
ready => queue:new(),
|
||||
inflight => #{},
|
||||
followups => #{},
|
||||
next_seq => 1,
|
||||
reads => Reads,
|
||||
active => #{},
|
||||
|
||||
@@ -449,6 +449,7 @@ finalize_guild_connection(GuildId, GuildPid, State, ReadyFun) ->
|
||||
finalize_guild_monitor(GuildId, GuildPid, Guilds0, State, ReadyFun) ->
|
||||
MonitorRef = monitor(process, GuildPid),
|
||||
Guilds = Guilds0#{GuildId => {GuildPid, MonitorRef}},
|
||||
ok = session_lifecycle:send_guild_push_hold({GuildPid, MonitorRef}, State),
|
||||
apply_ready_fun(GuildId, GuildPid, ReadyFun, State#{guilds => Guilds}).
|
||||
|
||||
-spec apply_ready_fun(
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
handle_resume/3,
|
||||
handle_resume_offline_timeout/2,
|
||||
handle_presence_update_cast/2,
|
||||
handle_initial_global_presences/2
|
||||
handle_initial_global_presences/2,
|
||||
send_guild_push_hold/2
|
||||
]).
|
||||
|
||||
-export_type([session_state/0, channel_id/0, user_id/0, session_id/0, seq/0, status/0]).
|
||||
@@ -352,6 +353,7 @@ handle_resume(Seq, SocketPid, #{seq := CurrentSeq} = State) ->
|
||||
NewState1 = replace_socket(SocketPid, NewState0),
|
||||
ReplyEvents = replay_missed_events_inline(MissedEvents, SocketPid),
|
||||
NewState = NewState1#{status => ResumeStatus, resume_status => ResumeStatus},
|
||||
ok = sync_guild_push_hold(State, NewState),
|
||||
NewState2 = ensure_presence_attached_on_resume(
|
||||
NewState, SessionId, ResumeStatus, Afk, Mobile
|
||||
),
|
||||
@@ -509,8 +511,40 @@ handle_presence_update_cast(Update, State) ->
|
||||
NewStatus, State#{status => NewStatus, afk => NewAfk, mobile => NewMobile}
|
||||
),
|
||||
send_presence_update(State, SessionId, NewStatus, NewAfk, NewMobile, Update),
|
||||
ok = sync_guild_push_hold(State, NewState),
|
||||
{noreply, NewState}.
|
||||
|
||||
-spec sync_guild_push_hold(session_state(), session_state()) -> ok.
|
||||
sync_guild_push_hold(OldState, NewState) ->
|
||||
notify_guild_push_hold(guild_push_hold(OldState), guild_push_hold(NewState), NewState).
|
||||
|
||||
-spec notify_guild_push_hold(boolean(), boolean(), session_state()) -> ok.
|
||||
notify_guild_push_hold(Hold, Hold, _State) ->
|
||||
ok;
|
||||
notify_guild_push_hold(_OldHold, _NewHold, State) ->
|
||||
maps:foreach(
|
||||
fun(_GuildId, GuildRef) -> send_guild_push_hold(GuildRef, State) end,
|
||||
maps:get(guilds, State, #{})
|
||||
).
|
||||
|
||||
-spec send_guild_push_hold(term(), session_state()) -> ok.
|
||||
send_guild_push_hold({GuildPid, _Ref}, #{id := SessionId} = State) when is_pid(GuildPid) ->
|
||||
case maps:get(bot, State, false) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
gen_server:cast(
|
||||
GuildPid, {set_session_push_hold, SessionId, guild_push_hold(State)}
|
||||
)
|
||||
end;
|
||||
send_guild_push_hold(_GuildRef, _State) ->
|
||||
ok.
|
||||
|
||||
-spec guild_push_hold(session_state()) -> boolean().
|
||||
guild_push_hold(State) ->
|
||||
lists:member(maps:get(status, State, online), [online, dnd, invisible]) andalso
|
||||
not maps:get(afk, State, false) andalso not maps:get(mobile, State, false).
|
||||
|
||||
-spec maybe_update_resume_status(status(), session_state()) -> session_state().
|
||||
maybe_update_resume_status(offline, State) ->
|
||||
State;
|
||||
|
||||
Reference in new issue
Block a user