fix(presence): enforce visibility and relationship invariants (#1507)

This commit is contained in:
Hampus
2026-07-30 21:39:05 +02:00
committed by GitHub
parent 21a898e602
commit 9d33993413
13 changed files with 256 additions and 124 deletions
@@ -313,9 +313,9 @@ export class GroupDmOperationsService {
private async syncGroupDmRecipientsForUser(userId: UserID): Promise<void> {
const channels = await this.userRepository.listPrivateChannels(userId);
const dmChannels = channels.filter((ch) => ch.type === ChannelTypes.DM || ch.type === ChannelTypes.GROUP_DM);
const groupDmChannels = channels.filter((ch) => ch.type === ChannelTypes.GROUP_DM);
const recipientsByChannel: Record<string, Array<string>> = {};
for (const channel of dmChannels) {
for (const channel of groupDmChannels) {
const otherRecipients = Array.from(channel.recipientIds)
.filter((recId) => recId !== userId)
.map((recId) => recId.toString());
@@ -109,10 +109,10 @@ handle_sync_dm_recipients(#{
<<"recipients_by_channel">> := RecipientsByChannel
}) ->
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
NormalizedRecipients = normalize_recipients(RecipientsByChannel),
_ = normalize_recipients(RecipientsByChannel),
case lookup_owner_presence(UserId) of
{ok, Pid} ->
gen_server:cast(Pid, {sync_group_dm_recipients, NormalizedRecipients}),
gen_server:cast(Pid, presence_rejoin),
true;
{error, not_found} ->
true;
+27
View File
@@ -115,6 +115,11 @@ handle_cast({dispatch, Event, Data}, State) when is_atom(Event), is_map(Data) ->
handle_dispatch_cast(Event, Data, State);
handle_cast(presence_rejoin, State) ->
handle_presence_rejoin(State);
handle_cast(reconcile_flattened_presence, State) ->
{noreply,
presence_broadcast:publish_global_presence(
maps:get(sessions, State), State
)};
handle_cast({presence_update, Request}, State) when is_map(Request) ->
handle_presence_update_cast(Request, State);
handle_cast({terminate_session, SessionIdHashes}, State) when is_list(SessionIdHashes) ->
@@ -291,6 +296,28 @@ presence_rejoin_with_no_sessions_is_noop_test() ->
State = test_state(#{}),
?assertEqual({noreply, State}, handle_cast(presence_rejoin, State)).
reconcile_flattened_presence_uses_current_session_state_test() ->
State = test_state(#{}),
PublishedState = State#{last_published_presence => #{status => <<"offline">>}},
meck:new(presence_broadcast, [passthrough]),
meck:expect(
presence_broadcast,
publish_global_presence,
fun(Sessions, ReceivedState) ->
?assertEqual(#{}, Sessions),
?assertEqual(State, ReceivedState),
PublishedState
end
),
try
?assertEqual(
{noreply, PublishedState},
handle_cast(reconcile_flattened_presence, State)
)
after
meck:unload(presence_broadcast)
end.
-spec test_state(sessions()) -> state().
test_state(Sessions) ->
#{
@@ -319,7 +319,7 @@ handle_session_connect_existing_session_refreshes_status_test() ->
?assertEqual(Ref, maps:get(mref, UpdatedSession)),
[AllSession | _] = SessionsData,
?assertEqual(<<"dnd">>, maps:get(<<"status">>, AllSession)),
?assertEqual(true, maps:get(<<"mobile">>, AllSession)).
?assertEqual(false, maps:get(<<"mobile">>, AllSession)).
flush_test_messages() ->
receive
+57 -15
View File
@@ -20,16 +20,11 @@
-spec get_current_status(sessions()) -> status().
get_current_status(Sessions) ->
AllStatuses = [maps:get(status, S) || S <- maps:values(Sessions)],
case lists:member(invisible, AllStatuses) of
true ->
invisible;
false ->
resolve_status_precedence(AllStatuses)
end.
resolve_status_precedence(AllStatuses).
-spec resolve_status_precedence([status()]) -> status().
resolve_status_precedence(AllStatuses) ->
StatusPrecedence = [dnd, online, idle],
StatusPrecedence = [dnd, online, idle, invisible],
lists:foldl(
fun(Status, Acc) ->
promote_status(Status, Acc, AllStatuses)
@@ -49,12 +44,13 @@ promote_status(_Status, Acc, _AllStatuses) ->
-spec get_flattened_mobile(sessions()) -> boolean().
get_flattened_mobile(Sessions) ->
lists:any(
fun(Session) ->
maps:get(mobile, Session, false)
end,
maps:values(Sessions)
).
get_current_status(Sessions) =:= online andalso
lists:any(fun is_online_mobile_session/1, maps:values(Sessions)).
-spec is_online_mobile_session(map()) -> boolean().
is_online_mobile_session(Session) ->
maps:get(status, Session, offline) =:= online andalso
maps:get(mobile, Session, false).
-spec get_flattened_afk(sessions()) -> boolean().
get_flattened_afk(Sessions) ->
@@ -150,11 +146,18 @@ get_current_status_dnd_over_idle_test() ->
},
?assertEqual(dnd, get_current_status(Sessions)).
get_current_status_invisible_test() ->
get_current_status_visible_over_invisible_test() ->
Sessions = #{
<<"s1">> => #{status => invisible, afk => false, mobile => false},
<<"s2">> => #{status => online, afk => false, mobile => false}
},
?assertEqual(online, get_current_status(Sessions)).
get_current_status_invisible_without_visible_session_test() ->
Sessions = #{
<<"s1">> => #{status => invisible, afk => false, mobile => false},
<<"s2">> => #{status => offline, afk => false, mobile => false}
},
?assertEqual(invisible, get_current_status(Sessions)).
get_flattened_mobile_true_test() ->
@@ -168,7 +171,46 @@ get_flattened_mobile_false_test() ->
Sessions = #{
<<"s1">> => #{status => online, afk => false, mobile => false}
},
?assertEqual(false, get_flattened_mobile(Sessions)).
?assertEqual(false, get_flattened_mobile(Sessions)),
?assertEqual(
false,
get_flattened_mobile(#{
<<"s1">> => #{status => idle, afk => false, mobile => true}
})
),
?assertEqual(
false,
get_flattened_mobile(#{
<<"s1">> => #{status => dnd, afk => false, mobile => true}
})
),
?assertEqual(
false,
get_flattened_mobile(#{
<<"s1">> => #{status => invisible, afk => false, mobile => true}
})
),
?assertEqual(
false,
get_flattened_mobile(#{
<<"s1">> => #{status => dnd, afk => false, mobile => false},
<<"s2">> => #{status => online, afk => false, mobile => true}
})
),
?assertEqual(
false,
get_flattened_mobile(#{
<<"s1">> => #{status => online, afk => false, mobile => false},
<<"s2">> => #{status => idle, afk => false, mobile => true}
})
),
?assertEqual(
true,
get_flattened_mobile(#{
<<"s1">> => #{status => online, afk => false, mobile => true},
<<"s2">> => #{status => invisible, afk => false, mobile => false}
})
).
get_flattened_mobile_empty_test() ->
?assertEqual(false, get_flattened_mobile(#{})).
@@ -5,6 +5,8 @@
-export([
friend_ids_from_state/1,
group_dm_recipients_from_state/1,
group_dm_channel_recipient_ids/2,
dm_recipients_from_state/1,
dm_channel_recipient_ids/2,
map_from_ids/1
@@ -24,13 +26,11 @@ friend_ids_from_state(State) ->
-spec accumulate_friend_id(term(), term(), [user_id()]) -> [user_id()].
accumulate_friend_id(UserId, 1, Acc) when is_integer(UserId) ->
[UserId | Acc];
accumulate_friend_id(UserId, 3, Acc) when is_integer(UserId) ->
[UserId | Acc];
accumulate_friend_id(_UserId, _Type, Acc) ->
Acc.
-spec dm_recipients_from_state(state()) -> #{channel_id() => #{user_id() => true}}.
dm_recipients_from_state(State) ->
-spec group_dm_recipients_from_state(state()) -> #{channel_id() => #{user_id() => true}}.
group_dm_recipients_from_state(State) ->
UserId = maps:get(user_id, State, undefined),
Channels = maps:get(channels, State, #{}),
maps:fold(
@@ -41,11 +41,15 @@ dm_recipients_from_state(State) ->
Channels
).
-spec dm_recipients_from_state(state()) -> #{channel_id() => #{user_id() => true}}.
dm_recipients_from_state(State) ->
group_dm_recipients_from_state(State).
-spec accumulate_dm_channel(term(), term(), user_id() | undefined, map()) -> map().
accumulate_dm_channel(ChannelId, Channel, UserId, Acc) when
is_integer(ChannelId), is_map(Channel)
->
case is_dm_channel_type(maps:get(<<"type">>, Channel, 0)) of
case is_group_dm_channel_type(maps:get(<<"type">>, Channel, 0)) of
true ->
RecipientIds = extract_recipient_ids(Channel),
Acc#{ChannelId => map_from_ids([Rid || Rid <- RecipientIds, Rid =/= UserId])};
@@ -55,22 +59,25 @@ accumulate_dm_channel(ChannelId, Channel, UserId, Acc) when
accumulate_dm_channel(_ChannelId, _Channel, _UserId, Acc) ->
Acc.
-spec is_dm_channel_type(term()) -> boolean().
is_dm_channel_type(1) -> true;
is_dm_channel_type(3) -> true;
is_dm_channel_type(_) -> false.
-spec is_group_dm_channel_type(term()) -> boolean().
is_group_dm_channel_type(3) -> true;
is_group_dm_channel_type(_) -> false.
-spec dm_channel_recipient_ids(term(), user_id() | undefined) -> [user_id()].
dm_channel_recipient_ids(Channel, SelfUserId) when is_map(Channel) ->
case is_dm_channel_type(maps:get(<<"type">>, Channel, 0)) of
-spec group_dm_channel_recipient_ids(term(), user_id() | undefined) -> [user_id()].
group_dm_channel_recipient_ids(Channel, SelfUserId) when is_map(Channel) ->
case is_group_dm_channel_type(maps:get(<<"type">>, Channel, 0)) of
true ->
[Rid || Rid <- extract_recipient_ids(Channel), Rid =/= SelfUserId];
false ->
[]
end;
dm_channel_recipient_ids(_Channel, _SelfUserId) ->
group_dm_channel_recipient_ids(_Channel, _SelfUserId) ->
[].
-spec dm_channel_recipient_ids(term(), user_id() | undefined) -> [user_id()].
dm_channel_recipient_ids(Channel, SelfUserId) ->
group_dm_channel_recipient_ids(Channel, SelfUserId).
-spec extract_recipient_ids(map()) -> [user_id()].
extract_recipient_ids(Channel) ->
Recipients = maps:get(
@@ -130,7 +137,7 @@ friend_ids_from_state_filters_relationship_types_test() ->
}
},
Ids = lists:sort(friend_ids_from_state(State)),
?assertEqual([10, 11], Ids).
?assertEqual([10], Ids).
friend_ids_from_state_empty_test() ->
State = #{relationships => #{}},
@@ -140,7 +147,7 @@ friend_ids_from_state_missing_key_test() ->
State = #{},
?assertEqual([], friend_ids_from_state(State)).
dm_recipients_from_state_test() ->
group_dm_recipients_from_state_test() ->
State = #{
user_id => 1,
channels => #{
@@ -148,10 +155,10 @@ dm_recipients_from_state_test() ->
200 => api_channel(<<"200">>, 0, [<<"4">>])
}
},
Result = dm_recipients_from_state(State),
Result = group_dm_recipients_from_state(State),
?assertEqual(#{100 => #{2 => true, 3 => true}}, Result).
dm_recipients_from_state_includes_one_to_one_dms_test() ->
group_dm_recipients_from_state_excludes_one_to_one_dms_test() ->
State = #{
user_id => 1,
channels => #{
@@ -160,16 +167,10 @@ dm_recipients_from_state_includes_one_to_one_dms_test() ->
300 => api_channel(<<"300">>, 0, [<<"5">>])
}
},
Result = dm_recipients_from_state(State),
?assertEqual(
#{
100 => #{2 => true},
200 => #{3 => true, 4 => true}
},
Result
).
Result = group_dm_recipients_from_state(State),
?assertEqual(#{200 => #{3 => true, 4 => true}}, Result).
dm_recipients_excludes_self_test() ->
group_dm_recipients_excludes_self_test() ->
State = #{
user_id => 2,
channels => #{
@@ -177,17 +178,17 @@ dm_recipients_excludes_self_test() ->
200 => api_channel(<<"200">>, 1, [<<"2">>, <<"7">>])
}
},
Result = dm_recipients_from_state(State),
?assertEqual(#{100 => #{3 => true}, 200 => #{7 => true}}, Result).
Result = group_dm_recipients_from_state(State),
?assertEqual(#{100 => #{3 => true}}, Result).
dm_recipients_supports_recipient_ids_field_test() ->
group_dm_recipients_supports_recipient_ids_field_test() ->
State = #{
user_id => 1,
channels => #{
100 => #{<<"type">> => 1, <<"recipient_ids">> => [<<"2">>]}
100 => #{<<"type">> => 3, <<"recipient_ids">> => [<<"2">>]}
}
},
Result = dm_recipients_from_state(State),
Result = group_dm_recipients_from_state(State),
?assertEqual(#{100 => #{2 => true}}, Result).
api_channel(IdBin, Type, RecipientIdBins) ->
+2 -1
View File
@@ -309,7 +309,8 @@ do_handle_info_misc(_Info, State) ->
-spec handle_presence_rejoin_check(session_state()) -> {noreply, session_state()}.
handle_presence_rejoin_check(State) ->
{noreply, session_connection:repair_presence_connection(State)}.
RepairedState = session_connection:repair_presence_connection(State),
{noreply, session_dispatch_presence:sync_presence_targets(RepairedState)}.
-spec terminate(term(), session_state()) -> ok.
terminate(Reason, State) ->
@@ -50,14 +50,14 @@ safe_presence_lookup(Request) ->
-spec build_presence_request(session_state()) -> map().
build_presence_request(State) when is_map(State) ->
FriendIds = presence_targets:friend_ids_from_state(State),
DmRecipients = presence_targets:dm_recipients_from_state(State),
GroupDmRecipients = presence_targets:group_dm_recipients_from_state(State),
#{
user_id => maps:get(user_id, State),
user_data => maps:get(user_data, State),
guild_ids => maps:keys(maps:get(guilds, State)),
status => maps:get(status, State),
friend_ids => FriendIds,
group_dm_recipients => DmRecipients,
group_dm_recipients => GroupDmRecipients,
custom_status => maps:get(custom_status, State, null)
}.
@@ -70,7 +70,7 @@ do_session_connect(Pid, Attempt, State) when is_map(State) ->
Mobile = maps:get(mobile, State),
SocketPid = maps:get(socket_pid, State, undefined),
FriendIds = presence_targets:friend_ids_from_state(State),
DmRecipients = presence_targets:dm_recipients_from_state(State),
GroupDmRecipients = presence_targets:group_dm_recipients_from_state(State),
try_session_connect(
Pid,
SessionId,
@@ -79,7 +79,7 @@ do_session_connect(Pid, Attempt, State) when is_map(State) ->
Mobile,
SocketPid,
FriendIds,
DmRecipients,
GroupDmRecipients,
Attempt,
State
).
@@ -10,6 +10,7 @@
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
@@ -32,21 +33,28 @@ should_buffer_presence(presence_update, Data, State) ->
true ->
true;
false ->
check_non_guild_presence(Data, State)
check_presence_scope(Data, State)
end;
should_buffer_presence(_, _, _) ->
false.
-spec check_non_guild_presence(map(), session_state()) -> boolean().
check_non_guild_presence(Data, State) ->
HasGuildId =
is_map(Data) andalso (maps:get(<<"guild_id">>, Data, undefined) =/= undefined),
case HasGuildId of
true ->
false;
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)
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().
@@ -55,8 +63,8 @@ check_user_presence_buffering(undefined, _State) ->
check_user_presence_buffering(UserId, State) ->
Relationships = maps:get(relationships, State, #{}),
IsRelationship = relationship_allows_presence(UserId, Relationships),
IsDmRecipient = is_dm_recipient(UserId, State),
not (IsRelationship orelse IsDmRecipient).
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
@@ -64,31 +72,30 @@ relationship_allows_presence(UserId, Relationships) when
->
case maps:get(UserId, Relationships, undefined) of
1 -> true;
3 -> true;
_ -> false
end;
relationship_allows_presence(_, _) ->
false.
-spec is_dm_recipient(user_id(), session_state()) -> boolean().
is_dm_recipient(UserId, State) when is_map(State) ->
DmRecipients = presence_targets:dm_recipients_from_state(State),
-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,
DmRecipients
GroupDmRecipients
).
-spec buffer_presence(event(), map(), session_state()) -> session_state().
buffer_presence(Event, Data, State) ->
Pending = maps:get(pending_presences, State, []),
Pending = ensure_queue(maps:get(pending_presences, State, [])),
UserId = presence_user_id(Data),
Entry = #{event => Event, data => Data, user_id => UserId},
Trimmed = trim_rev_list(Pending, ?MAX_PENDING_PRESENCE_BUFFER_SIZE - 1),
NewPending = [Entry | Trimmed],
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()) ->
@@ -110,7 +117,7 @@ maybe_flush_pending_presences(_, _, State) ->
{session_state(), [user_id()]}.
flush_dm_channel_pending_presences(Data, State) ->
SelfUserId = maps:get(user_id, State, undefined),
RecipientIds = presence_targets:dm_channel_recipient_ids(Data, SelfUserId),
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()) ->
@@ -139,9 +146,6 @@ maybe_flush_relationship_pending_presences(Data, State) ->
1 ->
TargetId = relationship_target_id(Data),
{flush_pending_presences(TargetId, State), flushed_id_list(TargetId)};
3 ->
TargetId = relationship_target_id(Data),
{flush_pending_presences(TargetId, State), flushed_id_list(TargetId)};
_ ->
{State, []}
end.
@@ -207,6 +211,10 @@ maybe_sync_presence_targets(Event, 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;
@@ -226,9 +234,9 @@ sync_presence_targets(FlushedIds, State) when is_map(State) ->
State;
Pid when is_pid(Pid) ->
FriendIds = presence_targets:friend_ids_from_state(State),
DmRecipients = presence_targets:dm_recipients_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, DmRecipients}),
gen_server:cast(Pid, {sync_group_dm_recipients, GroupDmRecipients}),
State
end.
@@ -253,9 +261,12 @@ relationship_target_id(Data) when is_map(Data) ->
ensure_queue(List) when is_list(List) -> queue:from_list(List);
ensure_queue(Q) -> Q.
-spec trim_rev_list([T], non_neg_integer()) -> [T].
trim_rev_list(List, MaxLen) when length(List) =< MaxLen -> List;
trim_rev_list(List, MaxLen) -> lists:sublist(List, MaxLen).
-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) ->
@@ -269,7 +280,7 @@ send_to_socket(Pid, Event, Data, Seq) when is_pid(Pid) ->
relationship_allows_presence_test() ->
?assertEqual(true, relationship_allows_presence(1, #{1 => 1})),
?assertEqual(true, relationship_allows_presence(1, #{1 => 3})),
?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})),
@@ -299,10 +310,10 @@ buffering_test_state() ->
presence_data(UserIdBin) ->
#{<<"user">> => #{<<"id">> => UserIdBin}, <<"status">> => <<"online">>}.
should_buffer_presence_passes_one_to_one_dm_recipient_test() ->
should_buffer_presence_buffers_one_to_one_dm_recipient_test() ->
State = buffering_test_state(),
?assertEqual(
false, should_buffer_presence(presence_update, presence_data(<<"2">>), State)
true, should_buffer_presence(presence_update, presence_data(<<"2">>), State)
).
should_buffer_presence_passes_group_dm_recipient_test() ->
@@ -323,11 +334,16 @@ should_buffer_presence_passes_friend_test() ->
false, should_buffer_presence(presence_update, presence_data(<<"4">>), State)
).
should_buffer_presence_passes_guild_presence_test() ->
State = buffering_test_state(),
Data = (presence_data(<<"99">>))#{<<"guild_id">> => <<"42">>},
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,
@@ -344,7 +360,7 @@ flush_test_state(PendingUserIds) ->
]
}.
channel_create_flushes_pending_dm_recipient_presence_test() ->
channel_create_does_not_flush_one_to_one_dm_recipient_presence_test() ->
State = flush_test_state([2, 99]),
ChannelData = #{
<<"id">> => <<"100">>,
@@ -354,10 +370,10 @@ channel_create_flushes_pending_dm_recipient_presence_test() ->
{NewState, FlushedIds} = maybe_flush_pending_presences(
channel_create, ChannelData, State
),
?assertEqual([2], FlushedIds),
?assertEqual(1, maps:get(seq, NewState)),
?assertEqual([], FlushedIds),
?assertEqual(0, maps:get(seq, NewState)),
Remaining = queue:to_list(ensure_queue(maps:get(pending_presences, NewState))),
?assertEqual([99], [maps:get(user_id, P) || P <- Remaining]).
?assertEqual([2, 99], [maps:get(user_id, P) || P <- Remaining]).
channel_create_ignores_guild_channels_test() ->
State = flush_test_state([2]),
@@ -52,14 +52,14 @@ collect_ready_presences(State, _CollectedGuilds) ->
-spec collect_presence_targets(session_state(), user_id()) -> [user_id()].
collect_presence_targets(State, CurrentUserId) when is_map(State) ->
FIds = presence_targets:friend_ids_from_state(State),
DmMap = presence_targets:dm_recipients_from_state(State),
GroupDmMap = presence_targets:group_dm_recipients_from_state(State),
TargetMap0 = add_presence_target_ids(FIds, CurrentUserId, #{}),
TargetMap = maps:fold(
fun(_Cid, Recipients, Acc) ->
add_presence_target_map(Recipients, CurrentUserId, Acc)
end,
TargetMap0,
DmMap
GroupDmMap
),
maps:keys(TargetMap).
@@ -350,7 +350,7 @@ collect_presence_targets_deduplicates_before_fetch_test() ->
}
}
},
?assertEqual([2, 3, 5], lists:sort(collect_presence_targets(State, 1))).
?assertEqual([2, 5], lists:sort(collect_presence_targets(State, 1))).
collect_ready_users_collects_directly_into_dedup_map_test() ->
UserA = #{<<"id">> => <<"10">>, <<"username">> => <<"a">>},
@@ -26,7 +26,7 @@ gdm_subscription_add_remove_test() ->
State2 = presence_broadcast_subscriptions:sync_group_dm_subscriptions(#{}, State1),
?assertEqual(false, maps:is_key(10, maps:get(subscriptions, State2, #{}))).
one_to_one_dm_recipients_get_subscriptions_test() ->
only_group_dm_recipients_get_global_subscriptions_test() ->
maybe_start_presence_bus(),
maybe_start_presence_cache(),
SessionState = #{
@@ -47,7 +47,7 @@ one_to_one_dm_recipients_get_subscriptions_test() ->
}
}
},
DmRecipients = presence_targets:dm_recipients_from_state(SessionState),
GroupDmRecipients = presence_targets:group_dm_recipients_from_state(SessionState),
BaseState = #{
user_id => 1,
is_bot => false,
@@ -58,20 +58,21 @@ one_to_one_dm_recipients_get_subscriptions_test() ->
group_dm_recipients => #{}
},
State1 = presence_broadcast_subscriptions:sync_group_dm_subscriptions(
DmRecipients, BaseState
GroupDmRecipients, BaseState
),
Subscriptions = maps:get(subscriptions, State1),
?assertEqual(true, maps:get(100, maps:get(gdm_channels, maps:get(2, Subscriptions)))),
?assertEqual(false, maps:is_key(2, Subscriptions)),
?assertEqual(true, maps:get(200, maps:get(gdm_channels, maps:get(3, Subscriptions)))),
?assertEqual(true, maps:get(200, maps:get(gdm_channels, maps:get(4, Subscriptions)))),
StateAfterDmClose = presence_broadcast_subscriptions:sync_group_dm_subscriptions(
maps:remove(100, DmRecipients), State1
StateAfterGroupDmClose = presence_broadcast_subscriptions:sync_group_dm_subscriptions(
maps:remove(200, GroupDmRecipients), State1
),
SubsAfterClose = maps:get(subscriptions, StateAfterDmClose),
SubsAfterClose = maps:get(subscriptions, StateAfterGroupDmClose),
?assertEqual(false, maps:is_key(2, SubsAfterClose)),
?assertEqual(true, maps:is_key(3, SubsAfterClose)).
?assertEqual(false, maps:is_key(3, SubsAfterClose)),
?assertEqual(false, maps:is_key(4, SubsAfterClose)).
initial_subscriptions_include_one_to_one_dm_recipients_test() ->
one_to_one_dm_does_not_create_global_subscription_test() ->
maybe_start_presence_bus(),
SessionState = #{
user_id => 1,
@@ -90,11 +91,11 @@ initial_subscriptions_include_one_to_one_dm_recipients_test() ->
user_data => #{},
subscriptions => #{},
friends => #{},
group_dm_recipients => presence_targets:dm_recipients_from_state(SessionState)
group_dm_recipients => presence_targets:group_dm_recipients_from_state(SessionState)
},
State1 = presence_broadcast_subscriptions:ensure_initial_global_subscriptions(State),
Subscriptions = maps:get(subscriptions, State1),
?assertEqual(true, maps:get(100, maps:get(gdm_channels, maps:get(2, Subscriptions)))).
?assertEqual(#{}, Subscriptions).
map_from_ids_test() ->
?assertEqual(#{}, presence_broadcast_subscriptions:map_from_ids([])),
@@ -27,8 +27,17 @@ base_state(Opts) ->
Opts
).
presence_update_with_guild_id_not_buffered_test() ->
State0 = base_state(#{}),
one_to_one_dm_presence_with_attached_guild_id_not_buffered_test() ->
State0 = base_state(#{
guilds => #{123 => connected},
channels => #{
100 => #{
<<"id">> => <<"100">>,
<<"type">> => 1,
<<"recipients">> => [#{<<"id">> => <<"2">>}]
}
}
}),
Presence = #{
<<"guild_id">> => <<"123">>,
<<"user">> => #{<<"id">> => <<"2">>},
@@ -38,11 +47,31 @@ presence_update_with_guild_id_not_buffered_test() ->
?assertEqual([], maps:get(pending_presences, State1, [])),
?assertEqual(1, limited_deque:size(maps:get(buffer, State1))).
one_to_one_dm_presence_with_unattached_guild_id_buffered_test() ->
State0 = base_state(#{
guilds => #{456 => connected},
channels => #{
100 => #{
<<"id">> => <<"100">>,
<<"type">> => 1,
<<"recipients">> => [#{<<"id">> => <<"2">>}]
}
}
}),
Presence = #{
<<"guild_id">> => <<"123">>,
<<"user">> => #{<<"id">> => <<"2">>},
<<"status">> => <<"idle">>
},
{noreply, State1} = session_dispatch:handle_dispatch(presence_update, Presence, State0),
?assertEqual(1, pending_presence_count(State1)),
?assertEqual(0, limited_deque:size(maps:get(buffer, State1))).
presence_update_without_guild_id_buffered_for_non_relationship_test() ->
State0 = base_state(#{}),
Presence = #{<<"user">> => #{<<"id">> => <<"2">>}, <<"status">> => <<"online">>},
{noreply, State1} = session_dispatch:handle_dispatch(presence_update, Presence, State0),
?assertEqual(1, length(maps:get(pending_presences, State1, []))),
?assertEqual(1, pending_presence_count(State1)),
?assertEqual(0, limited_deque:size(maps:get(buffer, State1))).
presence_update_without_guild_id_not_buffered_for_relationship_test() ->
@@ -56,28 +85,43 @@ presence_update_buffered_for_outgoing_request_relationship_test() ->
State0 = base_state(#{relationships => #{2 => 4}}),
Presence = #{<<"user">> => #{<<"id">> => <<"2">>}, <<"status">> => <<"online">>},
{noreply, State1} = session_dispatch:handle_dispatch(presence_update, Presence, State0),
?assertEqual(1, length(maps:get(pending_presences, State1, []))),
?assertEqual(1, pending_presence_count(State1)),
?assertEqual(0, limited_deque:size(maps:get(buffer, State1))).
presence_update_not_buffered_for_incoming_request_test() ->
presence_update_buffered_for_incoming_request_test() ->
State0 = base_state(#{relationships => #{2 => 3}}),
Presence = #{<<"user">> => #{<<"id">> => <<"2">>}, <<"status">> => <<"online">>},
{noreply, State1} = session_dispatch:handle_dispatch(presence_update, Presence, State0),
?assertEqual([], maps:get(pending_presences, State1, [])),
?assertEqual(1, limited_deque:size(maps:get(buffer, State1))).
?assertEqual(1, pending_presence_count(State1)),
?assertEqual(0, limited_deque:size(maps:get(buffer, State1))).
presence_update_not_buffered_after_relationship_add_test() ->
relationship_mutations_keep_presence_buffer_valid_test() ->
State0 = base_state(#{}),
Relationship = #{
Friend = #{
<<"id">> => <<"2">>, <<"type">> => 1, <<"user">> => #{<<"id">> => <<"2">>}
},
{noreply, State1} = session_dispatch:handle_dispatch(
relationship_add, Relationship, State0
relationship_add, Friend, State0
),
Presence = #{<<"user">> => #{<<"id">> => <<"2">>}, <<"status">> => <<"online">>},
{noreply, State2} = session_dispatch:handle_dispatch(presence_update, Presence, State1),
Online = #{<<"user">> => #{<<"id">> => <<"2">>}, <<"status">> => <<"online">>},
{noreply, State2} = session_dispatch:handle_dispatch(presence_update, Online, State1),
?assertEqual(0, pending_presence_count(State2)),
?assertEqual(2, limited_deque:size(maps:get(buffer, State2))).
Blocked = Friend#{<<"type">> => 2},
{noreply, State3} = session_dispatch:handle_dispatch(
relationship_update, Blocked, State2
),
Offline = Online#{<<"status">> => <<"offline">>},
{noreply, State4} = session_dispatch:handle_dispatch(presence_update, Offline, State3),
?assertEqual(1, pending_presence_count(State4)),
{noreply, State5} = session_dispatch:handle_dispatch(
relationship_add, Friend, State4
),
?assertEqual(0, pending_presence_count(State5)),
{noreply, State6} = session_dispatch:handle_dispatch(
relationship_remove, #{<<"id">> => <<"2">>}, State5
),
{noreply, State7} = session_dispatch:handle_dispatch(presence_update, Offline, State6),
?assertEqual(1, pending_presence_count(State7)).
presence_update_skips_sync_presence_targets_test() ->
State0 = base_state(#{relationships => #{2 => 1}}),
+2 -2
View File
@@ -367,7 +367,7 @@ base_state_for_guild_dispatch_test() ->
collected_guild_states => []
}.
collect_ready_presences_includes_one_to_one_dm_recipients_test() ->
collect_ready_presences_does_not_add_one_to_one_dm_as_global_target_test() ->
{ok, CachePid} = maybe_start_presence_cache(),
OnlineDmUser = #{
<<"status">> => <<"online">>,
@@ -398,7 +398,7 @@ collect_ready_presences_includes_one_to_one_dm_recipients_test() ->
maps:get(<<"id">>, maps:get(<<"user">>, P, #{}), undefined)
|| P <- Presences
],
?assertEqual([<<"2">>], PresenceIds),
?assertEqual([], PresenceIds),
?assertEqual(ok, gen_server:stop(CachePid)).
collect_ready_presences_includes_friend_without_dm_recipients_test() ->