mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(gateway): track dm partner presence in mutual guilds (#2658)
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(guild_dm_partners_tests).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(GUILD_ID, 42).
|
||||
-define(VIEWER_ROLE, 1000).
|
||||
-define(OTHER_ROLE, 2000).
|
||||
-define(SESSION, <<"session-10">>).
|
||||
-define(FLAG, dm_presence_mutual_context).
|
||||
|
||||
registration_notifies_mutual_partners_test() ->
|
||||
with_flag(true, fun() ->
|
||||
State = register_partners(state(), [20, 30, 777]),
|
||||
?assertEqual({dm_partner_mutual, ?GUILD_ID, [20]}, receive_mutual()),
|
||||
?assert(maps:is_key(?SESSION, maps:get(dm_partners, State)))
|
||||
end).
|
||||
|
||||
unknown_session_is_not_registered_test() ->
|
||||
with_flag(true, fun() ->
|
||||
{noreply, State} = guild_dm_partners:handle_cast(
|
||||
{update_dm_partners, <<"missing">>, [20]}, state()
|
||||
),
|
||||
?assertEqual(#{}, maps:get(dm_partners, State, #{})),
|
||||
?assertEqual(none, receive_mutual())
|
||||
end).
|
||||
|
||||
disabled_flag_does_not_register_test() ->
|
||||
with_flag(false, fun() ->
|
||||
State = register_partners(state(), [20]),
|
||||
?assertEqual(#{}, maps:get(dm_partners, State, #{})),
|
||||
?assertEqual(none, receive_mutual())
|
||||
end).
|
||||
|
||||
unchanged_eligibility_is_not_sent_again_test() ->
|
||||
with_flag(true, fun() ->
|
||||
State = register_partners(state(), [20]),
|
||||
{dm_partner_mutual, _, [20]} = receive_mutual(),
|
||||
_ = register_partners(State, [20, 777]),
|
||||
?assertEqual(none, receive_mutual())
|
||||
end).
|
||||
|
||||
partner_leaving_the_guild_withdraws_the_partner_test() ->
|
||||
with_flag(true, fun() ->
|
||||
Before = register_partners(state(), [20]),
|
||||
{dm_partner_mutual, _, [20]} = receive_mutual(),
|
||||
After = without_member(20, Before),
|
||||
_ = guild_dm_partners:maybe_reevaluate(
|
||||
guild_member_remove, #{<<"user">> => #{<<"id">> => <<"20">>}}, Before, After
|
||||
),
|
||||
?assertEqual({dm_partner_mutual, ?GUILD_ID, []}, receive_mutual())
|
||||
end).
|
||||
|
||||
guild_wide_event_without_visibility_change_does_nothing_test() ->
|
||||
with_flag(true, fun() ->
|
||||
Before = register_partners(state(), [20]),
|
||||
{dm_partner_mutual, _, [20]} = receive_mutual(),
|
||||
Renamed = with_channels(
|
||||
[
|
||||
(channel(500, viewer_overwrites()))#{<<"name">> => <<"renamed">>},
|
||||
channel(600, other_overwrites())
|
||||
],
|
||||
Before
|
||||
),
|
||||
_ = guild_dm_partners:maybe_reevaluate(channel_update, #{}, Before, Renamed),
|
||||
?assertEqual(none, receive_mutual())
|
||||
end).
|
||||
|
||||
guild_wide_visibility_change_reevaluates_test() ->
|
||||
with_flag(true, fun() ->
|
||||
Before = register_partners(state(), [30]),
|
||||
?assertEqual(none, receive_mutual()),
|
||||
Opened = with_channels(
|
||||
[
|
||||
channel(500, viewer_overwrites() ++ [overwrite(?OTHER_ROLE, 0)]),
|
||||
channel(600, other_overwrites())
|
||||
],
|
||||
Before
|
||||
),
|
||||
_ = guild_dm_partners:maybe_reevaluate(channel_update, #{}, Before, Opened),
|
||||
?assertEqual({dm_partner_mutual, ?GUILD_ID, [30]}, receive_mutual())
|
||||
end).
|
||||
|
||||
disconnected_sessions_are_dropped_on_reevaluation_test() ->
|
||||
with_flag(true, fun() ->
|
||||
Before = register_partners(state(), [20]),
|
||||
{dm_partner_mutual, _, [20]} = receive_mutual(),
|
||||
Gone = Before#{sessions => #{}},
|
||||
After = guild_dm_partners:maybe_reevaluate(guild_role_delete, #{}, Before, Gone#{
|
||||
virtual_channel_access => #{1 => sets:new()}
|
||||
}),
|
||||
?assertEqual(#{}, maps:get(dm_partners, After))
|
||||
end).
|
||||
|
||||
register_partners(State, PartnerIds) ->
|
||||
{noreply, NewState} = guild_dm_partners:handle_cast(
|
||||
{update_dm_partners, ?SESSION, PartnerIds}, State
|
||||
),
|
||||
NewState.
|
||||
|
||||
state() ->
|
||||
Base = #{
|
||||
id => ?GUILD_ID,
|
||||
virtual_channel_access => #{},
|
||||
data => data(
|
||||
[channel(500, viewer_overwrites()), channel(600, other_overwrites())], members()
|
||||
)
|
||||
},
|
||||
Viewable = guild_sessions:build_viewable_channel_map(
|
||||
guild_visibility:get_user_viewable_channels(10, Base)
|
||||
),
|
||||
Base#{
|
||||
sessions => #{
|
||||
?SESSION => #{user_id => 10, pid => self(), viewable_channels => Viewable}
|
||||
}
|
||||
}.
|
||||
|
||||
data(Channels, Members) ->
|
||||
#{
|
||||
<<"guild">> => #{<<"owner_id">> => <<"7">>},
|
||||
<<"roles">> => [role(?GUILD_ID), role(?VIEWER_ROLE), role(?OTHER_ROLE)],
|
||||
<<"members">> => Members,
|
||||
<<"channels">> => Channels,
|
||||
<<"channel_index">> => guild_data_index:build_id_index(Channels)
|
||||
}.
|
||||
|
||||
members() ->
|
||||
maps:from_list(
|
||||
[{7, member(7, [])}] ++
|
||||
[{Id, member(Id, [?VIEWER_ROLE])} || Id <- [10, 20]] ++
|
||||
[{30, member(30, [?OTHER_ROLE])}]
|
||||
).
|
||||
|
||||
without_member(UserId, #{data := Data} = State) ->
|
||||
State#{data => Data#{<<"members">> => maps:remove(UserId, maps:get(<<"members">>, Data))}}.
|
||||
|
||||
with_channels(Channels, #{data := Data} = State) ->
|
||||
State#{
|
||||
data => Data#{
|
||||
<<"channels">> => Channels,
|
||||
<<"channel_index">> => guild_data_index:build_id_index(Channels)
|
||||
}
|
||||
}.
|
||||
|
||||
role(RoleId) ->
|
||||
#{<<"id">> => integer_to_binary(RoleId), <<"permissions">> => <<"0">>}.
|
||||
|
||||
member(UserId, RoleIds) ->
|
||||
#{
|
||||
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
|
||||
<<"roles">> => [integer_to_binary(RoleId) || RoleId <- RoleIds]
|
||||
}.
|
||||
|
||||
viewer_overwrites() ->
|
||||
[overwrite(?VIEWER_ROLE, 0)].
|
||||
|
||||
other_overwrites() ->
|
||||
[overwrite(?OTHER_ROLE, 0)].
|
||||
|
||||
overwrite(TargetId, Type) ->
|
||||
#{
|
||||
<<"id">> => integer_to_binary(TargetId),
|
||||
<<"type">> => Type,
|
||||
<<"allow">> => integer_to_binary(constants:view_channel_permission()),
|
||||
<<"deny">> => <<"0">>
|
||||
}.
|
||||
|
||||
channel(ChannelId, Overwrites) ->
|
||||
#{
|
||||
<<"id">> => integer_to_binary(ChannelId),
|
||||
<<"type">> => 0,
|
||||
<<"permission_overwrites">> => Overwrites
|
||||
}.
|
||||
|
||||
receive_mutual() ->
|
||||
receive
|
||||
{dm_partner_mutual, _, _} = Msg -> Msg
|
||||
after 100 ->
|
||||
none
|
||||
end.
|
||||
|
||||
with_flag(Value, Fun) ->
|
||||
Previous = application:get_env(fluxer_gateway, ?FLAG),
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_flag(Previous)
|
||||
end.
|
||||
|
||||
restore_flag(undefined) ->
|
||||
application:unset_env(fluxer_gateway, ?FLAG);
|
||||
restore_flag({ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value).
|
||||
@@ -0,0 +1,93 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(presence_targets_dm_partner_tests).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(SELF, 1).
|
||||
-define(GUILD, 900).
|
||||
-define(FLAG, dm_presence_mutual_context).
|
||||
|
||||
eligible_direct_dm_is_a_presence_target_test() ->
|
||||
with_flag(true, fun() ->
|
||||
State = state(#{?GUILD => #{2 => true}}, connected()),
|
||||
?assertEqual(
|
||||
#{100 => #{2 => true}, 300 => #{5 => true, 6 => true}},
|
||||
presence_targets:group_dm_recipients_from_state(State)
|
||||
)
|
||||
end).
|
||||
|
||||
direct_dm_without_mutual_context_is_not_a_target_test() ->
|
||||
with_flag(true, fun() ->
|
||||
State = state(#{}, connected()),
|
||||
?assertEqual(
|
||||
#{300 => #{5 => true, 6 => true}},
|
||||
presence_targets:group_dm_recipients_from_state(State)
|
||||
)
|
||||
end).
|
||||
|
||||
mutual_context_from_a_disconnected_guild_is_ignored_test() ->
|
||||
with_flag(true, fun() ->
|
||||
State = state(#{?GUILD => #{2 => true}}, #{?GUILD => undefined}),
|
||||
?assertEqual(
|
||||
#{300 => #{5 => true, 6 => true}},
|
||||
presence_targets:group_dm_recipients_from_state(State)
|
||||
)
|
||||
end).
|
||||
|
||||
disabled_flag_keeps_direct_dms_out_test() ->
|
||||
with_flag(false, fun() ->
|
||||
State = state(#{?GUILD => #{2 => true, 3 => true}}, connected()),
|
||||
?assertEqual(
|
||||
#{300 => #{5 => true, 6 => true}},
|
||||
presence_targets:group_dm_recipients_from_state(State)
|
||||
)
|
||||
end).
|
||||
|
||||
user_allowlist_gates_direct_dms_test() ->
|
||||
State = state(#{?GUILD => #{2 => true}}, connected()),
|
||||
with_flag({users, [?SELF]}, fun() ->
|
||||
?assert(maps:is_key(100, presence_targets:group_dm_recipients_from_state(State)))
|
||||
end),
|
||||
with_flag({users, [42]}, fun() ->
|
||||
?assertNot(maps:is_key(100, presence_targets:group_dm_recipients_from_state(State)))
|
||||
end).
|
||||
|
||||
direct_dm_partner_ids_lists_one_to_one_partners_only_test() ->
|
||||
?assertEqual([2, 3], presence_targets:direct_dm_partner_ids(state(#{}, connected()))).
|
||||
|
||||
state(MutualByGuild, Guilds) ->
|
||||
#{
|
||||
user_id => ?SELF,
|
||||
guilds => Guilds,
|
||||
dm_mutual_by_guild => MutualByGuild,
|
||||
channels => #{
|
||||
100 => channel(1, [?SELF, 2]),
|
||||
200 => channel(1, [?SELF, 3]),
|
||||
300 => channel(3, [?SELF, 5, 6])
|
||||
}
|
||||
}.
|
||||
|
||||
channel(Type, RecipientIds) ->
|
||||
#{
|
||||
<<"type">> => Type,
|
||||
<<"recipients">> => [#{<<"id">> => integer_to_binary(Id)} || Id <- RecipientIds]
|
||||
}.
|
||||
|
||||
connected() ->
|
||||
#{?GUILD => {self(), make_ref()}}.
|
||||
|
||||
with_flag(Value, Fun) ->
|
||||
Previous = application:get_env(fluxer_gateway, ?FLAG),
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_flag(Previous)
|
||||
end.
|
||||
|
||||
restore_flag(undefined) ->
|
||||
application:unset_env(fluxer_gateway, ?FLAG);
|
||||
restore_flag({ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value).
|
||||
@@ -0,0 +1,97 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(session_dm_partners_tests).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(SELF, 1).
|
||||
-define(GUILD, 900).
|
||||
-define(SESSION_ID, <<"session-1">>).
|
||||
-define(FLAG, dm_presence_mutual_context).
|
||||
|
||||
handle_mutual_stores_partners_for_a_connected_guild_test() ->
|
||||
{noreply, State} = session_dm_partners:handle_mutual(?GUILD, [2], state(connected())),
|
||||
?assertEqual(#{?GUILD => #{2 => true}}, maps:get(dm_mutual_by_guild, State)).
|
||||
|
||||
handle_mutual_ignores_a_guild_that_is_not_connected_test() ->
|
||||
Initial = state(#{}),
|
||||
?assertEqual({noreply, Initial}, session_dm_partners:handle_mutual(?GUILD, [2], Initial)).
|
||||
|
||||
handle_mutual_drops_the_guild_when_nothing_is_mutual_test() ->
|
||||
{noreply, With} = session_dm_partners:handle_mutual(?GUILD, [2], state(connected())),
|
||||
{noreply, Without} = session_dm_partners:handle_mutual(?GUILD, [], With),
|
||||
?assertEqual(#{}, maps:get(dm_mutual_by_guild, Without)).
|
||||
|
||||
forget_guild_drops_its_partners_test() ->
|
||||
{noreply, With} = session_dm_partners:handle_mutual(?GUILD, [2], state(connected())),
|
||||
?assertEqual(
|
||||
#{}, maps:get(dm_mutual_by_guild, session_dm_partners:forget_guild(?GUILD, With))
|
||||
).
|
||||
|
||||
register_all_sends_direct_partners_to_every_connected_guild_test() ->
|
||||
with_flag(true, fun() ->
|
||||
_ = session_dm_partners:register_all(state(connected())),
|
||||
?assertEqual({update_dm_partners, ?SESSION_ID, [2, 3]}, receive_cast())
|
||||
end).
|
||||
|
||||
register_guild_sends_direct_partners_to_that_guild_test() ->
|
||||
with_flag(true, fun() ->
|
||||
_ = session_dm_partners:register_guild(?GUILD, self(), state(#{})),
|
||||
?assertEqual({update_dm_partners, ?SESSION_ID, [2, 3]}, receive_cast())
|
||||
end).
|
||||
|
||||
register_all_is_inert_when_disabled_test() ->
|
||||
with_flag(false, fun() ->
|
||||
_ = session_dm_partners:register_all(state(connected())),
|
||||
?assertEqual(none, receive_cast())
|
||||
end).
|
||||
|
||||
bots_never_register_test() ->
|
||||
with_flag(true, fun() ->
|
||||
_ = session_dm_partners:register_all((state(connected()))#{bot => true}),
|
||||
?assertEqual(none, receive_cast())
|
||||
end).
|
||||
|
||||
state(Guilds) ->
|
||||
#{
|
||||
id => ?SESSION_ID,
|
||||
user_id => ?SELF,
|
||||
presence_pid => undefined,
|
||||
guilds => Guilds,
|
||||
channels => #{
|
||||
100 => channel(1, [?SELF, 2]),
|
||||
200 => channel(1, [?SELF, 3]),
|
||||
300 => channel(3, [?SELF, 5, 6])
|
||||
}
|
||||
}.
|
||||
|
||||
channel(Type, RecipientIds) ->
|
||||
#{
|
||||
<<"type">> => Type,
|
||||
<<"recipients">> => [#{<<"id">> => integer_to_binary(Id)} || Id <- RecipientIds]
|
||||
}.
|
||||
|
||||
connected() ->
|
||||
#{?GUILD => {self(), make_ref()}}.
|
||||
|
||||
receive_cast() ->
|
||||
receive
|
||||
{'$gen_cast', Msg} -> Msg
|
||||
after 100 ->
|
||||
none
|
||||
end.
|
||||
|
||||
with_flag(Value, Fun) ->
|
||||
Previous = application:get_env(fluxer_gateway, ?FLAG),
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_flag(Previous)
|
||||
end.
|
||||
|
||||
restore_flag(undefined) ->
|
||||
application:unset_env(fluxer_gateway, ?FLAG);
|
||||
restore_flag({ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, ?FLAG, Value).
|
||||
Reference in New Issue
Block a user