fluxer/fluxer_gateway/src/presence/presence_utils.erl
2026-02-17 12:22:36 +00:00

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.