%% 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 . -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.