mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
441 lines
17 KiB
Erlang
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.
|