initial commit
This commit is contained in:
699
fluxer_gateway/src/guild/voice/guild_voice_connection.erl
Normal file
699
fluxer_gateway/src/guild/voice/guild_voice_connection.erl
Normal file
@@ -0,0 +1,699 @@
|
||||
%% 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_voice_connection).
|
||||
|
||||
-include_lib("fluxer_gateway/include/voice_state.hrl").
|
||||
|
||||
-export([voice_state_update/2]).
|
||||
-export([confirm_voice_connection_from_livekit/2]).
|
||||
-export([request_voice_token/4]).
|
||||
|
||||
-type guild_state() :: map().
|
||||
-type voice_state() :: map().
|
||||
-type voice_state_map() :: #{binary() => voice_state()}.
|
||||
-type pending_voice_connections() :: #{binary() => map()}.
|
||||
-type context() :: #{
|
||||
user_id := integer() | undefined,
|
||||
channel_id := integer() | null | undefined,
|
||||
session_id := term(),
|
||||
connection_id := binary() | undefined,
|
||||
raw_connection_id := term(),
|
||||
self_mute := boolean(),
|
||||
self_deaf := boolean(),
|
||||
self_video := boolean(),
|
||||
self_stream := boolean(),
|
||||
is_mobile := boolean(),
|
||||
viewer_stream_key := term()
|
||||
}.
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-endif.
|
||||
|
||||
-ifdef(TEST).
|
||||
-define(LOG_INVALID_GUILD_ID(_Value), ok).
|
||||
-else.
|
||||
-define(LOG_INVALID_GUILD_ID(Value),
|
||||
logger:warning(
|
||||
"[guild_voice_connection] Invalid guild_id value: ~p", [Value]
|
||||
)
|
||||
).
|
||||
-endif.
|
||||
|
||||
voice_state_update(Request, State) ->
|
||||
Context = build_context(Request),
|
||||
case maps:get(user_id, Context) of
|
||||
undefined ->
|
||||
gateway_errors:error(voice_invalid_user_id);
|
||||
UserId ->
|
||||
logger:debug(
|
||||
"[guild_voice_connection] Processing voice state update: UserId=~p, ChannelId=~p, "
|
||||
"ConnectionId=~p",
|
||||
[UserId, maps:get(channel_id, Context), maps:get(connection_id, Context)]
|
||||
),
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
case guild_voice_member:find_member_by_user_id(UserId, State) of
|
||||
undefined ->
|
||||
logger:warning("[guild_voice_connection] Member not found for UserId: ~p", [
|
||||
UserId
|
||||
]),
|
||||
gateway_errors:error(voice_member_not_found);
|
||||
Member ->
|
||||
handle_member_voice(Context, Member, VoiceStates, State)
|
||||
end
|
||||
end.
|
||||
|
||||
-spec handle_member_voice(context(), map(), voice_state_map(), guild_state()) ->
|
||||
{reply, map(), guild_state()} | {error, atom(), binary()}.
|
||||
handle_member_voice(Context, Member, VoiceStates, State) ->
|
||||
case maps:get(channel_id, Context) of
|
||||
undefined ->
|
||||
gateway_errors:error(voice_invalid_channel_id);
|
||||
null ->
|
||||
handle_disconnect(Context, VoiceStates, State);
|
||||
ChannelIdValue ->
|
||||
handle_voice_connect_or_update(Context, ChannelIdValue, Member, VoiceStates, State)
|
||||
end.
|
||||
|
||||
handle_disconnect(Context, VoiceStates, State) ->
|
||||
guild_voice_disconnect:handle_voice_disconnect(
|
||||
maps:get(raw_connection_id, Context),
|
||||
maps:get(session_id, Context),
|
||||
maps:get(user_id, Context),
|
||||
VoiceStates,
|
||||
State
|
||||
).
|
||||
|
||||
handle_voice_connect_or_update(Context, ChannelIdValue, Member, VoiceStates, State) ->
|
||||
ConnectionId = maps:get(connection_id, Context),
|
||||
Channel = guild_voice_member:find_channel_by_id(ChannelIdValue, State),
|
||||
|
||||
case Channel of
|
||||
undefined ->
|
||||
logger:warning("[guild_voice_connection] Channel not found: ~p", [ChannelIdValue]),
|
||||
gateway_errors:error(voice_channel_not_found);
|
||||
_ ->
|
||||
case ConnectionId of
|
||||
undefined ->
|
||||
handle_new_connection(Context, Member, Channel, VoiceStates, State);
|
||||
_ ->
|
||||
handle_update_connection(
|
||||
Context,
|
||||
ChannelIdValue,
|
||||
Member,
|
||||
Channel,
|
||||
VoiceStates,
|
||||
State
|
||||
)
|
||||
end
|
||||
end.
|
||||
|
||||
handle_update_connection(Context, ChannelIdValue, Member, Channel, VoiceStates, State) ->
|
||||
ConnectionId = maps:get(connection_id, Context),
|
||||
|
||||
case maps:get(ConnectionId, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
gateway_errors:error(voice_connection_not_found);
|
||||
ExistingVoiceState ->
|
||||
ExistingChannelIdBin = maps:get(<<"channel_id">>, ExistingVoiceState, null),
|
||||
NewChannelIdBin = integer_to_binary(ChannelIdValue),
|
||||
IsChannelChange = ExistingChannelIdBin =/= NewChannelIdBin,
|
||||
UserId = maps:get(user_id, Context),
|
||||
GuildId = map_utils:get_integer(State, id, undefined),
|
||||
ViewerKeyResult =
|
||||
resolve_viewer_stream_key(
|
||||
Context, GuildId, ChannelIdValue, VoiceStates, ExistingVoiceState
|
||||
),
|
||||
|
||||
PermCheck =
|
||||
case IsChannelChange of
|
||||
true ->
|
||||
guild_voice_permissions:check_voice_permissions_and_limits(
|
||||
UserId, ChannelIdValue, Channel, VoiceStates, State, false
|
||||
);
|
||||
false ->
|
||||
{ok, allowed}
|
||||
end,
|
||||
|
||||
case PermCheck of
|
||||
{error, _Category, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{ok, allowed} ->
|
||||
case ViewerKeyResult of
|
||||
{error, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{ok, ParsedViewerKey} ->
|
||||
Flags = voice_state_utils:voice_flags_from_context(Context),
|
||||
guild_voice_state:update_voice_state_data(
|
||||
ConnectionId,
|
||||
NewChannelIdBin,
|
||||
Flags,
|
||||
Member,
|
||||
ExistingVoiceState,
|
||||
VoiceStates,
|
||||
State,
|
||||
false,
|
||||
ParsedViewerKey
|
||||
)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
handle_new_connection(Context, Member, Channel, VoiceStates, State) ->
|
||||
UserId = maps:get(user_id, Context),
|
||||
ChannelIdValue = maps:get(channel_id, Context),
|
||||
GuildId = map_utils:get_integer(State, id, undefined),
|
||||
ViewerKeyResult = resolve_viewer_stream_key(Context, GuildId, ChannelIdValue, VoiceStates, #{}),
|
||||
|
||||
PermCheck = guild_voice_permissions:check_voice_permissions_and_limits(
|
||||
UserId, ChannelIdValue, Channel, VoiceStates, State, false
|
||||
),
|
||||
|
||||
case {PermCheck, ViewerKeyResult} of
|
||||
{{error, _Category, ErrorAtom}, _} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{{ok, allowed}, {error, ErrorAtom}} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{{ok, allowed}, {ok, ParsedViewerKey}} ->
|
||||
get_voice_token_and_create_state(Context, Member, ParsedViewerKey, State)
|
||||
end.
|
||||
|
||||
get_voice_token_and_create_state(Context, Member, ParsedViewerStreamKey, State) ->
|
||||
ChannelIdValue = maps:get(channel_id, Context),
|
||||
UserId = maps:get(user_id, Context),
|
||||
SessionId = maps:get(session_id, Context),
|
||||
|
||||
case resolve_guild_identity(State) of
|
||||
{error, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{ok, GuildId, GuildIdBin} ->
|
||||
logger:info(
|
||||
"[guild_voice_connection] Requesting voice token for GuildId=~p, ChannelId=~p, UserId=~p",
|
||||
[GuildId, ChannelIdValue, UserId]
|
||||
),
|
||||
VoicePermissions = voice_utils:compute_voice_permissions(UserId, ChannelIdValue, State),
|
||||
logger:debug("[guild_voice_connection] Computed voice permissions: ~p", [
|
||||
VoicePermissions
|
||||
]),
|
||||
case request_voice_token(GuildId, ChannelIdValue, UserId, VoicePermissions) of
|
||||
{ok, TokenData} ->
|
||||
logger:debug("[guild_voice_connection] Voice token request succeeded"),
|
||||
Token = maps:get(token, TokenData),
|
||||
Endpoint = maps:get(endpoint, TokenData),
|
||||
ConnectionId = maps:get(connection_id, TokenData),
|
||||
|
||||
ChannelIdBin = integer_to_binary(ChannelIdValue),
|
||||
UserIdBin = integer_to_binary(UserId),
|
||||
ServerMute = maps:get(<<"mute">>, Member, false),
|
||||
ServerDeaf = maps:get(<<"deaf">>, Member, false),
|
||||
|
||||
Flags = voice_state_utils:voice_flags_from_context(Context),
|
||||
#voice_flags{
|
||||
self_mute = SelfMuteFlag,
|
||||
self_deaf = SelfDeafFlag,
|
||||
self_video = SelfVideoFlag,
|
||||
self_stream = SelfStreamFlag,
|
||||
is_mobile = IsMobileFlag
|
||||
} = Flags,
|
||||
SessionIdValue = maps:get(session_id, Context, undefined),
|
||||
SessionIdBin = normalize_session_id(SessionIdValue),
|
||||
|
||||
VoiceState0 = guild_voice_state:create_voice_state(
|
||||
GuildIdBin,
|
||||
ChannelIdBin,
|
||||
UserIdBin,
|
||||
ConnectionId,
|
||||
ServerMute,
|
||||
ServerDeaf,
|
||||
Flags,
|
||||
ParsedViewerStreamKey
|
||||
),
|
||||
VoiceState1 = maybe_attach_session_id(VoiceState0, SessionIdBin),
|
||||
VoiceState = maybe_attach_member(VoiceState1, Member),
|
||||
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
NewVoiceStates = maps:put(ConnectionId, VoiceState, VoiceStates),
|
||||
StateWithVoiceStates = maps:put(voice_states, NewVoiceStates, State),
|
||||
|
||||
guild_voice_broadcast:broadcast_voice_state_update(
|
||||
VoiceState, StateWithVoiceStates, ChannelIdBin
|
||||
),
|
||||
|
||||
PendingMetadata = #{
|
||||
user_id => UserId,
|
||||
guild_id => GuildId,
|
||||
channel_id => ChannelIdValue,
|
||||
session_id => SessionIdBin,
|
||||
self_mute => SelfMuteFlag,
|
||||
self_deaf => SelfDeafFlag,
|
||||
self_video => SelfVideoFlag,
|
||||
self_stream => SelfStreamFlag,
|
||||
is_mobile => IsMobileFlag,
|
||||
server_mute => ServerMute,
|
||||
server_deaf => ServerDeaf,
|
||||
member => Member,
|
||||
viewer_stream_key => ParsedViewerStreamKey
|
||||
},
|
||||
PendingConnections = pending_voice_connections(StateWithVoiceStates),
|
||||
NewPendingConnections = maps:put(
|
||||
ConnectionId,
|
||||
PendingMetadata,
|
||||
PendingConnections
|
||||
),
|
||||
NewState = maps:put(
|
||||
pending_voice_connections, NewPendingConnections, StateWithVoiceStates
|
||||
),
|
||||
|
||||
maybe_broadcast_voice_server_update(
|
||||
SessionId, GuildId, Token, Endpoint, ConnectionId, NewState
|
||||
),
|
||||
|
||||
{reply,
|
||||
#{
|
||||
success => true,
|
||||
token => Token,
|
||||
endpoint => Endpoint,
|
||||
connection_id => ConnectionId,
|
||||
voice_state => VoiceState
|
||||
},
|
||||
NewState};
|
||||
{error, Reason} ->
|
||||
logger:error("[guild_voice_connection] Failed to request voice token: ~p", [
|
||||
Reason
|
||||
]),
|
||||
{reply, gateway_errors:error(voice_token_failed), State}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec build_context(map()) -> context().
|
||||
build_context(Request0) ->
|
||||
Request = map_utils:ensure_map(Request0),
|
||||
RawConnectionId = maps:get(connection_id, Request, undefined),
|
||||
#{
|
||||
user_id => normalize_user_id(maps:get(user_id, Request, undefined)),
|
||||
channel_id => normalize_channel_id_value(maps:get(channel_id, Request, null)),
|
||||
session_id => maps:get(session_id, Request, undefined),
|
||||
connection_id => normalize_connection_id(RawConnectionId),
|
||||
raw_connection_id => RawConnectionId,
|
||||
self_mute => normalize_boolean(maps:get(self_mute, Request, false)),
|
||||
self_deaf => normalize_boolean(maps:get(self_deaf, Request, false)),
|
||||
self_video => normalize_boolean(maps:get(self_video, Request, false)),
|
||||
self_stream => normalize_boolean(maps:get(self_stream, Request, false)),
|
||||
is_mobile => normalize_boolean(maps:get(is_mobile, Request, false)),
|
||||
viewer_stream_key => maps:get(viewer_stream_key, Request, undefined)
|
||||
}.
|
||||
|
||||
normalize_connection_id(undefined) ->
|
||||
undefined;
|
||||
normalize_connection_id(null) ->
|
||||
undefined;
|
||||
normalize_connection_id(ConnectionId) ->
|
||||
ConnectionId.
|
||||
|
||||
-spec normalize_user_id(term()) -> integer() | undefined.
|
||||
normalize_user_id(Value) ->
|
||||
type_conv:to_integer(Value).
|
||||
|
||||
-spec normalize_channel_id_value(term()) -> integer() | null | undefined.
|
||||
normalize_channel_id_value(null) ->
|
||||
null;
|
||||
normalize_channel_id_value(Value) ->
|
||||
type_conv:to_integer(Value).
|
||||
|
||||
-spec normalize_boolean(term()) -> boolean().
|
||||
normalize_boolean(true) -> true;
|
||||
normalize_boolean(<<"true">>) -> true;
|
||||
normalize_boolean(false) -> false;
|
||||
normalize_boolean(<<"false">>) -> false;
|
||||
normalize_boolean(_) -> false.
|
||||
|
||||
maybe_attach_session_id(VoiceState, undefined) ->
|
||||
VoiceState;
|
||||
maybe_attach_session_id(VoiceState, SessionId) when is_binary(SessionId) ->
|
||||
maps:put(<<"session_id">>, SessionId, VoiceState).
|
||||
|
||||
maybe_attach_member(VoiceState, Member) when is_map(Member) ->
|
||||
case maps:size(Member) of
|
||||
0 -> VoiceState;
|
||||
_ -> maps:put(<<"member">>, Member, VoiceState)
|
||||
end.
|
||||
|
||||
normalize_session_id(undefined) ->
|
||||
undefined;
|
||||
normalize_session_id(null) ->
|
||||
undefined;
|
||||
normalize_session_id(SessionId) when is_binary(SessionId) ->
|
||||
SessionId;
|
||||
normalize_session_id(SessionId) when is_integer(SessionId) ->
|
||||
integer_to_binary(SessionId);
|
||||
normalize_session_id(SessionId) when is_list(SessionId) ->
|
||||
list_to_binary(SessionId);
|
||||
normalize_session_id(SessionId) ->
|
||||
try
|
||||
erlang:iolist_to_binary(SessionId)
|
||||
catch
|
||||
_:_ -> undefined
|
||||
end.
|
||||
|
||||
resolve_viewer_stream_key(Context, GuildId, ChannelIdValue, VoiceStates, ExistingVoiceState) ->
|
||||
RawKey = maps:get(viewer_stream_key, Context, undefined),
|
||||
case RawKey of
|
||||
undefined ->
|
||||
{ok, maps:get(<<"viewer_stream_key">>, ExistingVoiceState, null)};
|
||||
null ->
|
||||
{ok, null};
|
||||
_ when not is_binary(RawKey) ->
|
||||
{error, voice_invalid_state};
|
||||
_ ->
|
||||
case voice_state_utils:parse_stream_key(RawKey) of
|
||||
{error, _} ->
|
||||
{error, voice_invalid_state};
|
||||
{ok, #{
|
||||
scope := guild,
|
||||
guild_id := ParsedGuildId,
|
||||
channel_id := ParsedChannelId,
|
||||
connection_id := StreamConnId
|
||||
}} when
|
||||
is_integer(ChannelIdValue), ParsedChannelId =:= ChannelIdValue
|
||||
->
|
||||
GuildScopeCheck =
|
||||
case GuildId of
|
||||
undefined -> ok;
|
||||
ParsedGuildId -> ok;
|
||||
_ -> error
|
||||
end,
|
||||
case GuildScopeCheck of
|
||||
ok ->
|
||||
case maps:get(StreamConnId, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
{error, voice_connection_not_found};
|
||||
StreamVS ->
|
||||
case
|
||||
map_utils:get_integer(StreamVS, <<"channel_id">>, undefined)
|
||||
of
|
||||
ChannelIdValue -> {ok, RawKey};
|
||||
_ -> {error, voice_invalid_state}
|
||||
end
|
||||
end;
|
||||
error ->
|
||||
{error, voice_invalid_state}
|
||||
end;
|
||||
{ok, #{scope := dm, channel_id := ParsedChannelId}} when
|
||||
is_integer(ChannelIdValue), ParsedChannelId =:= ChannelIdValue
|
||||
->
|
||||
{ok, RawKey};
|
||||
_ ->
|
||||
{error, voice_invalid_state}
|
||||
end
|
||||
end.
|
||||
|
||||
resolve_voice_state_from_pending(ConnectionId, PendingData, State, VoiceStates) ->
|
||||
case maps:get(ConnectionId, VoiceStates, undefined) of
|
||||
VoiceState when is_map(VoiceState) ->
|
||||
VoiceState;
|
||||
_ ->
|
||||
case maps:get(voice_state, PendingData, undefined) of
|
||||
VoiceState when is_map(VoiceState) ->
|
||||
VoiceState;
|
||||
_ ->
|
||||
build_voice_state_from_pending(PendingData, ConnectionId, State)
|
||||
end
|
||||
end.
|
||||
|
||||
build_voice_state_from_pending(PendingData, ConnectionId, State) ->
|
||||
GuildIdState = map_utils:get_integer(State, id, undefined),
|
||||
GuildId0 = pending_get_integer(PendingData, guild_id),
|
||||
GuildId =
|
||||
case GuildId0 of
|
||||
undefined -> GuildIdState;
|
||||
_ -> GuildId0
|
||||
end,
|
||||
ChannelId = pending_get_integer(PendingData, channel_id),
|
||||
UserId = pending_get_integer(PendingData, user_id),
|
||||
case {GuildId, ChannelId, UserId} of
|
||||
{undefined, _, _} ->
|
||||
undefined;
|
||||
{_, undefined, _} ->
|
||||
undefined;
|
||||
{_, _, undefined} ->
|
||||
undefined;
|
||||
{GId, ChId, UId} ->
|
||||
GuildIdBin = integer_to_binary(GId),
|
||||
ChannelIdBin = integer_to_binary(ChId),
|
||||
UserIdBin = integer_to_binary(UId),
|
||||
Flags = #voice_flags{
|
||||
self_mute = pending_get_boolean(PendingData, self_mute),
|
||||
self_deaf = pending_get_boolean(PendingData, self_deaf),
|
||||
self_video = pending_get_boolean(PendingData, self_video),
|
||||
self_stream = pending_get_boolean(PendingData, self_stream),
|
||||
is_mobile = pending_get_boolean(PendingData, is_mobile)
|
||||
},
|
||||
ServerMute = pending_get_boolean(PendingData, server_mute),
|
||||
ServerDeaf = pending_get_boolean(PendingData, server_deaf),
|
||||
ViewerStreamKey = pending_get_value(PendingData, viewer_stream_key),
|
||||
VoiceState0 = guild_voice_state:create_voice_state(
|
||||
GuildIdBin,
|
||||
ChannelIdBin,
|
||||
UserIdBin,
|
||||
ConnectionId,
|
||||
ServerMute,
|
||||
ServerDeaf,
|
||||
Flags,
|
||||
ViewerStreamKey
|
||||
),
|
||||
SessionId = pending_get_binary(PendingData, session_id),
|
||||
Member = pending_get_map(PendingData, member),
|
||||
VoiceState1 = maybe_attach_session_id(VoiceState0, SessionId),
|
||||
maybe_attach_member(VoiceState1, Member)
|
||||
end.
|
||||
|
||||
pending_get_value(PendingData, Key) ->
|
||||
case maps:get(Key, PendingData, undefined) of
|
||||
undefined ->
|
||||
BinKey = atom_to_binary(Key, utf8),
|
||||
maps:get(BinKey, PendingData, undefined);
|
||||
Value ->
|
||||
Value
|
||||
end.
|
||||
|
||||
pending_get_integer(PendingData, Key) ->
|
||||
case pending_get_value(PendingData, Key) of
|
||||
undefined -> undefined;
|
||||
Value -> type_conv:to_integer(Value)
|
||||
end.
|
||||
|
||||
pending_get_boolean(PendingData, Key) ->
|
||||
case pending_get_value(PendingData, Key) of
|
||||
true -> true;
|
||||
false -> false;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
pending_get_binary(PendingData, Key) ->
|
||||
case pending_get_value(PendingData, Key) of
|
||||
undefined -> undefined;
|
||||
Value -> type_conv:to_binary(Value)
|
||||
end.
|
||||
|
||||
pending_get_map(PendingData, Key) ->
|
||||
case pending_get_value(PendingData, Key) of
|
||||
Map when is_map(Map) -> Map;
|
||||
_ -> #{}
|
||||
end.
|
||||
|
||||
resolve_guild_identity(State) ->
|
||||
Data = guild_data(State),
|
||||
DataGuildIdBin = maps:get(<<"id">>, Data, undefined),
|
||||
StateGuildId = map_utils:get_integer(State, id, undefined),
|
||||
GuildMeta = map_utils:ensure_map(maps:get(<<"guild">>, Data, #{})),
|
||||
GuildMetaIdBin = maps:get(<<"id">>, GuildMeta, undefined),
|
||||
|
||||
resolve_guild_id_priority([
|
||||
{DataGuildIdBin, fun normalize_guild_id/1},
|
||||
{StateGuildId, fun normalize_guild_id/1},
|
||||
{GuildMetaIdBin, fun normalize_guild_id/1}
|
||||
]).
|
||||
|
||||
resolve_guild_id_priority([]) ->
|
||||
logger:error("[guild_voice_connection] Missing guild id in state"),
|
||||
{error, voice_guild_id_missing};
|
||||
resolve_guild_id_priority([{undefined, _} | Rest]) ->
|
||||
resolve_guild_id_priority(Rest);
|
||||
resolve_guild_id_priority([{Value, NormalizeFun} | _]) ->
|
||||
NormalizeFun(Value).
|
||||
|
||||
normalize_guild_id(Value) ->
|
||||
case type_conv:to_integer(Value) of
|
||||
undefined ->
|
||||
?LOG_INVALID_GUILD_ID(Value),
|
||||
{error, voice_invalid_guild_id};
|
||||
Int ->
|
||||
{ok, Int, guild_id_binary(Value, Int)}
|
||||
end.
|
||||
|
||||
guild_id_binary(Value, Int) ->
|
||||
case type_conv:to_binary(Value) of
|
||||
undefined -> integer_to_binary(Int);
|
||||
Bin -> Bin
|
||||
end.
|
||||
|
||||
maybe_broadcast_voice_server_update(undefined, _GuildId, _Token, _Endpoint, _ConnectionId, _State) ->
|
||||
ok;
|
||||
maybe_broadcast_voice_server_update(null, _GuildId, _Token, _Endpoint, _ConnectionId, _State) ->
|
||||
ok;
|
||||
maybe_broadcast_voice_server_update(SessionId, GuildId, Token, Endpoint, ConnectionId, State) ->
|
||||
guild_voice_broadcast:broadcast_voice_server_update_to_session(
|
||||
GuildId, SessionId, Token, Endpoint, ConnectionId, State
|
||||
).
|
||||
|
||||
guild_data(State) ->
|
||||
map_utils:ensure_map(maps:get(data, State, #{})).
|
||||
|
||||
confirm_voice_connection_from_livekit(Request, State) ->
|
||||
ConnectionId = maps:get(connection_id, Request, undefined),
|
||||
|
||||
logger:info(
|
||||
"[guild_voice_connection] confirm_voice_connection_from_livekit connection_id=~p pending_count=~p",
|
||||
[ConnectionId, maps:size(pending_voice_connections(State))]
|
||||
),
|
||||
|
||||
case ConnectionId of
|
||||
undefined ->
|
||||
gateway_errors:error(voice_missing_connection_id);
|
||||
_ ->
|
||||
PendingConnections = pending_voice_connections(State),
|
||||
|
||||
case maps:get(ConnectionId, PendingConnections, undefined) of
|
||||
undefined ->
|
||||
logger:warning(
|
||||
"[guild_voice_connection] confirm_voice_connection_from_livekit missing pending connection_id=~p",
|
||||
[ConnectionId]
|
||||
),
|
||||
gateway_errors:error(voice_connection_not_found);
|
||||
PendingData ->
|
||||
logger:info(
|
||||
"[guild_voice_connection] Found pending connection_id=~p for guild=~p",
|
||||
[ConnectionId, map_utils:get_integer(State, id, 0)]
|
||||
),
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
VoiceState = resolve_voice_state_from_pending(
|
||||
ConnectionId, PendingData, State, VoiceStates
|
||||
),
|
||||
|
||||
NewPendingConnections = maps:remove(ConnectionId, PendingConnections),
|
||||
StateWithoutPending = maps:put(
|
||||
pending_voice_connections, NewPendingConnections, State
|
||||
),
|
||||
|
||||
case VoiceState of
|
||||
undefined ->
|
||||
logger:warning(
|
||||
"[guild_voice_connection] Missing voice_state for confirmed connection ~p",
|
||||
[ConnectionId]
|
||||
),
|
||||
{reply, #{success => true}, StateWithoutPending};
|
||||
_ ->
|
||||
UpdatedVoiceStates = maps:put(ConnectionId, VoiceState, VoiceStates),
|
||||
StateWithVoiceStates = maps:put(
|
||||
voice_states, UpdatedVoiceStates, StateWithoutPending
|
||||
),
|
||||
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, null),
|
||||
UserId = maps:get(<<"user_id">>, VoiceState, <<"unknown">>),
|
||||
GuildId = maps:get(id, StateWithVoiceStates, 0),
|
||||
Sessions = maps:get(sessions, StateWithVoiceStates, #{}),
|
||||
logger:info(
|
||||
"[guild_voice_connection] confirm_voice_connection_from_livekit: "
|
||||
"guild_id=~p user_id=~p channel_id=~p connection_id=~p sessions_count=~p",
|
||||
[GuildId, UserId, ChannelIdBin, ConnectionId, maps:size(Sessions)]
|
||||
),
|
||||
guild_voice_broadcast:broadcast_voice_state_update(
|
||||
VoiceState, StateWithVoiceStates, ChannelIdBin
|
||||
),
|
||||
|
||||
{reply, #{success => true}, StateWithVoiceStates}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec request_voice_token(integer(), integer(), integer(), map()) ->
|
||||
{ok, map()} | {error, term()}.
|
||||
request_voice_token(GuildId, ChannelId, UserId, VoicePermissions) ->
|
||||
Req = voice_utils:build_voice_token_rpc_request(
|
||||
GuildId, ChannelId, UserId, null, null, null, VoicePermissions
|
||||
),
|
||||
case rpc_client:call(Req) of
|
||||
{ok, Data} ->
|
||||
{ok, #{
|
||||
token => maps:get(<<"token">>, Data),
|
||||
endpoint => maps:get(<<"endpoint">>, Data),
|
||||
connection_id => maps:get(<<"connectionId">>, Data)
|
||||
}};
|
||||
{error, Reason} ->
|
||||
logger:error("[guild_voice_connection] RPC request failed: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec pending_voice_connections(guild_state()) -> pending_voice_connections().
|
||||
pending_voice_connections(State) ->
|
||||
case maps:get(pending_voice_connections, State, undefined) of
|
||||
Map when is_map(Map) -> Map;
|
||||
_ -> #{}
|
||||
end.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
build_context_normalizes_fields_test() ->
|
||||
Request = #{
|
||||
user_id => <<"42">>,
|
||||
channel_id => <<"99">>,
|
||||
connection_id => <<"conn">>,
|
||||
self_mute => true,
|
||||
self_deaf => <<"nope">>,
|
||||
self_video => true,
|
||||
self_stream => false,
|
||||
is_mobile => <<"yes">>
|
||||
},
|
||||
Context = build_context(Request),
|
||||
?assertEqual(42, maps:get(user_id, Context)),
|
||||
?assertEqual(99, maps:get(channel_id, Context)),
|
||||
?assertEqual(<<"conn">>, maps:get(connection_id, Context)),
|
||||
?assertEqual(true, maps:get(self_mute, Context)),
|
||||
?assertEqual(false, maps:get(self_deaf, Context)),
|
||||
?assertEqual(true, maps:get(self_video, Context)),
|
||||
?assertEqual(false, maps:get(self_stream, Context)),
|
||||
?assertEqual(false, maps:get(is_mobile, Context)).
|
||||
|
||||
resolve_guild_identity_prefers_data_test() ->
|
||||
State = #{
|
||||
id => 7,
|
||||
data => #{
|
||||
<<"id">> => <<"555">>,
|
||||
<<"guild">> => #{<<"id">> => <<"111">>}
|
||||
}
|
||||
},
|
||||
?assertMatch({ok, 555, <<"555">>}, resolve_guild_identity(State)).
|
||||
|
||||
normalize_guild_id_invalid_test() ->
|
||||
?assertMatch({error, voice_invalid_guild_id}, normalize_guild_id(foo)).
|
||||
|
||||
voice_state_update_invalid_user_id_test() ->
|
||||
{error, validation_error, voice_invalid_user_id} =
|
||||
voice_state_update(#{channel_id => null}, #{}).
|
||||
|
||||
-endif.
|
||||
Reference in New Issue
Block a user