initial commit
This commit is contained in:
313
fluxer_gateway/src/guild/guild_data.erl
Normal file
313
fluxer_gateway/src/guild/guild_data.erl
Normal file
@@ -0,0 +1,313 @@
|
||||
%% 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(guild_data).
|
||||
|
||||
-export([get_guild_data/2]).
|
||||
-export([get_guild_member/2]).
|
||||
-export([has_member/2]).
|
||||
-export([list_guild_members/2]).
|
||||
-export([get_vanity_url_channel/1]).
|
||||
-export([get_first_viewable_text_channel/1]).
|
||||
-export([get_guild_state/2]).
|
||||
-export([find_everyone_viewable_text_channel/2]).
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type guild_reply(T) :: {reply, T, guild_state()}.
|
||||
-type guild_data_map() :: map().
|
||||
-type guild_member() :: map().
|
||||
-type channel_list() :: [map()].
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-spec get_guild_data(map(), guild_state()) -> guild_reply(map()).
|
||||
get_guild_data(#{user_id := UserId}, State) ->
|
||||
Data = guild_data_map(State),
|
||||
case UserId of
|
||||
null ->
|
||||
GuildData = build_complete_guild_data(Data, State),
|
||||
Reply = #{guild_data => GuildData},
|
||||
{reply, Reply, State};
|
||||
_ ->
|
||||
Members = map_utils:ensure_list(maps:get(<<"members">>, Data, [])),
|
||||
case member_in_list(UserId, Members) of
|
||||
false ->
|
||||
{reply, #{guild_data => null, error_reason => <<"forbidden">>}, State};
|
||||
true ->
|
||||
GuildData = build_complete_guild_data(Data, State),
|
||||
{reply, #{guild_data => GuildData}, State}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec get_guild_member(map(), guild_state()) -> guild_reply(map()).
|
||||
get_guild_member(#{user_id := UserId}, State) ->
|
||||
case find_member_by_user_id(UserId, State) of
|
||||
undefined ->
|
||||
{reply, #{success => false, member_data => null}, State};
|
||||
Member ->
|
||||
{reply, #{success => true, member_data => Member}, State}
|
||||
end.
|
||||
|
||||
-spec has_member(map(), guild_state()) -> guild_reply(map()).
|
||||
has_member(#{user_id := UserId}, State) ->
|
||||
case find_member_by_user_id(UserId, State) of
|
||||
undefined ->
|
||||
{reply, #{has_member => false}, State};
|
||||
_ ->
|
||||
{reply, #{has_member => true}, State}
|
||||
end.
|
||||
|
||||
-spec list_guild_members(map(), guild_state()) -> guild_reply(map()).
|
||||
list_guild_members(#{limit := Limit, offset := Offset}, State) ->
|
||||
Data = guild_data_map(State),
|
||||
AllMembers = map_utils:ensure_list(maps:get(<<"members">>, Data, [])),
|
||||
TotalCount = length(AllMembers),
|
||||
PaginatedMembers = paginate_members(AllMembers, Limit, Offset),
|
||||
{reply, #{members => PaginatedMembers, total => TotalCount}, State}.
|
||||
|
||||
-spec get_vanity_url_channel(guild_state()) -> guild_reply(map()).
|
||||
get_vanity_url_channel(State) ->
|
||||
Channels = channels_from_state(State),
|
||||
EveryoneChannelId = find_everyone_viewable_text_channel(Channels, State),
|
||||
{reply, #{channel_id => EveryoneChannelId}, State}.
|
||||
|
||||
-spec get_first_viewable_text_channel(guild_state()) -> guild_reply(map()).
|
||||
get_first_viewable_text_channel(State) ->
|
||||
Channels = channels_from_state(State),
|
||||
EveryoneChannelId = find_everyone_viewable_text_channel(Channels, State),
|
||||
{reply, #{channel_id => EveryoneChannelId}, State}.
|
||||
|
||||
-spec get_guild_state(integer(), guild_state()) -> map().
|
||||
get_guild_state(UserId, State) ->
|
||||
Data = guild_data_map(State),
|
||||
GuildId = map_utils:get_integer(State, id, 0),
|
||||
AllChannels = channels_from_data(Data),
|
||||
AllMembers = map_utils:ensure_list(maps:get(<<"members">>, Data, [])),
|
||||
Member = find_member_by_user_id(UserId, State),
|
||||
{ViewableChannels, JoinedAt} = derive_member_view(UserId, Member, State, AllChannels),
|
||||
OnlineCount = guild_member_list:get_online_count(State),
|
||||
OwnMemberList = case Member of
|
||||
undefined -> [];
|
||||
M -> [M]
|
||||
end,
|
||||
#{
|
||||
<<"id">> => integer_to_binary(GuildId),
|
||||
<<"properties">> => maps:get(<<"guild">>, Data, #{}),
|
||||
<<"roles">> => map_utils:ensure_list(maps:get(<<"roles">>, Data, [])),
|
||||
<<"channels">> => ViewableChannels,
|
||||
<<"emojis">> => maps:get(<<"emojis">>, Data, []),
|
||||
<<"stickers">> => maps:get(<<"stickers">>, Data, []),
|
||||
<<"members">> => OwnMemberList,
|
||||
<<"member_count">> => length(AllMembers),
|
||||
<<"online_count">> => OnlineCount,
|
||||
<<"presences">> => [],
|
||||
<<"voice_states">> => guild_voice:get_voice_states_list(State),
|
||||
<<"joined_at">> => JoinedAt
|
||||
}.
|
||||
|
||||
-spec find_everyone_viewable_text_channel(channel_list(), guild_state()) -> integer() | null.
|
||||
find_everyone_viewable_text_channel(Channels, State) ->
|
||||
GuildId = map_utils:get_integer(State, id, 0),
|
||||
Data = guild_data_map(State),
|
||||
Roles = map_utils:ensure_list(maps:get(<<"roles">>, Data, [])),
|
||||
EveryonePerms = role_permissions_for_id(Roles, GuildId),
|
||||
lists:foldl(
|
||||
fun(Channel, Acc) ->
|
||||
case Acc of
|
||||
null ->
|
||||
select_first_viewable(Channel, GuildId, EveryonePerms);
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
null,
|
||||
map_utils:ensure_list(Channels)
|
||||
).
|
||||
|
||||
find_member_by_user_id(UserId, State) ->
|
||||
guild_permissions:find_member_by_user_id(UserId, State).
|
||||
|
||||
-spec guild_data_map(guild_state()) -> guild_data_map().
|
||||
guild_data_map(State) ->
|
||||
map_utils:ensure_map(map_utils:get_safe(State, data, #{})).
|
||||
|
||||
-spec build_complete_guild_data(guild_data_map(), guild_state()) -> map().
|
||||
build_complete_guild_data(Data, _State) ->
|
||||
GuildProperties = maps:get(<<"guild">>, Data, #{}),
|
||||
maps:merge(GuildProperties, #{
|
||||
<<"roles">> => map_utils:ensure_list(maps:get(<<"roles">>, Data, [])),
|
||||
<<"channels">> => map_utils:ensure_list(maps:get(<<"channels">>, Data, [])),
|
||||
<<"emojis">> => map_utils:ensure_list(maps:get(<<"emojis">>, Data, [])),
|
||||
<<"stickers">> => map_utils:ensure_list(maps:get(<<"stickers">>, Data, []))
|
||||
}).
|
||||
|
||||
-spec channels_from_state(guild_state()) -> channel_list().
|
||||
channels_from_state(State) ->
|
||||
Data = guild_data_map(State),
|
||||
channels_from_data(Data).
|
||||
|
||||
-spec channels_from_data(guild_data_map()) -> channel_list().
|
||||
channels_from_data(Data) ->
|
||||
map_utils:ensure_list(maps:get(<<"channels">>, Data, [])).
|
||||
|
||||
-spec member_in_list(integer(), [guild_member()]) -> boolean().
|
||||
member_in_list(UserId, Members) ->
|
||||
lists:any(fun(Member) -> member_matches(UserId, Member) end, Members).
|
||||
|
||||
-spec member_matches(integer(), guild_member()) -> boolean().
|
||||
member_matches(UserId, Member) ->
|
||||
MemberUser = map_utils:ensure_map(maps:get(<<"user">>, Member, #{})),
|
||||
case map_utils:get_integer(MemberUser, <<"id">>, undefined) of
|
||||
undefined -> false;
|
||||
Id -> Id =:= UserId
|
||||
end.
|
||||
|
||||
-spec paginate_members([guild_member()], non_neg_integer(), non_neg_integer()) -> [guild_member()].
|
||||
paginate_members(Members, Limit, Offset) ->
|
||||
case Offset >= length(Members) of
|
||||
true ->
|
||||
[];
|
||||
false ->
|
||||
Remaining = lists:nthtail(Offset, Members),
|
||||
case length(Remaining) > Limit of
|
||||
true -> lists:sublist(Remaining, Limit);
|
||||
false -> Remaining
|
||||
end
|
||||
end.
|
||||
|
||||
-spec derive_member_view(integer(), guild_member() | undefined, guild_state(), channel_list()) ->
|
||||
{channel_list(), term()}.
|
||||
derive_member_view(_UserId, undefined, _State, _Channels) ->
|
||||
{[], null};
|
||||
derive_member_view(UserId, Member, State, Channels) ->
|
||||
Filtered =
|
||||
lists:filter(
|
||||
fun(Channel) ->
|
||||
ChannelId = map_utils:get_integer(Channel, <<"id">>, undefined),
|
||||
case ChannelId of
|
||||
undefined -> false;
|
||||
_ -> guild_permissions:can_view_channel(UserId, ChannelId, Member, State)
|
||||
end
|
||||
end,
|
||||
Channels
|
||||
),
|
||||
JoinedAt = maps:get(<<"joined_at">>, Member, null),
|
||||
{Filtered, JoinedAt}.
|
||||
|
||||
-spec role_permissions_for_id(list(), integer()) -> integer().
|
||||
role_permissions_for_id(Roles, GuildId) ->
|
||||
lists:foldl(
|
||||
fun(Role, Acc) ->
|
||||
case map_utils:get_integer(Role, <<"id">>, undefined) of
|
||||
GuildId -> map_utils:get_integer(Role, <<"permissions">>, 0);
|
||||
_ -> Acc
|
||||
end
|
||||
end,
|
||||
0,
|
||||
map_utils:ensure_list(Roles)
|
||||
).
|
||||
|
||||
-spec select_first_viewable(map(), integer(), integer()) -> integer() | null.
|
||||
select_first_viewable(Channel, GuildId, BasePerms) ->
|
||||
ChannelType = map_utils:get_integer(Channel, <<"type">>, undefined),
|
||||
ChannelId = map_utils:get_integer(Channel, <<"id">>, undefined),
|
||||
select_first_viewable(ChannelType, ChannelId, Channel, GuildId, BasePerms).
|
||||
|
||||
select_first_viewable(0, ChannelId, Channel, GuildId, BasePerms) when is_integer(ChannelId) ->
|
||||
case (BasePerms band constants:administrator_permission()) =/= 0 of
|
||||
true ->
|
||||
ChannelId;
|
||||
false ->
|
||||
FinalPerms = guild_permissions:apply_channel_overwrites(
|
||||
BasePerms, GuildId, [GuildId], Channel, GuildId
|
||||
),
|
||||
case (FinalPerms band constants:view_channel_permission()) =/= 0 of
|
||||
true -> ChannelId;
|
||||
false -> null
|
||||
end
|
||||
end;
|
||||
select_first_viewable(_, _, _, _, _) ->
|
||||
null.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
get_guild_data_membership_gate_test() ->
|
||||
State = test_state(),
|
||||
{reply, Reply1, _} = get_guild_data(#{user_id => 999}, State),
|
||||
?assertEqual(null, maps:get(guild_data, Reply1)),
|
||||
?assertEqual(<<"forbidden">>, maps:get(error_reason, Reply1)),
|
||||
|
||||
{reply, Reply2, _} = get_guild_data(#{user_id => 200}, State),
|
||||
Guild = maps:get(guild_data, Reply2),
|
||||
?assertEqual(<<"Fluxer">>, maps:get(<<"name">>, Guild)),
|
||||
Roles = maps:get(<<"roles">>, Guild, []),
|
||||
?assert(length(Roles) > 0).
|
||||
|
||||
get_guild_state_filters_channels_test() ->
|
||||
State = test_state(),
|
||||
GuildState = get_guild_state(200, State),
|
||||
Channels = maps:get(<<"channels">>, GuildState),
|
||||
?assert(lists:any(fun(Chan) -> maps:get(<<"id">>, Chan) =:= <<"500">> end, Channels)),
|
||||
?assertEqual(<<"2024-01-01T00:00:00Z">>, maps:get(<<"joined_at">>, GuildState)).
|
||||
|
||||
find_everyone_viewable_text_channel_test() ->
|
||||
State = test_state(),
|
||||
Data = guild_data_map(State),
|
||||
Channels = maps:get(<<"channels">>, Data),
|
||||
ChannelId = find_everyone_viewable_text_channel(Channels, State),
|
||||
?assertEqual(500, ChannelId).
|
||||
|
||||
test_state() ->
|
||||
GuildId = 100,
|
||||
ViewPerm = constants:view_channel_permission(),
|
||||
#{
|
||||
id => GuildId,
|
||||
data => #{
|
||||
<<"guild">> => #{<<"name">> => <<"Fluxer">>},
|
||||
<<"roles">> => [
|
||||
#{
|
||||
<<"id">> => integer_to_binary(GuildId),
|
||||
<<"permissions">> => integer_to_binary(ViewPerm)
|
||||
}
|
||||
],
|
||||
<<"channels">> => [
|
||||
#{
|
||||
<<"id">> => <<"500">>,
|
||||
<<"type">> => 0,
|
||||
<<"permission_overwrites">> => []
|
||||
},
|
||||
#{
|
||||
<<"id">> => <<"501">>,
|
||||
<<"type">> => 2,
|
||||
<<"permission_overwrites">> => []
|
||||
}
|
||||
],
|
||||
<<"members">> => [
|
||||
#{
|
||||
<<"user">> => #{<<"id">> => <<"200">>},
|
||||
<<"roles">> => [integer_to_binary(GuildId)],
|
||||
<<"joined_at">> => <<"2024-01-01T00:00:00Z">>
|
||||
}
|
||||
],
|
||||
<<"emojis">> => [],
|
||||
<<"stickers">> => []
|
||||
}
|
||||
}.
|
||||
|
||||
-endif.
|
||||
Reference in New Issue
Block a user