fluxer/fluxer_gateway/src/gateway/gateway_rpc_guild.erl

1184 lines
50 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(gateway_rpc_guild).
-export([execute_method/2]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-define(MAX_BATCH_SIZE, 100).
-define(BATCH_TIMEOUT_MS, 5000).
-define(GUILD_LOOKUP_TIMEOUT, 2000).
-define(GUILD_CALL_TIMEOUT, 3000).
-spec execute_method(binary(), map()) -> term().
execute_method(<<"guild.dispatch">>, #{
<<"guild_id">> := GuildIdBin, <<"event">> := Event, <<"data">> := Data
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
EventAtom = constants:dispatch_event_atom(Event),
IsAlive = erlang:is_process_alive(Pid),
logger:info("rpc guild.dispatch: guild_id=~p event=~p pid=~p alive=~p",
[GuildId, EventAtom, Pid, IsAlive]),
gen_server:cast(Pid, {dispatch, #{event => EventAtom, data => Data}}),
true
end);
execute_method(<<"guild.get_counts">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
case gen_server:call(Pid, {get_counts}, ?GUILD_CALL_TIMEOUT) of
#{member_count := MemberCount, presence_count := PresenceCount} ->
#{<<"member_count">> => MemberCount, <<"presence_count">> => PresenceCount};
_ ->
throw({error, <<"guild_counts_error">>})
end
end);
execute_method(<<"guild.get_data">>, #{<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
{ok, UserId} = validation:validate_optional_snowflake(UserIdBin),
with_guild(
GuildId,
fun(Pid) ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {get_guild_data, Request}, ?GUILD_CALL_TIMEOUT) of
#{guild_data := null, error_reason := <<"forbidden">>} ->
throw({error, <<"forbidden">>});
#{guild_data := null} ->
throw({error, <<"forbidden">>});
#{guild_data := GuildData} ->
GuildData;
_ ->
throw({error, <<"guild_data_error">>})
end
end,
<<"guild_not_found">>
);
execute_method(<<"guild.get_member">>, #{<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
case get_member_cached_or_rpc(GuildId, UserId) of
{ok, MemberData} when is_map(MemberData) ->
#{<<"success">> => true, <<"member_data">> => MemberData};
{ok, undefined} ->
#{<<"success">> => false};
error ->
throw({error, <<"guild_member_error">>})
end;
execute_method(<<"guild.has_member">>, #{<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
case get_has_member_cached_or_rpc(GuildId, UserId) of
{ok, HasMember} ->
#{<<"has_member">> => HasMember};
error ->
throw({error, <<"membership_check_error">>})
end;
execute_method(<<"guild.list_members">>, #{
<<"guild_id">> := GuildIdBin, <<"limit">> := Limit, <<"offset">> := Offset
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{limit => Limit, offset => Offset},
case gen_server:call(Pid, {list_guild_members, Request}, ?GUILD_CALL_TIMEOUT) of
#{members := Members, total := Total} ->
#{<<"members">> => Members, <<"total">> => Total};
_ ->
throw({error, <<"guild_members_error">>})
end
end);
execute_method(<<"guild.list_members_cursor">>, Request) ->
GuildIdBin = maps:get(<<"guild_id">>, Request, undefined),
LimitRaw = maps:get(<<"limit">>, Request, 1),
AfterParam = maps:get(<<"after">>, Request, undefined),
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
Limit = clamp_limit(LimitRaw),
AfterId = parse_optional_snowflake(AfterParam, <<"after">>),
with_guild(GuildId, fun(Pid) ->
CursorRequest = #{<<"limit">> => Limit, <<"after">> => AfterId},
case
gen_server:call(Pid, {list_guild_members_cursor, CursorRequest}, ?GUILD_CALL_TIMEOUT)
of
#{members := Members, total := Total} ->
#{<<"members">> => Members, <<"total">> => Total};
_ ->
throw({error, <<"guild_members_error">>})
end
end);
execute_method(<<"guild.start">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
case gen_server:call(guild_manager, {start_or_lookup, GuildId}, ?GUILD_LOOKUP_TIMEOUT) of
{ok, _Pid} -> true;
{error, Reason} ->
throw({error, <<"guild_start_error:", (error_term_to_binary(Reason))/binary>>});
Other ->
throw({error, <<"guild_start_error:", (error_term_to_binary(Other))/binary>>})
end;
execute_method(<<"guild.stop">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
case gen_server:call(guild_manager, {stop_guild, GuildId}, ?GUILD_CALL_TIMEOUT) of
ok -> true;
_ -> throw({error, <<"guild_stop_error">>})
end;
execute_method(<<"guild.reload">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
case gen_server:call(guild_manager, {reload_guild, GuildId}, ?GUILD_CALL_TIMEOUT) of
ok ->
true;
{error, not_found} ->
case gen_server:call(guild_manager, {start_or_lookup, GuildId}, 20000) of
{ok, _Pid} -> true;
_ -> throw({error, <<"guild_reload_error">>})
end;
_ ->
throw({error, <<"guild_reload_error">>})
end;
execute_method(<<"guild.reload_all">>, #{<<"guild_ids">> := GuildIdsBin}) ->
GuildIds = validation:snowflake_list_or_throw(<<"guild_ids">>, GuildIdsBin),
case gen_server:call(guild_manager, {reload_all_guilds, GuildIds}, 15000) of
#{count := Count} -> #{<<"count">> => Count};
_ -> throw({error, <<"guilds_reload_error">>})
end;
execute_method(<<"guild.shutdown">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
case gen_server:call(guild_manager, {shutdown_guild, GuildId}, ?GUILD_CALL_TIMEOUT) of
ok ->
true;
{error, timeout} ->
case gen_server:call(guild_manager, {stop_guild, GuildId}, ?GUILD_CALL_TIMEOUT) of
ok -> true;
_ -> throw({error, <<"guild_shutdown_error">>})
end;
_ ->
throw({error, <<"guild_shutdown_error">>})
end;
execute_method(<<"guild.get_user_permissions">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin, <<"channel_id">> := ChannelIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ChannelId = parse_channel_id(ChannelIdBin),
case get_permissions_cached_or_rpc(GuildId, UserId, ChannelId) of
{ok, Permissions} ->
#{<<"permissions">> => integer_to_binary(Permissions)};
error ->
throw({error, <<"permissions_error">>})
end;
execute_method(<<"guild.get_user_permissions_batch">>, #{
<<"guild_ids">> := GuildIdsBin, <<"user_id">> := UserIdBin, <<"channel_id">> := ChannelIdBin
}) ->
GuildIds = validation:snowflake_list_or_throw(<<"guild_ids">>, GuildIdsBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ChannelId = parse_channel_id(ChannelIdBin),
UniqueGuildIds = lists:usort(GuildIds),
PermissionsResults = process_batch(
UniqueGuildIds,
fun(GuildId) -> fetch_user_permissions_entry(GuildId, UserId, ChannelId) end,
?BATCH_TIMEOUT_MS
),
PermissionsList = [Result || Result <- PermissionsResults, is_map(Result)],
#{<<"permissions">> => PermissionsList};
execute_method(<<"guild.check_permission">>, #{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"permission">> := PermissionBin,
<<"channel_id">> := ChannelIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
Permission = validation:snowflake_or_throw(<<"permission">>, PermissionBin),
ChannelId = parse_channel_id(ChannelIdBin),
case get_permissions_cached_or_rpc(GuildId, UserId, ChannelId) of
{ok, Permissions} ->
HasPermission = (Permissions band Permission) =:= Permission,
#{<<"has_permission">> => HasPermission};
error ->
throw({error, <<"permission_check_error">>})
end;
execute_method(<<"guild.can_manage_roles">>, #{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"target_user_id">> := TargetUserIdBin,
<<"role_id">> := RoleIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
TargetUserId = validation:snowflake_or_throw(<<"target_user_id">>, TargetUserIdBin),
RoleId = validation:snowflake_or_throw(<<"role_id">>, RoleIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{user_id => UserId, target_user_id => TargetUserId, role_id => RoleId},
case gen_server:call(Pid, {can_manage_roles, Request}, ?GUILD_CALL_TIMEOUT) of
#{can_manage := CanManage} -> #{<<"can_manage">> => CanManage};
_ -> throw({error, <<"role_management_check_error">>})
end
end);
execute_method(<<"guild.can_manage_role">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin, <<"role_id">> := RoleIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
RoleId = validation:snowflake_or_throw(<<"role_id">>, RoleIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{user_id => UserId, role_id => RoleId},
case gen_server:call(Pid, {can_manage_role, Request}, ?GUILD_CALL_TIMEOUT) of
#{can_manage := CanManage} -> #{<<"can_manage">> => CanManage};
_ -> throw({error, <<"role_management_check_error">>})
end
end);
execute_method(<<"guild.get_assignable_roles">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {get_assignable_roles, Request}, ?GUILD_CALL_TIMEOUT) of
#{role_ids := RoleIds} ->
#{<<"role_ids">> => [integer_to_binary(RoleId) || RoleId <- RoleIds]};
_ ->
throw({error, <<"assignable_roles_error">>})
end
end);
execute_method(<<"guild.get_user_max_role_position">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {get_user_max_role_position, Request}, ?GUILD_CALL_TIMEOUT) of
#{position := Position} -> #{<<"position">> => Position};
_ -> throw({error, <<"max_role_position_error">>})
end
end);
execute_method(<<"guild.get_members_with_role">>, #{
<<"guild_id">> := GuildIdBin, <<"role_id">> := RoleIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
RoleId = validation:snowflake_or_throw(<<"role_id">>, RoleIdBin),
case get_members_with_role_cached_or_rpc(GuildId, RoleId) of
{ok, UserIds} ->
#{<<"user_ids">> => [integer_to_binary(UserId) || UserId <- UserIds]};
error ->
throw({error, <<"members_with_role_error">>})
end;
execute_method(<<"guild.check_target_member">>, #{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"target_user_id">> := TargetUserIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
TargetUserId = validation:snowflake_or_throw(<<"target_user_id">>, TargetUserIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{user_id => UserId, target_user_id => TargetUserId},
case gen_server:call(Pid, {check_target_member, Request}, ?GUILD_CALL_TIMEOUT) of
#{can_manage := CanManage} -> #{<<"can_manage">> => CanManage};
_ -> throw({error, <<"target_member_check_error">>})
end
end);
execute_method(<<"guild.get_viewable_channels">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
case get_viewable_channels_cached_or_rpc(GuildId, UserId) of
{ok, ChannelIds} ->
#{<<"channel_ids">> => [integer_to_binary(ChannelId) || ChannelId <- ChannelIds]};
error ->
throw({error, <<"viewable_channels_error">>})
end;
execute_method(<<"guild.get_users_to_mention_by_roles">>, #{
<<"guild_id">> := GuildIdBin,
<<"channel_id">> := ChannelIdBin,
<<"role_ids">> := RoleIds,
<<"author_id">> := AuthorIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
AuthorId = validation:snowflake_or_throw(<<"author_id">>, AuthorIdBin),
RoleIdsList = validation:snowflake_list_or_throw(<<"role_ids">>, RoleIds),
with_guild(GuildId, fun(Pid) ->
Request = #{channel_id => ChannelId, role_ids => RoleIdsList, author_id => AuthorId},
case gen_server:call(Pid, {get_users_to_mention_by_roles, Request}, ?GUILD_CALL_TIMEOUT) of
#{user_ids := UserIds} ->
#{<<"user_ids">> => [integer_to_binary(UserId) || UserId <- UserIds]};
_ ->
throw({error, <<"users_error">>})
end
end);
execute_method(<<"guild.get_users_to_mention_by_user_ids">>, #{
<<"guild_id">> := GuildIdBin,
<<"channel_id">> := ChannelIdBin,
<<"user_ids">> := UserIds,
<<"author_id">> := AuthorIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
AuthorId = validation:snowflake_or_throw(<<"author_id">>, AuthorIdBin),
UserIdsList = validation:snowflake_list_or_throw(<<"user_ids">>, UserIds),
with_guild(GuildId, fun(Pid) ->
Request = #{channel_id => ChannelId, user_ids => UserIdsList, author_id => AuthorId},
case
gen_server:call(Pid, {get_users_to_mention_by_user_ids, Request}, ?GUILD_CALL_TIMEOUT)
of
#{user_ids := ResultUserIds} ->
#{<<"user_ids">> => [integer_to_binary(UserId) || UserId <- ResultUserIds]};
_ ->
throw({error, <<"users_error">>})
end
end);
execute_method(<<"guild.get_all_users_to_mention">>, #{
<<"guild_id">> := GuildIdBin, <<"channel_id">> := ChannelIdBin, <<"author_id">> := AuthorIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
AuthorId = validation:snowflake_or_throw(<<"author_id">>, AuthorIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{channel_id => ChannelId, author_id => AuthorId},
case gen_server:call(Pid, {get_all_users_to_mention, Request}, ?GUILD_CALL_TIMEOUT) of
#{user_ids := UserIds} ->
#{<<"user_ids">> => [integer_to_binary(UserId) || UserId <- UserIds]};
_ ->
throw({error, <<"users_error">>})
end
end);
execute_method(<<"guild.resolve_all_mentions">>, #{
<<"guild_id">> := GuildIdBin,
<<"channel_id">> := ChannelIdBin,
<<"author_id">> := AuthorIdBin,
<<"mention_everyone">> := MentionEveryone,
<<"mention_here">> := MentionHere,
<<"role_ids">> := RoleIds,
<<"user_ids">> := UserIds
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
AuthorId = validation:snowflake_or_throw(<<"author_id">>, AuthorIdBin),
RoleIdsList = validation:snowflake_list_or_throw(<<"role_ids">>, RoleIds),
UserIdsList = validation:snowflake_list_or_throw(<<"user_ids">>, UserIds),
with_guild(GuildId, fun(Pid) ->
Request = #{
channel_id => ChannelId,
author_id => AuthorId,
mention_everyone => MentionEveryone,
mention_here => MentionHere,
role_ids => RoleIdsList,
user_ids => UserIdsList
},
case gen_server:call(Pid, {resolve_all_mentions, Request}, ?GUILD_CALL_TIMEOUT) of
#{user_ids := ResultUserIds} ->
#{<<"user_ids">> => [integer_to_binary(UserId) || UserId <- ResultUserIds]};
_ ->
throw({error, <<"resolve_mentions_error">>})
end
end);
execute_method(<<"guild.get_vanity_url_channel">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
case gen_server:call(Pid, {get_vanity_url_channel}, ?GUILD_CALL_TIMEOUT) of
#{channel_id := ChannelId} when ChannelId =/= null ->
#{<<"channel_id">> => integer_to_binary(ChannelId)};
#{channel_id := null} ->
#{<<"channel_id">> => null};
_ ->
throw({error, <<"vanity_url_channel_error">>})
end
end);
execute_method(<<"guild.get_first_viewable_text_channel">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
case gen_server:call(Pid, {get_first_viewable_text_channel}, ?GUILD_CALL_TIMEOUT) of
#{channel_id := ChannelId} when ChannelId =/= null ->
#{<<"channel_id">> => integer_to_binary(ChannelId)};
#{channel_id := null} ->
#{<<"channel_id">> => null};
_ ->
throw({error, <<"first_viewable_text_channel_error">>})
end
end);
execute_method(<<"guild.update_member_voice">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin, <<"mute">> := Mute, <<"deaf">> := Deaf
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = #{user_id => UserId, mute => Mute, deaf => Deaf},
case gen_server:call(VoicePid, {update_member_voice, Request}, ?GUILD_CALL_TIMEOUT) of
#{success := true} -> #{<<"success">> => true};
#{error := Error} -> throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(
<<"guild.disconnect_voice_user">>,
#{<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin} = Params
) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ConnectionId = maps:get(<<"connection_id">>, Params, null),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = #{user_id => UserId, connection_id => ConnectionId},
case gen_server:call(VoicePid, {disconnect_voice_user, Request}, ?GUILD_CALL_TIMEOUT) of
#{success := true} -> #{<<"success">> => true};
#{error := Error} -> throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(
<<"guild.disconnect_voice_user_if_in_channel">>,
#{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"expected_channel_id">> := ExpectedChannelIdBin
} = Params
) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ExpectedChannelId = validation:snowflake_or_throw(
<<"expected_channel_id">>, ExpectedChannelIdBin
),
ConnectionId = maps:get(<<"connection_id">>, Params, undefined),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = build_disconnect_request(UserId, ExpectedChannelId, ConnectionId),
case
gen_server:call(
VoicePid, {disconnect_voice_user_if_in_channel, Request}, ?GUILD_CALL_TIMEOUT
)
of
#{success := true, ignored := true} -> #{<<"success">> => true, <<"ignored">> => true};
#{success := true} -> #{<<"success">> => true};
#{error := Error} -> throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(<<"guild.disconnect_all_voice_users_in_channel">>, #{
<<"guild_id">> := GuildIdBin, <<"channel_id">> := ChannelIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = #{channel_id => ChannelId},
case
gen_server:call(
VoicePid, {disconnect_all_voice_users_in_channel, Request}, ?GUILD_CALL_TIMEOUT
)
of
#{success := true, disconnected_count := Count} ->
#{<<"success">> => true, <<"disconnected_count">> => Count};
#{error := Error} ->
throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(<<"guild.confirm_voice_connection_from_livekit">>, Params) ->
GuildIdBin = maps:get(<<"guild_id">>, Params),
ConnectionId = maps:get(<<"connection_id">>, Params),
TokenNonce = maps:get(<<"token_nonce">>, Params, undefined),
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = #{connection_id => ConnectionId, token_nonce => TokenNonce},
case
gen_server:call(
VoicePid, {confirm_voice_connection_from_livekit, Request}, ?GUILD_CALL_TIMEOUT
)
of
#{success := true} -> #{<<"success">> => true};
#{success := false, error := Error} ->
#{<<"success">> => false, <<"error">> => normalize_voice_rpc_error(Error)};
{error, _Category, ErrorAtom} ->
#{<<"success">> => false, <<"error">> => normalize_voice_rpc_error(ErrorAtom)};
#{error := Error} -> throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(<<"guild.get_voice_states_for_channel">>, Params) ->
GuildIdBin = maps:get(<<"guild_id">>, Params),
ChannelIdBin = maps:get(<<"channel_id">>, Params),
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
case gen_server:call(VoicePid, {get_voice_states_for_channel, ChannelIdBin}, 10000) of
#{voice_states := VoiceStates} ->
#{<<"voice_states">> => VoiceStates};
_ ->
throw({error, <<"voice_states_error">>})
end
end);
execute_method(<<"guild.get_pending_joins_for_channel">>, Params) ->
GuildIdBin = maps:get(<<"guild_id">>, Params),
ChannelIdBin = maps:get(<<"channel_id">>, Params),
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
case gen_server:call(VoicePid, {get_pending_joins_for_channel, ChannelIdBin}, 10000) of
#{pending_joins := PendingJoins} ->
#{<<"pending_joins">> => PendingJoins};
_ ->
throw({error, <<"pending_joins_error">>})
end
end);
execute_method(<<"guild.move_member">>, #{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"moderator_id">> := ModeratorIdBin,
<<"channel_id">> := ChannelIdBin
} = Params) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ModeratorId = validation:snowflake_or_throw(<<"moderator_id">>, ModeratorIdBin),
{ok, ChannelId} = validation:validate_optional_snowflake(ChannelIdBin),
ConnectionId = maps:get(<<"connection_id">>, Params, null),
logger:debug(
"Processing guild.move_member RPC",
#{
guild_id => GuildId,
user_id => UserId,
moderator_id => ModeratorId,
channel_id => ChannelId,
connection_id => ConnectionId
}
),
with_voice_server(GuildId, fun(VoicePid, GuildPid) ->
Request = #{
user_id => UserId,
moderator_id => ModeratorId,
channel_id => ChannelId,
connection_id => ConnectionId
},
handle_move_member_result(
gen_server:call(VoicePid, {move_member, Request}, ?GUILD_CALL_TIMEOUT),
GuildId,
ChannelId,
GuildPid
)
end);
execute_method(<<"guild.get_voice_state">>, #{
<<"guild_id">> := GuildIdBin, <<"user_id">> := UserIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
with_voice_server(GuildId, fun(VoicePid, _GuildPid) ->
Request = #{user_id => UserId},
case gen_server:call(VoicePid, {get_voice_state, Request}, ?GUILD_CALL_TIMEOUT) of
#{voice_state := null} -> #{<<"voice_state">> => null};
#{voice_state := VoiceState} -> #{<<"voice_state">> => VoiceState};
_ -> throw({error, <<"voice_state_error">>})
end
end);
execute_method(<<"guild.switch_voice_region">>, #{
<<"guild_id">> := GuildIdBin, <<"channel_id">> := ChannelIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
with_voice_server(GuildId, fun(VoicePid, GuildPid) ->
Request = #{channel_id => ChannelId},
case gen_server:call(VoicePid, {switch_voice_region, Request}, ?GUILD_CALL_TIMEOUT) of
#{success := true} ->
spawn(fun() -> guild_voice:switch_voice_region(GuildId, ChannelId, GuildPid) end),
#{<<"success">> => true};
#{error := Error} ->
throw({error, normalize_voice_rpc_error(Error)})
end
end);
execute_method(<<"guild.get_category_channel_count">>, #{
<<"guild_id">> := GuildIdBin, <<"category_id">> := CategoryIdBin
}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
CategoryId = validation:snowflake_or_throw(<<"category_id">>, CategoryIdBin),
with_guild(GuildId, fun(Pid) ->
Request = #{category_id => CategoryId},
case gen_server:call(Pid, {get_category_channel_count, Request}, ?GUILD_CALL_TIMEOUT) of
#{count := Count} -> #{<<"count">> => Count};
_ -> throw({error, <<"category_channel_count_error">>})
end
end);
execute_method(<<"guild.get_channel_count">>, #{<<"guild_id">> := GuildIdBin}) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
with_guild(GuildId, fun(Pid) ->
case gen_server:call(Pid, {get_channel_count}, ?GUILD_CALL_TIMEOUT) of
#{count := Count} -> #{<<"count">> => Count};
_ -> throw({error, <<"channel_count_error">>})
end
end);
execute_method(<<"guild.get_online_counts_batch">>, #{<<"guild_ids">> := GuildIdsBin}) ->
GuildIds = validation:snowflake_list_or_throw(<<"guild_ids">>, GuildIdsBin),
UniqueGuildIds = lists:usort(GuildIds),
validate_batch_size(length(UniqueGuildIds)),
Results = process_batch(
UniqueGuildIds,
fun(GuildId) -> fetch_online_count_entry(GuildId) end,
?BATCH_TIMEOUT_MS
),
OnlineCounts = [Result || Result <- Results, is_map(Result)],
#{<<"online_counts">> => OnlineCounts};
execute_method(<<"guild.batch_voice_state_update">>, #{<<"updates">> := UpdatesBin}) ->
BatchSize = length(UpdatesBin),
StartTime = erlang:monotonic_time(millisecond),
validate_batch_size(BatchSize),
Updates = lists:map(fun parse_voice_update/1, UpdatesBin),
Results = process_batch(Updates, fun process_voice_update/1, ?BATCH_TIMEOUT_MS),
Duration = erlang:monotonic_time(millisecond) - StartTime,
gateway_metrics_collector:record_rpc_latency(Duration),
#{<<"results">> => Results}.
-spec fetch_online_count_entry(integer()) -> map() | undefined.
fetch_online_count_entry(GuildId) ->
case guild_counts_cache:get(GuildId) of
{ok, MemberCount, OnlineCount} ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"member_count">> => MemberCount,
<<"online_count">> => OnlineCount
};
miss ->
fetch_online_count_entry_from_process(GuildId)
end.
-spec fetch_online_count_entry_from_process(integer()) -> map() | undefined.
fetch_online_count_entry_from_process(GuildId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
case gen_server:call(Pid, {get_counts}, ?GUILD_CALL_TIMEOUT) of
#{member_count := MemberCount, presence_count := PresenceCount} ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"member_count">> => MemberCount,
<<"online_count">> => PresenceCount
};
_ ->
undefined
end;
_ ->
undefined
end.
-spec with_guild(integer(), fun((pid()) -> T)) -> T when T :: term().
with_guild(GuildId, Fun) ->
with_guild(GuildId, Fun, <<"guild_not_found">>).
-spec with_guild(integer(), fun((pid()) -> T), binary()) -> T when T :: term().
with_guild(GuildId, Fun, NotFoundError) ->
case get_guild_pid(GuildId) of
{ok, Pid} -> Fun(Pid);
_ -> throw({error, NotFoundError})
end.
-spec with_voice_server(integer(), fun((pid(), pid()) -> T)) -> T when T :: term().
with_voice_server(GuildId, Fun) ->
case get_guild_pid(GuildId) of
{ok, GuildPid} ->
VoicePid = resolve_voice_pid(GuildId, GuildPid),
Fun(VoicePid, GuildPid);
_ ->
throw({error, <<"guild_not_found">>})
end.
-spec resolve_voice_pid(integer(), pid()) -> pid().
resolve_voice_pid(GuildId, FallbackGuildPid) ->
case guild_voice_server:lookup(GuildId) of
{ok, VoicePid} -> VoicePid;
{error, not_found} -> FallbackGuildPid
end.
-spec get_guild_pid(integer()) -> {ok, pid()} | error.
get_guild_pid(GuildId) ->
case lookup_guild_pid_from_cache(GuildId) of
{ok, Pid} ->
{ok, Pid};
not_found ->
lookup_guild_pid_from_manager(GuildId)
end.
-spec lookup_guild_pid_from_cache(integer()) -> {ok, pid()} | not_found.
lookup_guild_pid_from_cache(GuildId) ->
case catch ets:lookup(guild_pid_cache, GuildId) of
[{GuildId, Pid}] when is_pid(Pid) ->
case erlang:is_process_alive(Pid) of
true ->
{ok, Pid};
false ->
_ = ets:delete(guild_pid_cache, GuildId),
not_found
end;
_ ->
not_found
end.
-spec lookup_guild_pid_from_manager(integer()) -> {ok, pid()} | error.
lookup_guild_pid_from_manager(GuildId) ->
case gen_server:call(guild_manager, {start_or_lookup, GuildId}, ?GUILD_LOOKUP_TIMEOUT) of
{ok, Pid} when is_pid(Pid) ->
{ok, Pid};
_ ->
error
end.
-spec fetch_user_permissions_entry(integer(), integer(), integer() | undefined) -> map() | undefined.
fetch_user_permissions_entry(GuildId, UserId, ChannelId) ->
case get_permissions_cached_or_rpc(GuildId, UserId, ChannelId) of
{ok, Permissions} ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"permissions">> => integer_to_binary(Permissions)
};
error ->
undefined
end.
-spec get_permissions_cached_or_rpc(integer(), integer(), integer() | undefined) ->
{ok, integer()} | error.
get_permissions_cached_or_rpc(GuildId, UserId, ChannelId) ->
case guild_permission_cache:get_permissions(GuildId, UserId, ChannelId) of
{ok, Permissions} ->
{ok, Permissions};
{error, not_found} ->
get_permissions_via_rpc(GuildId, UserId, ChannelId)
end.
-spec get_permissions_via_rpc(integer(), integer(), integer() | undefined) -> {ok, integer()} | error.
get_permissions_via_rpc(GuildId, UserId, ChannelId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
Request = #{user_id => UserId, channel_id => ChannelId},
case gen_server:call(Pid, {get_user_permissions, Request}, ?GUILD_CALL_TIMEOUT) of
#{permissions := Permissions} -> {ok, Permissions};
_ -> error
end;
error ->
error
end.
-spec get_members_with_role_cached_or_rpc(integer(), integer()) -> {ok, [integer()]} | error.
get_members_with_role_cached_or_rpc(GuildId, RoleId) ->
case guild_permission_cache:get_snapshot(GuildId) of
{ok, Snapshot} ->
{ok, get_members_with_role_from_snapshot(RoleId, Snapshot)};
{error, not_found} ->
get_members_with_role_via_rpc(GuildId, RoleId)
end.
-spec get_members_with_role_from_snapshot(integer(), map()) -> [integer()].
get_members_with_role_from_snapshot(RoleId, Snapshot) ->
Data = maps:get(data, Snapshot, #{}),
MemberRoleIndex = guild_data_index:member_role_index(Data),
lists:sort(maps:keys(maps:get(RoleId, MemberRoleIndex, #{}))).
-spec get_members_with_role_via_rpc(integer(), integer()) -> {ok, [integer()]} | error.
get_members_with_role_via_rpc(GuildId, RoleId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
Request = #{role_id => RoleId},
case gen_server:call(Pid, {get_members_with_role, Request}, ?GUILD_CALL_TIMEOUT) of
#{user_ids := UserIds} ->
{ok, UserIds};
_ ->
error
end;
error ->
error
end.
-spec get_viewable_channels_cached_or_rpc(integer(), integer()) -> {ok, [integer()]} | error.
get_viewable_channels_cached_or_rpc(GuildId, UserId) ->
case guild_permission_cache:get_snapshot(GuildId) of
{ok, Snapshot} ->
{ok, guild_visibility:get_user_viewable_channels(UserId, Snapshot)};
{error, not_found} ->
get_viewable_channels_via_rpc(GuildId, UserId)
end.
-spec get_viewable_channels_via_rpc(integer(), integer()) -> {ok, [integer()]} | error.
get_viewable_channels_via_rpc(GuildId, UserId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {get_viewable_channels, Request}, ?GUILD_CALL_TIMEOUT) of
#{channel_ids := ChannelIds} ->
{ok, ChannelIds};
_ ->
error
end;
error ->
error
end.
-spec get_has_member_cached_or_rpc(integer(), integer()) -> {ok, boolean()} | error.
get_has_member_cached_or_rpc(GuildId, UserId) ->
case guild_permission_cache:has_member(GuildId, UserId) of
{ok, HasMember} ->
{ok, HasMember};
{error, not_found} ->
get_has_member_via_rpc(GuildId, UserId)
end.
-spec get_has_member_via_rpc(integer(), integer()) -> {ok, boolean()} | error.
get_has_member_via_rpc(GuildId, UserId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {has_member, Request}, ?GUILD_CALL_TIMEOUT) of
#{has_member := HasMember} when is_boolean(HasMember) ->
{ok, HasMember};
_ ->
error
end;
error ->
error
end.
-spec get_member_cached_or_rpc(integer(), integer()) -> {ok, map() | undefined} | error.
get_member_cached_or_rpc(GuildId, UserId) ->
case guild_permission_cache:get_member(GuildId, UserId) of
{ok, MemberOrUndefined} ->
{ok, MemberOrUndefined};
{error, not_found} ->
get_member_via_rpc(GuildId, UserId)
end.
-spec get_member_via_rpc(integer(), integer()) -> {ok, map() | undefined} | error.
get_member_via_rpc(GuildId, UserId) ->
case get_guild_pid(GuildId) of
{ok, Pid} ->
Request = #{user_id => UserId},
case gen_server:call(Pid, {get_guild_member, Request}, ?GUILD_CALL_TIMEOUT) of
#{success := true, member_data := MemberData} ->
{ok, MemberData};
#{success := false} ->
{ok, undefined};
_ ->
error
end;
error ->
error
end.
-spec parse_channel_id(binary()) -> integer() | undefined.
parse_channel_id(<<"0">>) -> undefined;
parse_channel_id(ChannelIdBin) -> validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin).
-spec parse_optional_snowflake(term(), binary()) -> integer() | undefined.
parse_optional_snowflake(undefined, _) -> undefined;
parse_optional_snowflake(Value, Field) -> validation:snowflake_or_throw(Field, Value).
-spec build_disconnect_request(integer(), integer(), term()) -> map().
build_disconnect_request(UserId, ExpectedChannelId, undefined) ->
#{user_id => UserId, expected_channel_id => ExpectedChannelId};
build_disconnect_request(UserId, ExpectedChannelId, ConnId) ->
#{user_id => UserId, expected_channel_id => ExpectedChannelId, connection_id => ConnId}.
-spec handle_move_member_result(map(), integer(), integer() | null, pid()) -> map().
handle_move_member_result(
#{success := true, needs_token := true, session_data := SessionData, connections_to_move := _},
GuildId,
ChannelId,
Pid
) when ChannelId =/= null ->
spawn(fun() ->
guild_voice:send_voice_server_updates_for_move(GuildId, ChannelId, SessionData, Pid)
end),
#{<<"success">> => true};
handle_move_member_result(#{success := true, user_id := DisconnectedUserId}, _, null, Pid) ->
spawn(fun() -> guild_voice:cleanup_virtual_access_on_disconnect(DisconnectedUserId, Pid) end),
#{<<"success">> => true};
handle_move_member_result(#{success := true}, _, _, _) ->
#{<<"success">> => true};
handle_move_member_result({error, _Category, ErrorAtom}, _, _, _) ->
#{<<"success">> => false, <<"error">> => normalize_voice_rpc_error(ErrorAtom)};
handle_move_member_result(#{error := Error}, _, _, _) ->
#{<<"success">> => false, <<"error">> => normalize_voice_rpc_error(Error)};
handle_move_member_result(_, _, _, _) ->
#{<<"success">> => false, <<"error">> => <<"move_member_error">>}.
-spec normalize_voice_rpc_error(term()) -> binary().
normalize_voice_rpc_error(voice_user_not_in_voice) -> <<"user_not_in_voice">>;
normalize_voice_rpc_error(voice_channel_not_found) -> <<"channel_not_found">>;
normalize_voice_rpc_error(voice_channel_not_voice) -> <<"channel_not_voice">>;
normalize_voice_rpc_error(voice_moderator_missing_connect) -> <<"moderator_missing_connect">>;
normalize_voice_rpc_error(voice_permission_denied) -> <<"target_missing_connect">>;
normalize_voice_rpc_error(voice_connection_not_found) -> <<"connection_not_found">>;
normalize_voice_rpc_error(voice_missing_connection_id) -> <<"connection_not_found">>;
normalize_voice_rpc_error(Error) when is_binary(Error) ->
normalize_voice_rpc_error_binary(Error);
normalize_voice_rpc_error(Error) when is_atom(Error) ->
error_term_to_binary(Error);
normalize_voice_rpc_error(_Error) ->
<<"move_member_error">>.
-spec normalize_voice_rpc_error_binary(binary()) -> binary().
normalize_voice_rpc_error_binary(<<"voice_user_not_in_voice">>) -> <<"user_not_in_voice">>;
normalize_voice_rpc_error_binary(<<"voice_channel_not_found">>) -> <<"channel_not_found">>;
normalize_voice_rpc_error_binary(<<"voice_channel_not_voice">>) -> <<"channel_not_voice">>;
normalize_voice_rpc_error_binary(<<"voice_moderator_missing_connect">>) ->
<<"moderator_missing_connect">>;
normalize_voice_rpc_error_binary(<<"voice_permission_denied">>) -> <<"target_missing_connect">>;
normalize_voice_rpc_error_binary(<<"voice_connection_not_found">>) -> <<"connection_not_found">>;
normalize_voice_rpc_error_binary(<<"voice_missing_connection_id">>) -> <<"connection_not_found">>;
normalize_voice_rpc_error_binary(Error) -> Error.
-spec error_term_to_binary(term()) -> binary().
error_term_to_binary(Value) when is_binary(Value) ->
Value;
error_term_to_binary(Value) when is_atom(Value) ->
atom_to_binary(Value, utf8);
error_term_to_binary(Value) when is_list(Value) ->
try
unicode:characters_to_binary(Value)
catch
_:_ ->
<<"invalid_error">>
end;
error_term_to_binary(Value) ->
try
unicode:characters_to_binary(io_lib:format("~p", [Value]))
catch
_:_ ->
<<"invalid_error">>
end.
-spec parse_voice_update(map()) -> {integer(), integer(), boolean(), boolean(), term()}.
parse_voice_update(
#{
<<"guild_id">> := GuildIdBin,
<<"user_id">> := UserIdBin,
<<"mute">> := Mute,
<<"deaf">> := Deaf
} = Update
) ->
GuildId = validation:snowflake_or_throw(<<"guild_id">>, GuildIdBin),
UserId = validation:snowflake_or_throw(<<"user_id">>, UserIdBin),
ConnectionId = maps:get(<<"connection_id">>, Update, null),
{GuildId, UserId, Mute, Deaf, ConnectionId}.
-spec process_voice_update({integer(), integer(), boolean(), boolean(), term()}) -> map().
process_voice_update({GuildId, UserId, Mute, Deaf, ConnectionId}) ->
case gen_server:call(guild_manager, {start_or_lookup, GuildId}, ?GUILD_LOOKUP_TIMEOUT) of
{ok, GuildPid} ->
VoicePid = resolve_voice_pid(GuildId, GuildPid),
Request = #{
user_id => UserId, mute => Mute, deaf => Deaf, connection_id => ConnectionId
},
case gen_server:call(VoicePid, {update_member_voice, Request}, ?GUILD_CALL_TIMEOUT) of
#{success := true} ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"user_id">> => integer_to_binary(UserId),
<<"success">> => true
};
#{error := Error} ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"user_id">> => integer_to_binary(UserId),
<<"success">> => false,
<<"error">> => Error
}
end;
_ ->
#{
<<"guild_id">> => integer_to_binary(GuildId),
<<"user_id">> => integer_to_binary(UserId),
<<"success">> => false,
<<"error">> => <<"guild_not_found">>
}
end.
-spec validate_batch_size(non_neg_integer()) -> ok.
validate_batch_size(Size) when Size > ?MAX_BATCH_SIZE ->
throw(
{error, <<"Batch size exceeds maximum of ", (integer_to_binary(?MAX_BATCH_SIZE))/binary>>}
);
validate_batch_size(_) ->
ok.
-spec process_batch([T], fun((T) -> R), pos_integer()) -> [R] when T :: term(), R :: term().
process_batch(Items, HandlerFun, Timeout) ->
Parent = self(),
Ref = make_ref(),
Workers = [
spawn_monitor(fun() ->
try
Parent ! {Ref, {ok, HandlerFun(Item)}}
catch
_:_ -> Parent ! {Ref, error}
end
end)
|| Item <- Items
],
collect_results(Ref, length(Workers), Timeout, []).
-spec collect_results(reference(), non_neg_integer(), non_neg_integer(), [T]) -> [T] when
T :: term().
collect_results(_, 0, _, Acc) ->
lists:reverse(Acc);
collect_results(Ref, Remaining, Timeout, Acc) when Timeout > 0 ->
receive
{Ref, {ok, Result}} ->
collect_results(Ref, Remaining - 1, Timeout, [Result | Acc]);
{Ref, error} ->
collect_results(Ref, Remaining - 1, Timeout, Acc);
{'DOWN', _, process, _, _} ->
collect_results(Ref, Remaining, Timeout, Acc)
after Timeout ->
lists:reverse(Acc)
end;
collect_results(_, _, _, Acc) ->
lists:reverse(Acc).
-spec clamp_limit(term()) -> pos_integer().
clamp_limit(Value) ->
case type_conv:to_integer(Value) of
undefined -> 1;
N when N < 1 -> 1;
N when N > 1000 -> 1000;
N -> N
end.
-ifdef(TEST).
clamp_limit_test() ->
?assertEqual(1, clamp_limit(undefined)),
?assertEqual(1, clamp_limit(0)),
?assertEqual(1, clamp_limit(-5)),
?assertEqual(50, clamp_limit(50)),
?assertEqual(1000, clamp_limit(2000)).
parse_channel_id_test() ->
?assertEqual(undefined, parse_channel_id(<<"0">>)).
validate_batch_size_test() ->
?assertEqual(ok, validate_batch_size(50)),
?assertEqual(ok, validate_batch_size(100)),
?assertThrow({error, _}, validate_batch_size(101)).
get_permissions_cached_or_rpc_prefers_cache_test() ->
GuildId = 12345,
UserId = 500,
ViewPermission = constants:view_channel_permission(),
Data = #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [
#{<<"id">> => integer_to_binary(GuildId), <<"permissions">> => integer_to_binary(ViewPermission)}
],
<<"members">> => #{
UserId => #{<<"user">> => #{<<"id">> => integer_to_binary(UserId)}, <<"roles">> => []}
},
<<"channels">> => [#{<<"id">> => <<"700">>, <<"permission_overwrites">> => []}]
},
ok = guild_permission_cache:put_data(GuildId, Data),
try
?assertMatch({ok, _}, get_permissions_cached_or_rpc(GuildId, UserId, 700))
after
ok = guild_permission_cache:delete(GuildId)
end.
get_members_with_role_cached_or_rpc_prefers_cache_test() ->
GuildId = 12346,
UserId = 501,
RoleId = 9999,
Data = #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [
#{<<"id">> => integer_to_binary(GuildId), <<"permissions">> => <<"0">>},
#{<<"id">> => integer_to_binary(RoleId), <<"permissions">> => <<"0">>}
],
<<"members">> => #{
UserId => #{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => [integer_to_binary(RoleId)]
}
},
<<"channels">> => []
},
ok = guild_permission_cache:put_data(GuildId, Data),
try
?assertEqual({ok, [UserId]}, get_members_with_role_cached_or_rpc(GuildId, RoleId))
after
ok = guild_permission_cache:delete(GuildId)
end.
get_viewable_channels_cached_or_rpc_prefers_cache_test() ->
GuildId = 12347,
UserId = 7001,
ChannelId = 321,
ViewPerm = constants:view_channel_permission(),
Data = #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [
#{
<<"id">> => integer_to_binary(GuildId),
<<"permissions">> => integer_to_binary(ViewPerm)
}
],
<<"members">> => #{
UserId => #{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => []
}
},
<<"channels">> => [#{<<"id">> => integer_to_binary(ChannelId), <<"permission_overwrites">> => []}]
},
ok = guild_permission_cache:put_data(GuildId, Data),
try
?assertEqual({ok, [ChannelId]}, get_viewable_channels_cached_or_rpc(GuildId, UserId))
after
ok = guild_permission_cache:delete(GuildId)
end.
get_has_member_cached_or_rpc_prefers_cache_test() ->
GuildId = 12348,
UserId = 502,
Data = #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [],
<<"members">> => #{
UserId => #{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => []
}
},
<<"channels">> => []
},
ok = guild_permission_cache:put_data(GuildId, Data),
try
?assertEqual({ok, true}, get_has_member_cached_or_rpc(GuildId, UserId)),
?assertEqual({ok, false}, get_has_member_cached_or_rpc(GuildId, 99999))
after
ok = guild_permission_cache:delete(GuildId)
end.
get_member_cached_or_rpc_prefers_cache_test() ->
GuildId = 12349,
UserId = 503,
Data = #{
<<"guild">> => #{<<"owner_id">> => <<"999">>},
<<"roles">> => [],
<<"members">> => #{
UserId => #{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"roles">> => [],
<<"nick">> => <<"CacheNick">>
}
},
<<"channels">> => []
},
ok = guild_permission_cache:put_data(GuildId, Data),
try
{ok, MemberData} = get_member_cached_or_rpc(GuildId, UserId),
?assertEqual(<<"CacheNick">>, maps:get(<<"nick">>, MemberData)),
?assertEqual({ok, undefined}, get_member_cached_or_rpc(GuildId, 99999))
after
ok = guild_permission_cache:delete(GuildId)
end.
-endif.