215 lines
7.2 KiB
Erlang
215 lines
7.2 KiB
Erlang
%% Copyright (C) 2026 Fluxer Contributors
|
|
%%
|
|
%% This file is part of Fluxer.
|
|
%%
|
|
%% Fluxer is free software: you can redistribute it and/or modify
|
|
%% it under the terms of the GNU Affero General Public License as published by
|
|
%% the Free Software Foundation, either version 3 of the License, or
|
|
%% (at your option) any later version.
|
|
%%
|
|
%% Fluxer is distributed in the hope that it will be useful,
|
|
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
%% GNU Affero General Public License for more details.
|
|
%%
|
|
%% You should have received a copy of the GNU Affero General Public License
|
|
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
-module(presence_utils).
|
|
|
|
-export([
|
|
collect_guild_member_presences/1,
|
|
collect_guild_member_ids/1,
|
|
filter_self_presence/2,
|
|
is_visible_presence/1,
|
|
batch_presences/1,
|
|
send_presence_bulk/4
|
|
]).
|
|
|
|
-define(PRESENCE_BATCH_SIZE, 500).
|
|
|
|
-type user_id() :: integer().
|
|
|
|
-spec collect_guild_member_presences(map()) -> [map()].
|
|
collect_guild_member_presences(GuildState) ->
|
|
MemberIds = collect_guild_member_ids(GuildState),
|
|
case MemberIds of
|
|
[] ->
|
|
[];
|
|
_ ->
|
|
Presences = presence_cache:bulk_get(MemberIds),
|
|
[P || P <- Presences, is_visible_presence(P)]
|
|
end.
|
|
|
|
-spec collect_guild_member_ids(map()) -> [user_id()].
|
|
collect_guild_member_ids(GuildState) ->
|
|
Members = get_members_from_guild_state(GuildState),
|
|
MemberIds = [member_user_id(M) || M <- Members],
|
|
[Id || Id <- MemberIds, Id =/= undefined].
|
|
|
|
-spec filter_self_presence(user_id(), [map()]) -> [map()].
|
|
filter_self_presence(UserId, Presences) ->
|
|
[P || P <- Presences, presence_user_id(P) =/= UserId].
|
|
|
|
-spec is_visible_presence(map()) -> boolean().
|
|
is_visible_presence(Presence) ->
|
|
Status = maps:get(<<"status">>, Presence, <<"offline">>),
|
|
Status =/= <<"offline">> andalso Status =/= <<"invisible">>.
|
|
|
|
-spec batch_presences([map()]) -> [[map()]].
|
|
batch_presences([]) ->
|
|
[];
|
|
batch_presences(Presences) ->
|
|
batch_presences(Presences, []).
|
|
|
|
-spec batch_presences([map()], [[map()]]) -> [[map()]].
|
|
batch_presences([], Acc) ->
|
|
lists:reverse(Acc);
|
|
batch_presences(Presences, Acc) ->
|
|
{Batch, Rest} = take_batch(Presences, ?PRESENCE_BATCH_SIZE),
|
|
batch_presences(Rest, [Batch | Acc]).
|
|
|
|
-spec send_presence_bulk(pid(), integer(), user_id(), [map()]) -> ok.
|
|
send_presence_bulk(_Pid, _GuildId, _UserId, []) ->
|
|
ok;
|
|
send_presence_bulk(Pid, GuildId, UserId, Presences) ->
|
|
FilteredPresences = filter_self_presence(UserId, Presences),
|
|
case FilteredPresences of
|
|
[] ->
|
|
ok;
|
|
_ ->
|
|
Batches = batch_presences(FilteredPresences),
|
|
lists:foreach(
|
|
fun(Batch) ->
|
|
BulkPayload = #{
|
|
<<"guild_id">> => integer_to_binary(GuildId),
|
|
<<"presences">> => Batch
|
|
},
|
|
gen_server:cast(Pid, {dispatch, presence_update_bulk, BulkPayload})
|
|
end,
|
|
Batches
|
|
)
|
|
end.
|
|
|
|
-spec get_members_from_guild_state(map()) -> [map()].
|
|
get_members_from_guild_state(GuildState) ->
|
|
case maps:get(data, GuildState, undefined) of
|
|
undefined ->
|
|
guild_data_index:member_values(GuildState);
|
|
Data ->
|
|
guild_data_index:member_values(Data)
|
|
end.
|
|
|
|
-spec member_user_id(map()) -> user_id() | undefined.
|
|
member_user_id(Member) ->
|
|
User = maps:get(<<"user">>, Member, #{}),
|
|
map_utils:get_integer(User, <<"id">>, undefined).
|
|
|
|
-spec presence_user_id(map() | term()) -> user_id() | undefined.
|
|
presence_user_id(P) when is_map(P) ->
|
|
User = maps:get(<<"user">>, P, #{}),
|
|
map_utils:get_integer(User, <<"id">>, undefined);
|
|
presence_user_id(_) ->
|
|
undefined.
|
|
|
|
-spec take_batch([T], pos_integer()) -> {[T], [T]} when T :: term().
|
|
take_batch(List, N) when length(List) =< N ->
|
|
{List, []};
|
|
take_batch(List, N) ->
|
|
{lists:sublist(List, N), lists:nthtail(N, List)}.
|
|
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
batch_presences_empty_test() ->
|
|
?assertEqual([], batch_presences([])).
|
|
|
|
batch_presences_small_list_test() ->
|
|
Presences = [#{<<"user">> => #{<<"id">> => I}} || I <- lists:seq(1, 10)],
|
|
Batches = batch_presences(Presences),
|
|
?assertEqual(1, length(Batches)),
|
|
?assertEqual(10, length(hd(Batches))).
|
|
|
|
batch_presences_exact_batch_size_test() ->
|
|
Presences = [#{<<"user">> => #{<<"id">> => I}} || I <- lists:seq(1, 500)],
|
|
Batches = batch_presences(Presences),
|
|
?assertEqual(1, length(Batches)),
|
|
?assertEqual(500, length(hd(Batches))).
|
|
|
|
batch_presences_multiple_batches_test() ->
|
|
Presences = [#{<<"user">> => #{<<"id">> => I}} || I <- lists:seq(1, 1250)],
|
|
Batches = batch_presences(Presences),
|
|
?assertEqual(3, length(Batches)),
|
|
?assertEqual(500, length(lists:nth(1, Batches))),
|
|
?assertEqual(500, length(lists:nth(2, Batches))),
|
|
?assertEqual(250, length(lists:nth(3, Batches))).
|
|
|
|
filter_self_presence_test() ->
|
|
Presences = [
|
|
#{<<"user">> => #{<<"id">> => <<"1">>}},
|
|
#{<<"user">> => #{<<"id">> => <<"2">>}},
|
|
#{<<"user">> => #{<<"id">> => <<"3">>}}
|
|
],
|
|
Filtered = filter_self_presence(2, Presences),
|
|
?assertEqual(2, length(Filtered)),
|
|
?assert(
|
|
not lists:any(
|
|
fun(P) -> presence_user_id(P) =:= 2 end,
|
|
Filtered
|
|
)
|
|
).
|
|
|
|
is_visible_presence_online_test() ->
|
|
?assert(is_visible_presence(#{<<"status">> => <<"online">>})),
|
|
?assert(is_visible_presence(#{<<"status">> => <<"idle">>})),
|
|
?assert(is_visible_presence(#{<<"status">> => <<"dnd">>})).
|
|
|
|
is_visible_presence_offline_test() ->
|
|
?assertNot(is_visible_presence(#{<<"status">> => <<"offline">>})),
|
|
?assertNot(is_visible_presence(#{<<"status">> => <<"invisible">>})),
|
|
?assertNot(is_visible_presence(#{})).
|
|
|
|
collect_guild_member_ids_internal_format_test() ->
|
|
GuildState = #{
|
|
data => #{
|
|
<<"members">> => [
|
|
#{<<"user">> => #{<<"id">> => <<"100">>}},
|
|
#{<<"user">> => #{<<"id">> => <<"200">>}}
|
|
]
|
|
}
|
|
},
|
|
Ids = collect_guild_member_ids(GuildState),
|
|
?assertEqual([100, 200], lists:sort(Ids)).
|
|
|
|
collect_guild_member_ids_external_format_test() ->
|
|
GuildState = #{
|
|
<<"members">> => [
|
|
#{<<"user">> => #{<<"id">> => <<"100">>}},
|
|
#{<<"user">> => #{<<"id">> => <<"200">>}}
|
|
]
|
|
},
|
|
Ids = collect_guild_member_ids(GuildState),
|
|
?assertEqual([100, 200], lists:sort(Ids)).
|
|
|
|
take_batch_small_list_test() ->
|
|
{Batch, Rest} = take_batch([1, 2, 3], 10),
|
|
?assertEqual([1, 2, 3], Batch),
|
|
?assertEqual([], Rest).
|
|
|
|
take_batch_exact_test() ->
|
|
{Batch, Rest} = take_batch([1, 2, 3], 3),
|
|
?assertEqual([1, 2, 3], Batch),
|
|
?assertEqual([], Rest).
|
|
|
|
take_batch_split_test() ->
|
|
{Batch, Rest} = take_batch([1, 2, 3, 4, 5], 2),
|
|
?assertEqual([1, 2], Batch),
|
|
?assertEqual([3, 4, 5], Rest).
|
|
|
|
presence_user_id_test() ->
|
|
?assertEqual(123, presence_user_id(#{<<"user">> => #{<<"id">> => <<"123">>}})),
|
|
?assertEqual(undefined, presence_user_id(#{<<"user">> => #{}})),
|
|
?assertEqual(undefined, presence_user_id(#{})),
|
|
?assertEqual(undefined, presence_user_id(invalid)).
|
|
-endif.
|