Files
fluxer/fluxer_gateway/src/session/session_dispatch_presence.erl
T

441 lines
17 KiB
Erlang

%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(session_dispatch_presence).
-typing([eqwalizer]).
-export([
should_buffer_presence/3,
buffer_presence/3,
maybe_flush_pending_presences/3,
flush_all_pending_presences/1,
dispatch_presence_now/2,
maybe_sync_presence_targets/3,
sync_presence_targets/1,
event_changes_presence_targets/1,
presence_user_id/1,
relationship_target_id/1
]).
-export_type([session_state/0, event/0, user_id/0]).
-define(MAX_EVENT_BUFFER_SIZE, 4096).
-define(MAX_SINGLE_EVENT_BUFFER_BYTES, 2097152).
-define(MAX_TOTAL_BUFFER_BYTES, 16777216).
-define(MAX_PENDING_PRESENCE_BUFFER_SIZE, 2048).
-type session_state() :: session:session_state().
-type event() :: atom() | binary().
-type user_id() :: session:user_id().
-spec should_buffer_presence(event(), map(), session_state()) -> boolean().
should_buffer_presence(presence_update, Data, State) ->
case maps:get(suppress_presence_updates, State, true) of
true ->
true;
false ->
check_presence_scope(Data, State)
end;
should_buffer_presence(_, _, _) ->
false.
-spec check_presence_scope(map(), session_state()) -> boolean().
check_presence_scope(Data, State) ->
case maps:find(<<"guild_id">>, Data) of
error ->
UserId = presence_user_id(Data),
check_user_presence_buffering(UserId, State);
{ok, GuildIdValue} ->
should_buffer_guild_presence(GuildIdValue, State)
end.
-spec should_buffer_guild_presence(term(), session_state()) -> boolean().
should_buffer_guild_presence(GuildIdValue, State) ->
case snowflake_id:parse_maybe(GuildIdValue) of
GuildId when is_integer(GuildId) ->
not maps:is_key(GuildId, maps:get(guilds, State, #{}));
undefined ->
true
end.
-spec check_user_presence_buffering(user_id() | undefined, session_state()) -> boolean().
check_user_presence_buffering(undefined, _State) ->
false;
check_user_presence_buffering(UserId, State) ->
Relationships = maps:get(relationships, State, #{}),
IsRelationship = relationship_allows_presence(UserId, Relationships),
IsGroupDmRecipient = is_group_dm_recipient(UserId, State),
not (IsRelationship orelse IsGroupDmRecipient).
-spec relationship_allows_presence(user_id(), #{user_id() => integer()}) -> boolean().
relationship_allows_presence(UserId, Relationships) when
is_integer(UserId), is_map(Relationships)
->
case maps:get(UserId, Relationships, undefined) of
1 -> true;
_ -> false
end;
relationship_allows_presence(_, _) ->
false.
-spec is_group_dm_recipient(user_id(), session_state()) -> boolean().
is_group_dm_recipient(UserId, State) when is_map(State) ->
GroupDmRecipients = presence_targets:group_dm_recipients_from_state(State),
maps:fold(
fun
(_, Recipients, false) -> maps:is_key(UserId, Recipients);
(_, _, true) -> true
end,
false,
GroupDmRecipients
).
-spec buffer_presence(event(), map(), session_state()) -> session_state().
buffer_presence(Event, Data, State) ->
Pending = ensure_queue(maps:get(pending_presences, State, [])),
UserId = presence_user_id(Data),
Entry = #{event => Event, data => Data, user_id => UserId},
Trimmed = trim_queue_from_tail(Pending, ?MAX_PENDING_PRESENCE_BUFFER_SIZE - 1),
NewPending = queue:in_r(Entry, Trimmed),
State#{pending_presences => NewPending}.
-spec maybe_flush_pending_presences(event(), map(), session_state()) ->
{session_state(), [user_id()]}.
maybe_flush_pending_presences(relationship_add, Data, State) ->
maybe_flush_relationship_pending_presences(Data, State);
maybe_flush_pending_presences(relationship_update, Data, State) ->
maybe_flush_relationship_pending_presences(Data, State);
maybe_flush_pending_presences(channel_create, Data, State) ->
flush_dm_channel_pending_presences(Data, State);
maybe_flush_pending_presences(channel_update, Data, State) ->
flush_dm_channel_pending_presences(Data, State);
maybe_flush_pending_presences(channel_recipient_add, Data, State) ->
flush_added_recipient_pending_presences(Data, State);
maybe_flush_pending_presences(_, _, State) ->
{State, []}.
-spec flush_dm_channel_pending_presences(map(), session_state()) ->
{session_state(), [user_id()]}.
flush_dm_channel_pending_presences(Data, State) ->
SelfUserId = maps:get(user_id, State, undefined),
RecipientIds = presence_targets:group_dm_channel_recipient_ids(Data, SelfUserId),
flush_pending_presences_for_ids(RecipientIds, State).
-spec flush_added_recipient_pending_presences(map(), session_state()) ->
{session_state(), [user_id()]}.
flush_added_recipient_pending_presences(Data, State) ->
case presence_user_id(Data) of
undefined -> {State, []};
RecipientId -> flush_pending_presences_for_ids([RecipientId], State)
end.
-spec flush_pending_presences_for_ids([user_id()], session_state()) ->
{session_state(), [user_id()]}.
flush_pending_presences_for_ids(UserIds, State) ->
lists:foldl(
fun(UserId, {AccState, Flushed}) ->
{flush_pending_presences(UserId, AccState), [UserId | Flushed]}
end,
{State, []},
UserIds
).
-spec maybe_flush_relationship_pending_presences(map(), session_state()) ->
{session_state(), [user_id()]}.
maybe_flush_relationship_pending_presences(Data, State) ->
case maps:get(<<"type">>, Data, undefined) of
1 ->
TargetId = relationship_target_id(Data),
{flush_pending_presences(TargetId, State), flushed_id_list(TargetId)};
_ ->
{State, []}
end.
-spec flushed_id_list(user_id() | undefined) -> [user_id()].
flushed_id_list(undefined) -> [];
flushed_id_list(Id) -> [Id].
-spec flush_pending_presences(user_id() | undefined, session_state()) -> session_state().
flush_pending_presences(undefined, State) ->
State;
flush_pending_presences(UserId, State) ->
PendingQ = ensure_queue(maps:get(pending_presences, State, [])),
PendingList = queue:to_list(PendingQ),
{ToSend, RemainingList} = lists:partition(
fun(P) -> maps:get(user_id, P, undefined) =:= UserId end,
PendingList
),
FlushedState = lists:foldl(
fun dispatch_presence_now/2, State, ToSend
),
FlushedState#{pending_presences => queue:from_list(RemainingList)}.
-spec dispatch_presence_now(map(), session_state()) -> session_state().
dispatch_presence_now(P, State) ->
Event = maps:get(event, P),
Data = maps:get(data, P),
Seq = maps:get(seq, State),
Buffer = maps:get(buffer, State),
SocketPid = maps:get(socket_pid, State, undefined),
NewSeq = Seq + 1,
Request = #{event => Event, data => Data, seq => NewSeq},
Deque =
case is_list(Buffer) of
true ->
limited_deque:from_list(
Buffer, ?MAX_EVENT_BUFFER_SIZE, ?MAX_TOTAL_BUFFER_BYTES
);
false ->
Buffer
end,
NewBuffer = limited_deque:push(Request, Deque),
send_to_socket(SocketPid, Event, Data, NewSeq),
State#{
seq => NewSeq,
buffer => NewBuffer,
buffer_bytes => limited_deque:bytes(NewBuffer)
}.
-spec flush_all_pending_presences(session_state()) -> session_state().
flush_all_pending_presences(State) ->
PendingQ = ensure_queue(maps:get(pending_presences, State, [])),
PendingList = queue:to_list(PendingQ),
FlushedState = lists:foldl(
fun dispatch_presence_now/2, State, PendingList
),
FlushedState#{pending_presences => queue:new()}.
-spec maybe_sync_presence_targets(event(), [user_id()], session_state()) -> session_state().
maybe_sync_presence_targets(Event, FlushedIds, State) ->
case event_changes_presence_targets(Event) of
true -> sync_presence_targets(FlushedIds, State);
false -> State
end.
-spec sync_presence_targets(session_state()) -> session_state().
sync_presence_targets(State) ->
sync_presence_targets([], State).
-spec event_changes_presence_targets(event()) -> boolean().
event_changes_presence_targets(relationship_add) -> true;
event_changes_presence_targets(relationship_update) -> true;
event_changes_presence_targets(relationship_remove) -> true;
event_changes_presence_targets(channel_create) -> true;
event_changes_presence_targets(channel_update) -> true;
event_changes_presence_targets(channel_delete) -> true;
event_changes_presence_targets(channel_recipient_add) -> true;
event_changes_presence_targets(channel_recipient_remove) -> true;
event_changes_presence_targets(_) -> false.
-spec sync_presence_targets([user_id()], session_state()) -> session_state().
sync_presence_targets(FlushedIds, State) when is_map(State) ->
PresencePid = maps:get(presence_pid, State, undefined),
case PresencePid of
undefined ->
State;
Pid when is_pid(Pid) ->
FriendIds = presence_targets:friend_ids_from_state(State),
GroupDmRecipients = presence_targets:group_dm_recipients_from_state(State),
gen_server:cast(Pid, {sync_friends, FriendIds, FlushedIds}),
gen_server:cast(Pid, {sync_group_dm_recipients, GroupDmRecipients}),
State
end.
-spec presence_user_id(map()) -> user_id() | undefined.
presence_user_id(Data) ->
case maps:find(<<"user">>, Data) of
{ok, User} when is_map(User) ->
user_id(User);
_ ->
undefined
end.
-spec user_id(map()) -> user_id() | undefined.
user_id(User) ->
snowflake_id:parse_maybe(maps:get(<<"id">>, User, undefined)).
-spec relationship_target_id(map()) -> user_id() | undefined.
relationship_target_id(Data) when is_map(Data) ->
type_conv:extract_id(Data, <<"id">>).
-spec ensure_queue(queue:queue(T) | [T]) -> queue:queue(T).
ensure_queue(List) when is_list(List) -> queue:from_list(List);
ensure_queue(Q) -> Q.
-spec trim_queue_from_tail(queue:queue(T), non_neg_integer()) -> queue:queue(T).
trim_queue_from_tail(Queue, MaxLen) ->
case queue:len(Queue) > MaxLen of
true -> trim_queue_from_tail(queue:drop_r(Queue), MaxLen);
false -> Queue
end.
-spec send_to_socket(pid() | undefined, event(), map(), non_neg_integer()) -> ok.
send_to_socket(undefined, _Event, _Data, _Seq) ->
ok;
send_to_socket(Pid, Event, Data, Seq) when is_pid(Pid) ->
Pid ! {dispatch, Event, guild_data_wire:payload(Data), Seq},
ok.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
relationship_allows_presence_test() ->
?assertEqual(true, relationship_allows_presence(1, #{1 => 1})),
?assertEqual(false, relationship_allows_presence(1, #{1 => 3})),
?assertEqual(false, relationship_allows_presence(1, #{1 => 0})),
?assertEqual(false, relationship_allows_presence(1, #{1 => 2})),
?assertEqual(false, relationship_allows_presence(1, #{1 => 4})),
?assertEqual(false, relationship_allows_presence(1, #{})),
?assertEqual(false, relationship_allows_presence(0, #{})),
ok.
buffering_test_state() ->
#{
user_id => 1,
suppress_presence_updates => false,
relationships => #{},
channels => #{
100 => #{
<<"id">> => <<"100">>,
<<"type">> => 1,
<<"recipients">> => [#{<<"id">> => <<"2">>, <<"username">> => <<"dm-user">>}]
},
200 => #{
<<"id">> => <<"200">>,
<<"type">> => 3,
<<"recipients">> => [#{<<"id">> => <<"3">>, <<"username">> => <<"gdm-user">>}]
}
}
}.
presence_data(UserIdBin) ->
#{<<"user">> => #{<<"id">> => UserIdBin}, <<"status">> => <<"online">>}.
should_buffer_presence_buffers_one_to_one_dm_recipient_test() ->
State = buffering_test_state(),
?assertEqual(
true, should_buffer_presence(presence_update, presence_data(<<"2">>), State)
).
should_buffer_presence_passes_group_dm_recipient_test() ->
State = buffering_test_state(),
?assertEqual(
false, should_buffer_presence(presence_update, presence_data(<<"3">>), State)
).
should_buffer_presence_buffers_unrelated_user_test() ->
State = buffering_test_state(),
?assertEqual(
true, should_buffer_presence(presence_update, presence_data(<<"99">>), State)
).
should_buffer_presence_passes_friend_test() ->
State = (buffering_test_state())#{channels => #{}, relationships => #{4 => 1}},
?assertEqual(
false, should_buffer_presence(presence_update, presence_data(<<"4">>), State)
).
one_to_one_dm_presence_passes_attached_guild_scope_test() ->
State = (buffering_test_state())#{guilds => #{42 => undefined}},
Data = (presence_data(<<"2">>))#{<<"guild_id">> => <<"42">>},
?assertEqual(false, should_buffer_presence(presence_update, Data, State)).
one_to_one_dm_presence_buffers_unattached_guild_scope_test() ->
State = (buffering_test_state())#{guilds => #{42 => undefined}},
Data = (presence_data(<<"2">>))#{<<"guild_id">> => <<"99">>},
?assertEqual(true, should_buffer_presence(presence_update, Data, State)).
flush_test_state(PendingUserIds) ->
#{
user_id => 1,
seq => 0,
buffer => [],
socket_pid => undefined,
pending_presences => [
#{
event => presence_update,
data => presence_data(integer_to_binary(Uid)),
user_id => Uid
}
|| Uid <- PendingUserIds
]
}.
channel_create_does_not_flush_one_to_one_dm_recipient_presence_test() ->
State = flush_test_state([2, 99]),
ChannelData = #{
<<"id">> => <<"100">>,
<<"type">> => 1,
<<"recipients">> => [#{<<"id">> => <<"2">>, <<"username">> => <<"dm-user">>}]
},
{NewState, FlushedIds} = maybe_flush_pending_presences(
channel_create, ChannelData, State
),
?assertEqual([], FlushedIds),
?assertEqual(0, maps:get(seq, NewState)),
Remaining = queue:to_list(ensure_queue(maps:get(pending_presences, NewState))),
?assertEqual([2, 99], [maps:get(user_id, P) || P <- Remaining]).
channel_create_ignores_guild_channels_test() ->
State = flush_test_state([2]),
ChannelData = #{
<<"id">> => <<"100">>,
<<"type">> => 0,
<<"recipients">> => [#{<<"id">> => <<"2">>, <<"username">> => <<"dm-user">>}]
},
{NewState, FlushedIds} = maybe_flush_pending_presences(
channel_create, ChannelData, State
),
?assertEqual([], FlushedIds),
?assertEqual(0, maps:get(seq, NewState)).
channel_recipient_add_flushes_pending_presence_test() ->
State = flush_test_state([3, 99]),
Data = #{
<<"channel_id">> => <<"200">>,
<<"user">> => #{<<"id">> => <<"3">>, <<"username">> => <<"gdm-user">>}
},
{NewState, FlushedIds} = maybe_flush_pending_presences(
channel_recipient_add, Data, State
),
?assertEqual([3], FlushedIds),
Remaining = queue:to_list(ensure_queue(maps:get(pending_presences, NewState))),
?assertEqual([99], [maps:get(user_id, P) || P <- Remaining]).
relationship_update_top_level_id_flushes_pending_presence_test() ->
State = flush_test_state([2, 99]),
Data = #{<<"id">> => <<"2">>, <<"type">> => 1, <<"user">> => #{<<"id">> => <<"2">>}},
{NewState, FlushedIds} = maybe_flush_pending_presences(relationship_update, Data, State),
?assertEqual([2], FlushedIds),
Remaining = queue:to_list(ensure_queue(maps:get(pending_presences, NewState))),
?assertEqual([99], [maps:get(user_id, P) || P <- Remaining]).
presence_user_id_test() ->
?assertEqual(123, presence_user_id(#{<<"user">> => #{<<"id">> => <<"123">>}})),
?assertEqual(undefined, presence_user_id(#{<<"user">> => #{<<"id">> => <<"001">>}})),
?assertEqual(undefined, presence_user_id(#{<<"user">> => #{}})),
?assertEqual(undefined, presence_user_id(#{})),
?assertEqual(undefined, presence_user_id(#{<<"user">> => not_a_map})),
ok.
event_changes_presence_targets_test() ->
?assertEqual(true, event_changes_presence_targets(relationship_add)),
?assertEqual(true, event_changes_presence_targets(relationship_update)),
?assertEqual(true, event_changes_presence_targets(relationship_remove)),
?assertEqual(true, event_changes_presence_targets(channel_create)),
?assertEqual(true, event_changes_presence_targets(channel_update)),
?assertEqual(true, event_changes_presence_targets(channel_delete)),
?assertEqual(true, event_changes_presence_targets(channel_recipient_add)),
?assertEqual(true, event_changes_presence_targets(channel_recipient_remove)),
?assertEqual(false, event_changes_presence_targets(presence_update)),
?assertEqual(false, event_changes_presence_targets(message_create)),
?assertEqual(false, event_changes_presence_targets(guild_member_update)),
?assertEqual(false, event_changes_presence_targets(typing_start)),
ok.
flushed_id_list_test() ->
?assertEqual([], flushed_id_list(undefined)),
?assertEqual([42], flushed_id_list(42)),
ok.
-endif.