mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(voice): add opt-in peer-to-peer calls and country rollouts (#3367)
This commit is contained in:
1 parent
e5a568acca
commit
bef5e33eec
215 files changed
+15840
-2693
No files matched your search
@@ -25,6 +25,7 @@
|
||||
| {stop_ringing, [integer()]}
|
||||
| {join, integer(), map(), binary(), pid()}
|
||||
| {join, integer(), map(), binary(), pid(), binary() | undefined}
|
||||
| {join, integer(), map(), binary(), pid(), binary(), pos_integer()}
|
||||
| {confirm_connection, binary()}
|
||||
| {disconnect_user_if_in_channel, integer(), integer(), binary() | undefined}
|
||||
| {leave, binary()}
|
||||
@@ -33,7 +34,8 @@
|
||||
| {get_pending_connections}.
|
||||
-type cast_request() ::
|
||||
{join_async, integer(), map(), binary(), pid()}
|
||||
| {set_caller, map()}.
|
||||
| {set_caller, map()}
|
||||
| {voice_signal, binary(), binary(), map()}.
|
||||
-type info_message() ::
|
||||
{'DOWN', reference(), process, pid(), term()}
|
||||
| {ring_timeout, integer()}
|
||||
@@ -134,9 +136,11 @@ handle_call_request({ring_recipients, Recipients}, State) ->
|
||||
handle_call_request({stop_ringing, Recipients}, State) ->
|
||||
handle_stop_ringing(Recipients, State);
|
||||
handle_call_request({join, UserId, VS, SId, SPid}, State) ->
|
||||
handle_join_request(UserId, VS, SId, SPid, undefined, State);
|
||||
handle_join_request(UserId, VS, SId, SPid, undefined, undefined, State);
|
||||
handle_call_request({join, UserId, VS, SId, SPid, CId}, State) ->
|
||||
handle_join_request(UserId, VS, SId, SPid, CId, State);
|
||||
handle_join_request(UserId, VS, SId, SPid, CId, undefined, State);
|
||||
handle_call_request({join, UserId, VS, SId, SPid, CId, MaxParticipants}, State) ->
|
||||
handle_join_request(UserId, VS, SId, SPid, CId, MaxParticipants, State);
|
||||
handle_call_request({confirm_connection, ConnectionId}, State) ->
|
||||
handle_confirm_connection(ConnectionId, State);
|
||||
handle_call_request({disconnect_user_if_in_channel, UserId, ExpChId, CId}, State) ->
|
||||
@@ -151,14 +155,16 @@ handle_call_request({get_pending_connections}, State) ->
|
||||
handle_get_pending_connections(State).
|
||||
|
||||
-spec handle_join_request(
|
||||
integer(), map(), binary(), pid(), binary() | undefined, map()
|
||||
) -> {reply, ok, map()}.
|
||||
handle_join_request(UserId, VS, SId, SPid, CId, #{sessions := Sessions} = State) ->
|
||||
integer(), map(), binary(), pid(), binary() | undefined, pos_integer() | undefined, map()
|
||||
) -> {reply, ok | {error, atom()}, map()}.
|
||||
handle_join_request(
|
||||
UserId, VS, SId, SPid, CId, MaxParticipants, #{sessions := Sessions} = State
|
||||
) ->
|
||||
case maps:get(SId, Sessions, undefined) of
|
||||
{UserId, SPid, _Ref} ->
|
||||
{reply, ok, State};
|
||||
_ ->
|
||||
call_voice:handle_join_internal(UserId, VS, SId, SPid, CId, State)
|
||||
call_voice:handle_join_internal(UserId, VS, SId, SPid, CId, MaxParticipants, State)
|
||||
end.
|
||||
|
||||
-spec handle_cast(term(), map()) -> {noreply, map()}.
|
||||
@@ -168,6 +174,9 @@ handle_cast(Request, State) ->
|
||||
call_voice:handle_join_async(UserId, VoiceState, SessionId, SessionPid, State);
|
||||
{ok, {set_caller, Caller}} ->
|
||||
{noreply, call_state:put_caller(Caller, State)};
|
||||
{ok, {voice_signal, SessionId, To, Data}} ->
|
||||
ok = call_voice:relay_voice_signal(SessionId, To, Data, State),
|
||||
{noreply, State};
|
||||
error ->
|
||||
{noreply, State}
|
||||
end.
|
||||
@@ -269,6 +278,18 @@ decode_session_call_request(
|
||||
is_binary(ConnectionId)
|
||||
->
|
||||
{ok, {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId}};
|
||||
decode_session_call_request(
|
||||
{join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants}
|
||||
) when
|
||||
is_integer(UserId),
|
||||
is_map(VoiceState),
|
||||
is_binary(SessionId),
|
||||
is_pid(SessionPid),
|
||||
is_binary(ConnectionId),
|
||||
is_integer(MaxParticipants),
|
||||
MaxParticipants >= 1
|
||||
->
|
||||
{ok, {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants}};
|
||||
decode_session_call_request({confirm_connection, ConnectionId}) when is_binary(ConnectionId) ->
|
||||
{ok, {confirm_connection, ConnectionId}};
|
||||
decode_session_call_request(
|
||||
@@ -323,6 +344,10 @@ decode_cast_request({join_async, UserId, VoiceState, SessionId, SessionPid}) whe
|
||||
{ok, {join_async, UserId, VoiceState, SessionId, SessionPid}};
|
||||
decode_cast_request({set_caller, Caller}) when is_map(Caller) ->
|
||||
{ok, {set_caller, Caller}};
|
||||
decode_cast_request({voice_signal, SessionId, To, Data}) when
|
||||
is_binary(SessionId), is_binary(To), is_map(Data)
|
||||
->
|
||||
{ok, {voice_signal, SessionId, To, Data}};
|
||||
decode_cast_request(_) ->
|
||||
error.
|
||||
|
||||
@@ -354,9 +379,15 @@ build_get_state_reply(State) ->
|
||||
handle_update_region(NewRegion, State) ->
|
||||
OldRegion = maps:get(region, State, undefined),
|
||||
NewState = State#{region => NewRegion},
|
||||
UpdatedState = call_ringing:dispatch_call_update(NewState),
|
||||
call_voice:maybe_spawn_region_switch(OldRegion, NewRegion, UpdatedState),
|
||||
{reply, ok, UpdatedState}.
|
||||
case voice_p2p:channel_mode(maps:values(maps:get(voice_states, NewState))) of
|
||||
p2p ->
|
||||
ConvertedState = call_voice:convert_to_sfu([], NewState),
|
||||
{reply, ok, call_ringing:dispatch_call_update(ConvertedState)};
|
||||
_ ->
|
||||
UpdatedState = call_ringing:dispatch_call_update(NewState),
|
||||
call_voice:maybe_spawn_region_switch(OldRegion, NewRegion, UpdatedState),
|
||||
{reply, ok, UpdatedState}
|
||||
end.
|
||||
|
||||
-spec handle_ring_recipients([integer()], map()) -> {reply, ok, map()}.
|
||||
handle_ring_recipients(Recipients, State) ->
|
||||
@@ -398,8 +429,7 @@ handle_confirm_connection(ConnectionId, State) ->
|
||||
handle_update_voice_state(UserId, VoiceState, State) ->
|
||||
case maps:is_key(UserId, maps:get(voice_states, State)) of
|
||||
true ->
|
||||
NewVS = (maps:get(voice_states, State))#{UserId => VoiceState},
|
||||
NewState = State#{voice_states => NewVS},
|
||||
NewState = call_voice:update_voice_state(UserId, VoiceState, State),
|
||||
CountedState = call_state:sync_voice_state_count_diff(State, NewState),
|
||||
UpdatedState = call_ringing:dispatch_call_update(CountedState),
|
||||
{reply, ok, UpdatedState};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
dispatch_call_create/1,
|
||||
dispatch_call_update/1,
|
||||
dispatch_call_delete/1,
|
||||
dispatch_voice_state_update/2,
|
||||
handle_ring_timeout/2,
|
||||
handle_idle_timeout/1,
|
||||
maybe_stop_or_reply/3,
|
||||
@@ -251,6 +252,14 @@ dispatch_call_delete(State) ->
|
||||
notify_call_ended(cancel_all_ringing_timers(State)),
|
||||
ok.
|
||||
|
||||
-spec dispatch_voice_state_update(map(), map()) -> ok.
|
||||
dispatch_voice_state_update(VoiceState, State) ->
|
||||
Event = voice_state_utils:sanitize_voice_state_for_broadcast(
|
||||
call_state:format_voice_state(VoiceState)
|
||||
),
|
||||
_ = dispatch_to_recipients(maps:get(recipients, State), voice_state_update, Event),
|
||||
ok.
|
||||
|
||||
-spec dispatch_to_recipients([integer()], atom(), map()) -> pid().
|
||||
dispatch_to_recipients(Recipients, EventType, Event) ->
|
||||
proc_lib:spawn(fun() ->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
handle_join_internal/6,
|
||||
handle_join_internal/7,
|
||||
handle_join_async/5,
|
||||
handle_session_down/2,
|
||||
handle_disconnect_user/4,
|
||||
@@ -12,15 +12,44 @@
|
||||
disconnect_user_after_pending_timeout/4,
|
||||
maybe_notify_session_force_disconnect/4,
|
||||
maybe_spawn_region_switch/3,
|
||||
convert_to_sfu/2,
|
||||
update_voice_state/3,
|
||||
relay_voice_signal/4,
|
||||
is_session_pid_alive/1
|
||||
]).
|
||||
|
||||
-define(PENDING_CONNECTION_TIMEOUT_MS, 30000).
|
||||
|
||||
-spec handle_join_internal(
|
||||
integer(), map(), binary(), pid(), binary() | undefined, map()
|
||||
) -> {reply, ok, map()}.
|
||||
handle_join_internal(UserId, VoiceState, SessionId, SessionPid, ConnectionId, State) ->
|
||||
integer(), map(), binary(), pid(), binary() | undefined, pos_integer() | undefined, map()
|
||||
) -> {reply, ok | {error, atom()}, map()}.
|
||||
handle_join_internal(
|
||||
UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants, State
|
||||
) ->
|
||||
case join_violation(UserId, VoiceState, MaxParticipants, State) of
|
||||
{error, ErrorAtom} ->
|
||||
{reply, {error, ErrorAtom}, State};
|
||||
ok ->
|
||||
NewState = admit_join(
|
||||
UserId,
|
||||
VoiceState,
|
||||
SessionId,
|
||||
SessionPid,
|
||||
pending_connection_id(ConnectionId, VoiceState),
|
||||
State
|
||||
),
|
||||
{reply, ok, NewState}
|
||||
end.
|
||||
|
||||
-spec join_violation(integer(), map(), pos_integer() | undefined, map()) ->
|
||||
ok | {error, atom()}.
|
||||
join_violation(UserId, VoiceState, MaxParticipants, #{voice_states := VoiceStates}) ->
|
||||
voice_p2p:invariant_violation(
|
||||
maps:values(VoiceStates#{UserId => VoiceState}), MaxParticipants
|
||||
).
|
||||
|
||||
-spec admit_join(integer(), map(), binary(), pid(), binary() | undefined, map()) -> map().
|
||||
admit_join(UserId, VoiceState, SessionId, SessionPid, PendingConnectionId, State) ->
|
||||
CleanState = call_ringing:cancel_ringing_timers([UserId], State),
|
||||
BaseState = call_ringing:remove_users_from_ringing([UserId], CleanState),
|
||||
#{
|
||||
@@ -28,23 +57,24 @@ handle_join_internal(UserId, VoiceState, SessionId, SessionPid, ConnectionId, St
|
||||
sessions := Sessions0,
|
||||
participants_history := History0
|
||||
} = BaseState,
|
||||
NewVoiceStates = VoiceStates0#{UserId => VoiceState},
|
||||
NewSessions = monitor_session_entry(
|
||||
Sessions0, SessionId, UserId, SessionPid
|
||||
),
|
||||
NewHistory = sets:add_element(UserId, History0),
|
||||
NewPending = build_pending(ConnectionId, UserId, SessionId, BaseState),
|
||||
NewState = BaseState#{
|
||||
voice_states => NewVoiceStates,
|
||||
sessions => NewSessions,
|
||||
pending_connections => NewPending,
|
||||
participants_history => NewHistory
|
||||
voice_states => VoiceStates0#{UserId => VoiceState},
|
||||
sessions => monitor_session_entry(Sessions0, SessionId, UserId, SessionPid),
|
||||
pending_connections => build_pending(PendingConnectionId, UserId, SessionId, BaseState),
|
||||
participants_history => sets:add_element(UserId, History0)
|
||||
},
|
||||
StateWithTimer = call_ringing:reset_idle_timer(NewState),
|
||||
{UpdatedState, Dispatched} = call_ringing:maybe_dispatch_state_update(
|
||||
BaseState, StateWithTimer
|
||||
),
|
||||
{reply, ok, ensure_call_update(UpdatedState, Dispatched)}.
|
||||
ensure_call_update(UpdatedState, Dispatched).
|
||||
|
||||
-spec pending_connection_id(binary() | undefined, map()) -> binary() | undefined.
|
||||
pending_connection_id(ConnectionId, VoiceState) ->
|
||||
case voice_p2p:is_p2p(VoiceState) of
|
||||
true -> undefined;
|
||||
false -> ConnectionId
|
||||
end.
|
||||
|
||||
-spec build_pending(binary() | undefined, integer(), binary(), map()) -> map().
|
||||
build_pending(undefined, _UserId, _SessionId, #{pending_connections := Pending}) ->
|
||||
@@ -86,26 +116,17 @@ monitor_session_entry(Sessions, SessionId, UserId, SessionPid) ->
|
||||
handle_join_async(
|
||||
UserId, VoiceState, SessionId, SessionPid, #{channel_id := ChannelId} = State
|
||||
) ->
|
||||
CleanState = call_ringing:cancel_ringing_timers([UserId], State),
|
||||
BaseState = call_ringing:remove_users_from_ringing([UserId], CleanState),
|
||||
#{
|
||||
voice_states := VoiceStates0,
|
||||
sessions := Sessions0,
|
||||
participants_history := History0
|
||||
} = BaseState,
|
||||
NewVS = VoiceStates0#{UserId => VoiceState},
|
||||
NewSess = monitor_session_entry(Sessions0, SessionId, UserId, SessionPid),
|
||||
NewHistory = sets:add_element(UserId, History0),
|
||||
NewState = BaseState#{
|
||||
voice_states => NewVS, sessions => NewSess, participants_history => NewHistory
|
||||
},
|
||||
StateWithTimer = call_ringing:reset_idle_timer(NewState),
|
||||
{UpdState, Dispatched} = call_ringing:maybe_dispatch_state_update(
|
||||
BaseState, StateWithTimer
|
||||
),
|
||||
FinalState = ensure_call_update(UpdState, Dispatched),
|
||||
SessionPid ! {call_join_result, ChannelId, {ok, FinalState}},
|
||||
{noreply, FinalState}.
|
||||
case join_violation(UserId, VoiceState, undefined, State) of
|
||||
{error, ErrorAtom} ->
|
||||
SessionPid ! {call_join_result, ChannelId, {error, ErrorAtom}},
|
||||
{noreply, State};
|
||||
ok ->
|
||||
FinalState = admit_join(
|
||||
UserId, VoiceState, SessionId, SessionPid, undefined, State
|
||||
),
|
||||
SessionPid ! {call_join_result, ChannelId, {ok, FinalState}},
|
||||
{noreply, FinalState}
|
||||
end.
|
||||
|
||||
-spec maybe_remove_user_voice_state(integer(), map(), map()) -> map().
|
||||
maybe_remove_user_voice_state(UserId, RemainingSessions, VoiceStates) ->
|
||||
@@ -285,18 +306,86 @@ maybe_spawn_region_switch_for_participants(
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec spawn_voice_server_updates(binary(), map(), map(), integer()) -> ok.
|
||||
-spec spawn_voice_server_updates(binary() | undefined | null, map(), map(), integer()) -> ok.
|
||||
spawn_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) ->
|
||||
spawn(fun() ->
|
||||
send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId)
|
||||
end),
|
||||
ok.
|
||||
|
||||
-spec update_voice_state(integer(), map(), map()) -> map().
|
||||
update_voice_state(UserId, VoiceState, #{voice_states := VoiceStates} = State) ->
|
||||
WasP2p = voice_p2p:is_p2p(maps:get(UserId, VoiceStates)),
|
||||
case {WasP2p, voice_p2p:is_p2p(VoiceState)} of
|
||||
{true, false} ->
|
||||
convert_to_sfu([UserId], State#{voice_states => VoiceStates#{UserId => VoiceState}});
|
||||
{false, true} ->
|
||||
Converted = voice_p2p:to_sfu(VoiceState),
|
||||
NewState = State#{voice_states => VoiceStates#{UserId => Converted}},
|
||||
ok = call_ringing:dispatch_voice_state_update(Converted, NewState),
|
||||
NewState;
|
||||
_ ->
|
||||
State#{voice_states => VoiceStates#{UserId => VoiceState}}
|
||||
end.
|
||||
|
||||
-spec convert_to_sfu([integer()], map()) -> map().
|
||||
convert_to_sfu(
|
||||
ExtraUserIds,
|
||||
#{voice_states := VoiceStates, sessions := Sessions, channel_id := ChannelId} = State
|
||||
) ->
|
||||
Converted = maps:map(
|
||||
fun(_UserId, VoiceState) -> voice_p2p:to_sfu(VoiceState) end,
|
||||
maps:filter(fun(_UserId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end, VoiceStates)
|
||||
),
|
||||
NewVoiceStates = maps:merge(VoiceStates, Converted),
|
||||
Reconnecting = maps:with(ExtraUserIds ++ maps:keys(Converted), NewVoiceStates),
|
||||
case maps:size(Reconnecting) of
|
||||
0 ->
|
||||
State;
|
||||
_ ->
|
||||
NewState = State#{voice_states => NewVoiceStates},
|
||||
ok = dispatch_voice_state_updates(Converted, NewState),
|
||||
spawn_voice_server_updates(
|
||||
maps:get(region, State, undefined), Reconnecting, Sessions, ChannelId
|
||||
),
|
||||
NewState
|
||||
end.
|
||||
|
||||
-spec dispatch_voice_state_updates(map(), map()) -> ok.
|
||||
dispatch_voice_state_updates(VoiceStates, State) ->
|
||||
maps:foreach(
|
||||
fun(_UserId, VoiceState) ->
|
||||
call_ringing:dispatch_voice_state_update(VoiceState, State)
|
||||
end,
|
||||
VoiceStates
|
||||
).
|
||||
|
||||
-spec relay_voice_signal(binary(), binary(), map(), map()) -> ok.
|
||||
relay_voice_signal(
|
||||
SessionId,
|
||||
To,
|
||||
Data,
|
||||
#{voice_states := VoiceStates, sessions := Sessions, channel_id := ChannelId}
|
||||
) ->
|
||||
case voice_p2p:signal_route(maps:values(VoiceStates), SessionId, To) of
|
||||
{ok, Sender, Target} ->
|
||||
dispatch_voice_signal(
|
||||
maps:get(maps:get(<<"session_id">>, Target, undefined), Sessions, undefined),
|
||||
voice_p2p:signal_payload(null, ChannelId, Sender, Data)
|
||||
);
|
||||
error ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec dispatch_voice_signal(term(), map()) -> ok.
|
||||
dispatch_voice_signal({_UserId, SessionPid, _Ref}, Payload) when is_pid(SessionPid) ->
|
||||
gateway_dispatch_relay:dispatch(SessionPid, voice_signal, Payload, 0);
|
||||
dispatch_voice_signal(_Session, _Payload) ->
|
||||
ok.
|
||||
|
||||
-spec send_voice_server_updates(
|
||||
binary() | undefined | null, map(), map(), integer()
|
||||
) -> ok.
|
||||
send_voice_server_updates(NewRegion, _VS, _Sessions, _ChId) when not is_binary(NewRegion) ->
|
||||
ok;
|
||||
send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) ->
|
||||
maps:foreach(
|
||||
fun(_SessionId, {UserId, SessionPid, _Ref}) ->
|
||||
@@ -308,7 +397,9 @@ send_voice_server_updates(NewRegion, VoiceStates, Sessions, ChannelId) ->
|
||||
),
|
||||
ok.
|
||||
|
||||
-spec maybe_send_voice_server_update(integer(), pid(), binary(), integer(), map()) -> ok.
|
||||
-spec maybe_send_voice_server_update(
|
||||
integer(), pid(), binary() | undefined | null, integer(), map()
|
||||
) -> ok.
|
||||
maybe_send_voice_server_update(UserId, SessionPid, NewRegion, ChannelId, VoiceStates) ->
|
||||
case maps:get(UserId, VoiceStates, undefined) of
|
||||
undefined ->
|
||||
@@ -317,7 +408,9 @@ maybe_send_voice_server_update(UserId, SessionPid, NewRegion, ChannelId, VoiceSt
|
||||
send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState)
|
||||
end.
|
||||
|
||||
-spec send_voice_server_update(integer(), integer(), pid(), binary(), map()) -> ok.
|
||||
-spec send_voice_server_update(
|
||||
integer(), integer(), pid(), binary() | undefined | null, map()
|
||||
) -> ok.
|
||||
send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState) ->
|
||||
ConnectionId = maps:get(<<"connection_id">>, VoiceState, null),
|
||||
case ConnectionId of
|
||||
@@ -329,16 +422,12 @@ send_voice_server_update(ChannelId, UserId, SessionPid, NewRegion, VoiceState) -
|
||||
Req0 = voice_utils:build_voice_token_rpc_request(
|
||||
null, ChannelId, UserId, ConnectionId, null, null
|
||||
),
|
||||
Req = voice_utils:add_rtc_region_to_request(Req0, rpc_region(NewRegion)),
|
||||
Req = voice_utils:add_rtc_region_to_request(
|
||||
Req0, dm_voice_state:voice_region_for_rpc(NewRegion)
|
||||
),
|
||||
dispatch_voice_server_rpc(ChannelId, SessionPid, Req)
|
||||
end.
|
||||
|
||||
-spec rpc_region(binary()) -> binary() | null.
|
||||
rpc_region(<<"automatic">>) ->
|
||||
null;
|
||||
rpc_region(NewRegion) ->
|
||||
NewRegion.
|
||||
|
||||
-spec dispatch_voice_server_rpc(integer(), pid(), map()) -> ok.
|
||||
dispatch_voice_server_rpc(ChannelId, SessionPid, Req) ->
|
||||
case rpc_client:call(Req) of
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
-export([
|
||||
error/1,
|
||||
error_category/1
|
||||
error_category/1,
|
||||
voice_error_info/1
|
||||
]).
|
||||
-export_type([error_atom/0, error_category/0]).
|
||||
|
||||
@@ -37,6 +38,8 @@
|
||||
| voice_pending_expired
|
||||
| voice_camera_user_limit
|
||||
| voice_e2ee_required
|
||||
| voice_p2p_consent_required
|
||||
| voice_p2p_unavailable
|
||||
| dm_channel_not_found
|
||||
| dm_not_recipient
|
||||
| dm_invalid_channel_type
|
||||
@@ -84,7 +87,8 @@ voice_error_category(ErrorAtom) ->
|
||||
|
||||
-spec voice_error_info(error_atom()) -> voice_error_info() | undefined.
|
||||
voice_error_info(ErrorAtom) ->
|
||||
maps:get(ErrorAtom, voice_error_infos(), undefined).
|
||||
P2pInfo = maps:get(ErrorAtom, voice_p2p_error_infos(), undefined),
|
||||
maps:get(ErrorAtom, voice_error_infos(), P2pInfo).
|
||||
|
||||
-spec voice_error_infos() -> #{error_atom() => voice_error_info()}.
|
||||
voice_error_infos() ->
|
||||
@@ -149,6 +153,18 @@ voice_error_infos() ->
|
||||
{<<"VOICE_E2EE_REQUIRED">>, voice_e2ee_required_message(), permission_denied}
|
||||
}.
|
||||
|
||||
-spec voice_p2p_error_infos() -> #{error_atom() => voice_error_info()}.
|
||||
voice_p2p_error_infos() ->
|
||||
#{
|
||||
voice_p2p_consent_required =>
|
||||
{<<"VOICE_P2P_CONSENT_REQUIRED">>,
|
||||
<<"This call is peer-to-peer and requires agreement to join">>,
|
||||
permission_denied},
|
||||
voice_p2p_unavailable =>
|
||||
{<<"VOICE_P2P_UNAVAILABLE">>, <<"Peer-to-peer voice is unavailable for this join">>,
|
||||
voice_error}
|
||||
}.
|
||||
|
||||
-spec voice_e2ee_required_message() -> binary().
|
||||
voice_e2ee_required_message() ->
|
||||
iolist_to_binary([
|
||||
|
||||
@@ -125,6 +125,8 @@ websocket_info({dispatch, Event, {pre_encoded, Bin} = Data, Seq}, State) when
|
||||
is_integer(Seq), is_binary(Event), is_binary(Bin)
|
||||
->
|
||||
gateway_handler_dispatch:handle_dispatch(Event, Data, Seq, State);
|
||||
websocket_info({gateway_error, ErrorAtom}, State) when is_atom(ErrorAtom) ->
|
||||
gateway_handler_dispatch:handle_gateway_error(ErrorAtom, State);
|
||||
websocket_info(rollout_config_changed, State) ->
|
||||
gateway_handler_identify:handle_rollout_config_changed(State);
|
||||
websocket_info({retry_pending_identify, Token}, State) when is_reference(Token) ->
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
-export([
|
||||
handle_opcode/3,
|
||||
handle_dispatch/4,
|
||||
handle_gateway_error/2,
|
||||
handle_session_down/1,
|
||||
handle_session_reconnect/1,
|
||||
handle_presence_update/3,
|
||||
@@ -72,6 +73,13 @@ handle_authenticated_opcode(voice_state_update, _Data, State) ->
|
||||
gateway_handler_encode:close_with_reason(
|
||||
decode_error, <<"Invalid voice payload">>, State
|
||||
);
|
||||
handle_authenticated_opcode(voice_signal, Data, #{session_pid := Pid} = State) when
|
||||
is_pid(Pid), is_map(Data)
|
||||
->
|
||||
gen_server:cast(Pid, {voice_signal, Data}),
|
||||
{ok, State};
|
||||
handle_authenticated_opcode(voice_signal, _Data, State) ->
|
||||
{ok, State};
|
||||
handle_authenticated_opcode(request_guild_members, Data, #{session_pid := Pid} = State) when
|
||||
is_pid(Pid)
|
||||
->
|
||||
@@ -125,6 +133,22 @@ do_dispatch(Event, {pre_encoded, EncodedData}, Seq, State) ->
|
||||
do_dispatch(Event, Data, Seq, State) ->
|
||||
dispatch_standard(Event, Data, Seq, State).
|
||||
|
||||
-spec handle_gateway_error(atom(), state()) -> ws_result().
|
||||
handle_gateway_error(ErrorAtom, State) ->
|
||||
{Code, ErrorMessage, _Category} = gateway_errors:voice_error_info(ErrorAtom),
|
||||
Message = #{
|
||||
<<"op">> => constants:opcode_to_num(gateway_error),
|
||||
<<"d">> => #{<<"code">> => Code, <<"message">> => ErrorMessage}
|
||||
},
|
||||
encode_frame(Message, State).
|
||||
|
||||
-spec encode_frame(map(), state()) -> ws_result().
|
||||
encode_frame(Message, State) ->
|
||||
case gateway_handler_encode:encode_and_compress(Message, State) of
|
||||
{ok, Frame, NewState} -> {[Frame], NewState};
|
||||
{error, _} -> {ok, State}
|
||||
end.
|
||||
|
||||
-spec peer_ip(state()) -> binary().
|
||||
peer_ip(#{peer_ip := PeerIP}) when is_binary(PeerIP) ->
|
||||
PeerIP;
|
||||
@@ -197,10 +221,7 @@ handle_session_down(State) ->
|
||||
request_guild_members_pid => undefined,
|
||||
request_guild_members_pending => undefined
|
||||
},
|
||||
case gateway_handler_encode:encode_and_compress(Message, NewState) of
|
||||
{ok, Frame, NewState2} -> {[Frame], NewState2};
|
||||
{error, _} -> {ok, NewState}
|
||||
end.
|
||||
encode_frame(Message, NewState).
|
||||
|
||||
-spec handle_session_reconnect(state()) -> ws_result().
|
||||
handle_session_reconnect(State) ->
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
-define(PRESENCE_RATE_LIMIT_MAX_EVENTS, 5).
|
||||
-define(FORUM_UNREADS_RATE_LIMIT_WINDOW_MS, 5000).
|
||||
-define(FORUM_UNREADS_RATE_LIMIT_MAX_EVENTS, 5).
|
||||
-define(VOICE_SIGNAL_RATE_LIMIT_WINDOW_MS, 10000).
|
||||
-define(VOICE_SIGNAL_RATE_LIMIT_MAX_EVENTS, 240).
|
||||
|
||||
-define(SHARED_IP_RATE_TABLE, gateway_shared_ip_rate).
|
||||
-define(SHARED_USER_RATE_TABLE, gateway_shared_user_rate).
|
||||
@@ -109,6 +111,14 @@ check_opcode_rate_limit(State, request_forum_unreads, Now) ->
|
||||
?FORUM_UNREADS_RATE_LIMIT_WINDOW_MS,
|
||||
?FORUM_UNREADS_RATE_LIMIT_MAX_EVENTS
|
||||
);
|
||||
check_opcode_rate_limit(State, voice_signal, Now) ->
|
||||
check_named_opcode_rate_limit(
|
||||
State,
|
||||
voice_signal,
|
||||
Now,
|
||||
?VOICE_SIGNAL_RATE_LIMIT_WINDOW_MS,
|
||||
?VOICE_SIGNAL_RATE_LIMIT_MAX_EVENTS
|
||||
);
|
||||
check_opcode_rate_limit(State, _Op, _Now) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
@@ -253,6 +253,7 @@ cast_handler(store_pending_connection) -> voice;
|
||||
cast_handler(add_virtual_channel_access) -> voice;
|
||||
cast_handler(remove_virtual_channel_access) -> voice;
|
||||
cast_handler(cleanup_virtual_access_for_user) -> voice;
|
||||
cast_handler(voice_signal) -> voice;
|
||||
cast_handler(update_member_subscriptions) -> subscription;
|
||||
cast_handler(update_dm_partners) -> dm_partners;
|
||||
cast_handler(_) -> undefined.
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
endpoint => binary(),
|
||||
connection_id => binary(),
|
||||
voice_state => map(),
|
||||
needs_token => boolean()
|
||||
needs_token => boolean(),
|
||||
p2p => true,
|
||||
ice_servers => [map()]
|
||||
}.
|
||||
|
||||
-type voice_state_update_rejection() :: #{
|
||||
|
||||
@@ -198,7 +198,39 @@ handle_cast({cleanup_virtual_access_for_user, UId}, State) when
|
||||
is_integer(UId)
|
||||
->
|
||||
NewState = guild_voice_disconnect:cleanup_virtual_channel_access_for_user(UId, State),
|
||||
{noreply, NewState}.
|
||||
{noreply, NewState};
|
||||
handle_cast({voice_signal, Request}, State) when is_map(Request) ->
|
||||
ok = relay_voice_signal(Request, State),
|
||||
{noreply, State}.
|
||||
|
||||
-spec relay_voice_signal(map(), guild_state()) -> ok.
|
||||
relay_voice_signal(
|
||||
#{session_id := SessionId, channel_id := ChannelId, to := To, data := Data}, State
|
||||
) ->
|
||||
ChannelVoiceStates = maps:values(
|
||||
voice_state_utils:channel_voice_states(ChannelId, voice_state_utils:voice_states(State))
|
||||
),
|
||||
case voice_p2p:signal_route(ChannelVoiceStates, SessionId, To) of
|
||||
{ok, Sender, Target} ->
|
||||
GuildId = maps:get(id, State),
|
||||
dispatch_voice_signal(
|
||||
maps:get(<<"session_id">>, Target, undefined),
|
||||
voice_p2p:signal_payload(GuildId, ChannelId, Sender, Data),
|
||||
GuildId,
|
||||
State
|
||||
);
|
||||
error ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec dispatch_voice_signal(term(), map(), integer(), guild_state()) -> ok.
|
||||
dispatch_voice_signal(SessionId, Payload, GuildId, State) ->
|
||||
case maps:get(SessionId, maps:get(sessions, State, #{}), undefined) of
|
||||
#{pid := Pid} when is_pid(Pid) ->
|
||||
gateway_dispatch_relay:dispatch(Pid, voice_signal, Payload, GuildId);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec cast_relay_voice_server(
|
||||
integer(), integer(), binary(), binary(), binary(), binary(), guild_state()
|
||||
|
||||
@@ -74,6 +74,8 @@ build_connect_request(
|
||||
longitude => maps:get(longitude, Opts),
|
||||
e2ee_capable => maps:get(e2ee_capable, Opts),
|
||||
bot => maps:get(bot, Opts),
|
||||
p2p => maps:get(p2p, Opts),
|
||||
country_code => maps:get(country_code, Opts),
|
||||
voice_states => maps:get(dm_voice_states, State, #{}),
|
||||
state => State
|
||||
}.
|
||||
@@ -98,7 +100,9 @@ extract_request_opts(Request) ->
|
||||
latitude => maps:get(latitude, Request, null),
|
||||
longitude => maps:get(longitude, Request, null),
|
||||
e2ee_capable => maps:get(e2ee_capable, Request, false),
|
||||
bot => maps:get(bot, Request, false)
|
||||
bot => maps:get(bot, Request, false),
|
||||
p2p => maps:get(p2p, Request, undefined),
|
||||
country_code => maps:get(country_code, Request, undefined)
|
||||
}.
|
||||
|
||||
-spec handle_dm_connect_or_update(
|
||||
@@ -245,13 +249,16 @@ commit_existing_update(
|
||||
state := State
|
||||
} = Req
|
||||
) ->
|
||||
UpdatedVS = build_updated_voice_state(ExistingVoiceState, Req),
|
||||
OldChannelId = maps:get(<<"channel_id">>, ExistingVoiceState, null),
|
||||
NeedsToken = not snowflake_id:equal(ChannelIdValue, OldChannelId),
|
||||
P2p =
|
||||
voice_p2p:is_p2p(ExistingVoiceState) andalso not NeedsToken andalso
|
||||
not voice_p2p:declined(maps:get(p2p, Req, undefined)),
|
||||
UpdatedVS = build_updated_voice_state(ExistingVoiceState, Req#{p2p => P2p}),
|
||||
NewVoiceStates = VoiceStates#{ConnectionId => UpdatedVS},
|
||||
NewState = State#{dm_voice_states => NewVoiceStates},
|
||||
_ = voice_state_counts_cache:upsert_voice_state(UpdatedVS),
|
||||
dm_voice_ring:broadcast_voice_state_update(ChannelIdValue, UpdatedVS, NewState),
|
||||
OldChannelId = maps:get(<<"channel_id">>, ExistingVoiceState, null),
|
||||
NeedsToken = not snowflake_id:equal(ChannelIdValue, OldChannelId),
|
||||
UserId = maps:get(user_id, Req),
|
||||
maybe_spawn_call_voice_state_update(NeedsToken, ChannelIdValue, UserId, UpdatedVS),
|
||||
dm_voice_token:maybe_spawn_join_call(
|
||||
@@ -294,6 +301,7 @@ build_updated_voice_state(ExistingVoiceState, Req) ->
|
||||
<<"is_mobile">> => maps:get(is_mobile, Req),
|
||||
<<"suppress">> => false,
|
||||
<<"viewer_stream_keys">> => maps:get(viewer_stream_keys, Req),
|
||||
<<"p2p">> => maps:get(p2p, Req),
|
||||
<<"version">> => OldVersion + 1
|
||||
}).
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
-export([handle_dm_disconnect/4, disconnect_voice_user/2]).
|
||||
-export([resolve_call_region/1, resolve_call_region/2, voice_region_for_rpc/1]).
|
||||
-export([call_voice_states/1]).
|
||||
-export([normalize_session_id/1, resolve_effective_session_id/2]).
|
||||
-export([validate_dm_viewer_stream_keys/3]).
|
||||
-export([maybe_attach_voice_routing_metadata/3]).
|
||||
@@ -212,6 +213,20 @@ call_region_from_pid(CallPid, Timeout) ->
|
||||
null
|
||||
end.
|
||||
|
||||
-spec call_voice_states(integer()) -> [voice_state()].
|
||||
call_voice_states(ChannelId) ->
|
||||
case call_manager:lookup(ChannelId) of
|
||||
{ok, CallPid} -> call_voice_states_from_pid(CallPid);
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
-spec call_voice_states_from_pid(pid()) -> [voice_state()].
|
||||
call_voice_states_from_pid(CallPid) ->
|
||||
case gateway_rpc_call_lookup:safe_gen_server_call(CallPid, {get_state}, 5000) of
|
||||
{ok, {ok, #{voice_states := VoiceStates}}} when is_list(VoiceStates) -> VoiceStates;
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
-spec voice_region_for_rpc(term()) -> binary() | null.
|
||||
voice_region_for_rpc(<<"automatic">>) -> null;
|
||||
voice_region_for_rpc(Region) when is_binary(Region) -> Region;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([get_dm_voice_token_and_create_state/1, get_voice_token/6]).
|
||||
-export([join_or_create_call/5, join_or_create_call/6]).
|
||||
-export([join_or_create_call/6]).
|
||||
-export([maybe_spawn_join_call/6, dispatch_to_session/4]).
|
||||
|
||||
-export_type([
|
||||
@@ -35,9 +35,42 @@ get_dm_voice_token_and_create_state(Req) ->
|
||||
"dm_voice_token_create_request: user_id=~p channel_id=~p rtc_region=~p",
|
||||
[UserId, ChannelId, Region]
|
||||
),
|
||||
case rpc_client:call(ReqWithRegion) of
|
||||
case dm_join_decision(Req) of
|
||||
{reject, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
Decision ->
|
||||
request_dm_token(
|
||||
voice_p2p:add_to_token_request(
|
||||
ReqWithRegion, Decision, maps:get(country_code, Req, undefined)
|
||||
),
|
||||
Decision,
|
||||
Req
|
||||
)
|
||||
end.
|
||||
|
||||
-spec dm_join_decision(token_request()) -> voice_p2p:join_decision().
|
||||
dm_join_decision(Req) ->
|
||||
UserId = maps:get(user_id, Req),
|
||||
voice_p2p:join_decision(
|
||||
voice_p2p:agreed(maps:get(p2p, Req, undefined), maps:get(bot, Req, false)),
|
||||
[
|
||||
VoiceState
|
||||
|| VoiceState <- dm_voice_state:call_voice_states(maps:get(channel_id, Req)),
|
||||
not guild_voice_state:user_matches_voice_state(VoiceState, UserId)
|
||||
],
|
||||
false,
|
||||
false
|
||||
).
|
||||
|
||||
-spec request_dm_token(map(), voice_p2p:join_decision(), token_request()) ->
|
||||
{reply, map(), dm_state()} | {reply, {error, atom(), atom()}, dm_state()}.
|
||||
request_dm_token(TokenReq, Decision, Req) ->
|
||||
UserId = maps:get(user_id, Req),
|
||||
ChannelId = maps:get(channel_id, Req),
|
||||
State = maps:get(state, Req),
|
||||
case rpc_client:call(TokenReq) of
|
||||
{ok, Data} ->
|
||||
handle_dm_token_success(Data, Req);
|
||||
handle_dm_token_success(Decision, Data, Req);
|
||||
{error, {rpc_error, _Status, Body}} ->
|
||||
handle_token_rpc_error(UserId, ChannelId, Body, State);
|
||||
{error, Reason} ->
|
||||
@@ -60,8 +93,40 @@ handle_token_rpc_error(UserId, ChannelId, Body, State) ->
|
||||
false -> {reply, gateway_errors:error(voice_token_failed), State}
|
||||
end.
|
||||
|
||||
-spec handle_dm_token_success(map(), token_request()) -> {reply, map(), dm_state()}.
|
||||
handle_dm_token_success(Data, Req) ->
|
||||
-spec handle_dm_token_success(voice_p2p:join_decision(), map(), token_request()) ->
|
||||
{reply, map(), dm_state()} | {reply, {error, atom(), atom()}, dm_state()}.
|
||||
handle_dm_token_success(Decision, Data, Req) ->
|
||||
case voice_p2p:admit(Decision, voice_p2p:grant(Data)) of
|
||||
{reject, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), maps:get(state, Req)};
|
||||
{p2p, ConnectionId, IceServers, MaxParticipants} ->
|
||||
handle_dm_p2p_grant(ConnectionId, IceServers, MaxParticipants, Req);
|
||||
sfu ->
|
||||
handle_dm_sfu_token(Data, Req)
|
||||
end.
|
||||
|
||||
-spec handle_dm_p2p_grant(binary(), [map()], pos_integer(), token_request()) ->
|
||||
{reply, map(), dm_state()}.
|
||||
handle_dm_p2p_grant(ConnectionId, IceServers, MaxParticipants, Req) ->
|
||||
UserId = maps:get(user_id, Req),
|
||||
ChannelId = maps:get(channel_id, Req),
|
||||
SessionId = maps:get(session_id, Req),
|
||||
State = maps:get(state, Req),
|
||||
EffE2EE = maps:get(e2ee_capable, Req) andalso guild_voice_e2ee:is_e2ee_enabled_for_dm(),
|
||||
VoiceState = build_voice_state(Req, ConnectionId, EffE2EE, true),
|
||||
NewState = store_and_broadcast(ConnectionId, ChannelId, VoiceState, State),
|
||||
SessionPid = maps:get(session_pid, State),
|
||||
dispatch_to_session(
|
||||
SessionPid,
|
||||
voice_server_update,
|
||||
voice_p2p:voice_server_update(ConnectionId, ChannelId, null, IceServers),
|
||||
null
|
||||
),
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants),
|
||||
{reply, #{success => true, needs_token => false, connection_id => ConnectionId}, NewState}.
|
||||
|
||||
-spec handle_dm_sfu_token(map(), token_request()) -> {reply, map(), dm_state()}.
|
||||
handle_dm_sfu_token(Data, Req) ->
|
||||
UserId = maps:get(user_id, Req),
|
||||
ChannelId = maps:get(channel_id, Req),
|
||||
SessionId = maps:get(session_id, Req),
|
||||
@@ -71,7 +136,7 @@ handle_dm_token_success(Data, Req) ->
|
||||
Endpoint = maps:get(<<"endpoint">>, Data),
|
||||
ConnectionId = maps:get(<<"connectionId">>, Data),
|
||||
EffE2EE = E2EECapable andalso guild_voice_e2ee:is_e2ee_enabled_for_dm(),
|
||||
VoiceState0 = build_voice_state(Req, ConnectionId, EffE2EE),
|
||||
VoiceState0 = build_voice_state(Req, ConnectionId, EffE2EE, false),
|
||||
VoiceState = dm_voice_state:maybe_attach_voice_routing_metadata(
|
||||
VoiceState0,
|
||||
maps:get(<<"regionId">>, Data, undefined),
|
||||
@@ -82,11 +147,11 @@ handle_dm_token_success(Data, Req) ->
|
||||
VSUpdate = build_voice_server_update(Token, Endpoint, ChannelId, ConnectionId, E2EEKey),
|
||||
SessionPid = maps:get(session_pid, State),
|
||||
dispatch_to_session(SessionPid, voice_server_update, VSUpdate, null),
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid),
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, undefined),
|
||||
{reply, #{success => true, needs_token => false, connection_id => ConnectionId}, NewState}.
|
||||
|
||||
-spec build_voice_state(token_request(), binary(), boolean()) -> voice_state().
|
||||
build_voice_state(Req, ConnectionId, EffE2EE) ->
|
||||
-spec build_voice_state(token_request(), binary(), boolean(), boolean()) -> voice_state().
|
||||
build_voice_state(Req, ConnectionId, EffE2EE, P2p) ->
|
||||
voice_state_utils:complete_voice_state(#{
|
||||
<<"guild_id">> => null,
|
||||
<<"user_id">> => integer_to_binary(maps:get(user_id, Req)),
|
||||
@@ -102,7 +167,8 @@ build_voice_state(Req, ConnectionId, EffE2EE) ->
|
||||
<<"self_stream">> => maps:get(self_stream, Req),
|
||||
<<"suppress">> => false,
|
||||
<<"viewer_stream_keys">> => maps:get(viewer_stream_keys, Req),
|
||||
<<"e2ee_capable">> => EffE2EE
|
||||
<<"e2ee_capable">> => EffE2EE,
|
||||
<<"p2p">> => P2p
|
||||
}).
|
||||
|
||||
-spec store_and_broadcast(binary(), integer(), voice_state(), dm_state()) -> dm_state().
|
||||
@@ -162,17 +228,27 @@ dispatch_to_session_with_relay(SessionPid, Event, Payload, GuildId) ->
|
||||
error:undef -> gen_server:cast(SessionPid, {dispatch, Event, Payload})
|
||||
end.
|
||||
|
||||
-spec spawn_join_call(integer(), integer(), voice_state(), binary(), pid()) -> ok.
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) ->
|
||||
-spec spawn_join_call(
|
||||
integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined
|
||||
) -> ok.
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) ->
|
||||
spawn(fun() ->
|
||||
safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid)
|
||||
safe_join_or_create_call(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants
|
||||
)
|
||||
end),
|
||||
ok.
|
||||
|
||||
-spec safe_join_or_create_call(integer(), integer(), voice_state(), binary(), pid()) -> ok.
|
||||
safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) ->
|
||||
-spec safe_join_or_create_call(
|
||||
integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined
|
||||
) -> ok.
|
||||
safe_join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) ->
|
||||
_ = shard_utils:safe_apply(
|
||||
fun() -> join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) end,
|
||||
fun() ->
|
||||
join_or_create_call(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants
|
||||
)
|
||||
end,
|
||||
ok
|
||||
),
|
||||
ok.
|
||||
@@ -187,7 +263,7 @@ maybe_spawn_join_call(true, ChannelId, UserId, VoiceState, SessionId, State) whe
|
||||
->
|
||||
case maps:get(session_pid, State, undefined) of
|
||||
Pid when is_pid(Pid) ->
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, Pid);
|
||||
spawn_join_call(ChannelId, UserId, VoiceState, SessionId, Pid, undefined);
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
@@ -246,21 +322,33 @@ handle_get_voice_token_ok(Data, UserId, ChannelId, SessionPid) ->
|
||||
}},
|
||||
ok.
|
||||
|
||||
-spec join_or_create_call(integer(), integer(), voice_state(), binary(), pid()) -> ok.
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid) ->
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, 10).
|
||||
-spec join_or_create_call(
|
||||
integer(), integer(), voice_state(), binary(), pid(), pos_integer() | undefined
|
||||
) -> ok.
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants) ->
|
||||
join_or_create_call(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, 10
|
||||
).
|
||||
|
||||
-spec join_or_create_call(
|
||||
integer(), integer(), voice_state(), binary(), pid(), non_neg_integer()
|
||||
integer(),
|
||||
integer(),
|
||||
voice_state(),
|
||||
binary(),
|
||||
pid(),
|
||||
pos_integer() | undefined,
|
||||
non_neg_integer()
|
||||
) -> ok.
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, _SessId, SessionPid, 0) ->
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, _SessId, SessionPid, _MaxParticipants, 0) ->
|
||||
logger:warning(
|
||||
"dm_voice_join_or_create_call_exhausted_retries: user_id=~p channel_id=~p",
|
||||
[UserId, ChannelId]
|
||||
),
|
||||
rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid),
|
||||
ok;
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) ->
|
||||
join_or_create_call(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries
|
||||
) ->
|
||||
ConnectionId = maps:get(<<"connection_id">>, VoiceState, undefined),
|
||||
case call_manager:lookup(ChannelId) of
|
||||
{ok, CallPid} ->
|
||||
@@ -272,10 +360,13 @@ join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retrie
|
||||
SessionId,
|
||||
SessionPid,
|
||||
ConnectionId,
|
||||
MaxParticipants,
|
||||
Retries
|
||||
);
|
||||
_ ->
|
||||
retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries)
|
||||
retry_join(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries
|
||||
)
|
||||
end.
|
||||
|
||||
-spec do_join_call(
|
||||
@@ -286,15 +377,28 @@ join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retrie
|
||||
binary(),
|
||||
pid(),
|
||||
term(),
|
||||
pos_integer() | undefined,
|
||||
non_neg_integer()
|
||||
) -> ok.
|
||||
do_join_call(
|
||||
CallPid, ChannelId, UserId, VoiceState, SessionId, SessionPid, ConnectionId, Retries
|
||||
CallPid,
|
||||
ChannelId,
|
||||
UserId,
|
||||
VoiceState,
|
||||
SessionId,
|
||||
SessionPid,
|
||||
ConnectionId,
|
||||
MaxParticipants,
|
||||
Retries
|
||||
) ->
|
||||
JoinMsg =
|
||||
case ConnectionId of
|
||||
undefined -> {join, UserId, VoiceState, SessionId, SessionPid};
|
||||
_ -> {join, UserId, VoiceState, SessionId, SessionPid, ConnectionId}
|
||||
case {ConnectionId, MaxParticipants} of
|
||||
{undefined, _} ->
|
||||
{join, UserId, VoiceState, SessionId, SessionPid};
|
||||
{_, undefined} ->
|
||||
{join, UserId, VoiceState, SessionId, SessionPid, ConnectionId};
|
||||
_ ->
|
||||
{join, UserId, VoiceState, SessionId, SessionPid, ConnectionId, MaxParticipants}
|
||||
end,
|
||||
case gateway_rpc_call_lookup:safe_gen_server_call(CallPid, JoinMsg, 5000) of
|
||||
{ok, ok} ->
|
||||
@@ -304,12 +408,17 @@ do_join_call(
|
||||
[UserId, ChannelId, Retries]
|
||||
),
|
||||
ok;
|
||||
{ok, {error, ErrorAtom}} ->
|
||||
gen_server:cast(SessionPid, {voice_rejected, ErrorAtom}),
|
||||
rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid);
|
||||
Error ->
|
||||
logger:warning(
|
||||
dm_join_call_failed_log_message(),
|
||||
[UserId, ChannelId, Retries, Error]
|
||||
),
|
||||
retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries)
|
||||
retry_join(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries
|
||||
)
|
||||
end.
|
||||
|
||||
-spec dm_join_call_failed_log_message() -> string().
|
||||
@@ -317,14 +426,24 @@ dm_join_call_failed_log_message() ->
|
||||
"dm_voice_join_or_create_call_failed: user_id=~p channel_id=~p "
|
||||
"retries_left=~p error=~p".
|
||||
|
||||
-spec retry_join(integer(), integer(), voice_state(), binary(), pid(), non_neg_integer()) -> ok.
|
||||
retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries) ->
|
||||
-spec retry_join(
|
||||
integer(),
|
||||
integer(),
|
||||
voice_state(),
|
||||
binary(),
|
||||
pid(),
|
||||
pos_integer() | undefined,
|
||||
non_neg_integer()
|
||||
) -> ok.
|
||||
retry_join(ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries) ->
|
||||
logger:warning(
|
||||
"dm_voice_join_or_create_call_lookup_not_found: user_id=~p channel_id=~p retries_left=~p",
|
||||
[UserId, ChannelId, Retries]
|
||||
),
|
||||
ok = gateway_retry_timer:wait(300),
|
||||
join_or_create_call(ChannelId, UserId, VoiceState, SessionId, SessionPid, Retries - 1).
|
||||
join_or_create_call(
|
||||
ChannelId, UserId, VoiceState, SessionId, SessionPid, MaxParticipants, Retries - 1
|
||||
).
|
||||
|
||||
-spec rollback_ghost_voice_state(integer(), voice_state(), pid()) -> ok.
|
||||
rollback_ghost_voice_state(ChannelId, VoiceState, SessionPid) ->
|
||||
@@ -357,7 +476,9 @@ do_join_call_dead_pid_exhausts_without_crash_test() ->
|
||||
VoiceState = #{<<"connection_id">> => <<"connection">>},
|
||||
?assertEqual(
|
||||
ok,
|
||||
do_join_call(DeadPid, 1234, 42, VoiceState, <<"session">>, self(), <<"connection">>, 1)
|
||||
do_join_call(
|
||||
DeadPid, 1234, 42, VoiceState, <<"session">>, self(), <<"connection">>, undefined, 1
|
||||
)
|
||||
),
|
||||
receive
|
||||
{'$gen_cast', {call_monitor, 1234, DeadPid}} ->
|
||||
@@ -370,7 +491,7 @@ exhausted_retries_casts_force_disconnect_test() ->
|
||||
VoiceState = #{<<"connection_id">> => <<"conn-x">>},
|
||||
?assertEqual(
|
||||
ok,
|
||||
join_or_create_call(1234, 42, VoiceState, <<"session">>, self(), 0)
|
||||
join_or_create_call(1234, 42, VoiceState, <<"session">>, self(), undefined, 0)
|
||||
),
|
||||
receive
|
||||
{'$gen_cast', {call_force_disconnect, 1234, <<"conn-x">>}} ->
|
||||
@@ -382,7 +503,7 @@ exhausted_retries_casts_force_disconnect_test() ->
|
||||
exhausted_retries_without_connection_id_casts_undefined_test() ->
|
||||
?assertEqual(
|
||||
ok,
|
||||
join_or_create_call(1234, 42, #{}, <<"session">>, self(), 0)
|
||||
join_or_create_call(1234, 42, #{}, <<"session">>, self(), undefined, 0)
|
||||
),
|
||||
receive
|
||||
{'$gen_cast', {call_force_disconnect, 1234, undefined}} ->
|
||||
|
||||
@@ -129,7 +129,7 @@ apply_same_channel_update(Update, ParsedViewerKey) ->
|
||||
ChannelIdValue = maps:get(channel_id, Update),
|
||||
VoicePerms = maps:get(voice_permissions, Update),
|
||||
Flags = guild_voice_connection_util:voice_flags_for_permissions(Context, VoicePerms),
|
||||
UpdateResult = guild_voice_state:update_voice_state_data(#{
|
||||
{reply, Reply, NewState} = guild_voice_state:update_voice_state_data(#{
|
||||
connection_id => maps:get(connection_id, Update),
|
||||
channel_id => integer_to_binary(ChannelIdValue),
|
||||
flags => Flags,
|
||||
@@ -140,4 +140,7 @@ apply_same_channel_update(Update, ParsedViewerKey) ->
|
||||
needs_token => false,
|
||||
viewer_stream_keys => ParsedViewerKey
|
||||
}),
|
||||
UpdateResult.
|
||||
case voice_p2p:declined(maps:get(p2p, Context, undefined)) of
|
||||
true -> {reply, Reply, guild_voice_region:switch_to_sfu(ChannelIdValue, NewState)};
|
||||
false -> {reply, Reply, NewState}
|
||||
end.
|
||||
@@ -86,7 +86,7 @@ log_pending_found(ConnectionId, PendingData) ->
|
||||
).
|
||||
|
||||
-spec activate_pending(binary(), map(), map(), guild_state()) ->
|
||||
{reply, map(), guild_state()}.
|
||||
{reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
activate_pending(ConnectionId, PendingData, PendingConnections, State) ->
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
VoiceState = guild_voice_connection_pending:resolve_voice_state_from_pending(
|
||||
@@ -102,14 +102,29 @@ activate_pending(ConnectionId, PendingData, PendingConnections, State) ->
|
||||
end.
|
||||
|
||||
-spec finalize_activation(binary(), voice_state(), map(), guild_state()) ->
|
||||
{reply, map(), guild_state()}.
|
||||
{reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
finalize_activation(ConnectionId, VoiceState, VoiceStates, State) ->
|
||||
UpdatedVoiceStates = VoiceStates#{ConnectionId => VoiceState},
|
||||
S1 = State#{voice_states => UpdatedVoiceStates},
|
||||
S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1),
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, null),
|
||||
guild_voice_broadcast:broadcast_voice_state_update(VoiceState, S2, ChannelIdBin),
|
||||
{reply, #{success => true}, S2}.
|
||||
case channel_invariant_violation(VoiceState, UpdatedVoiceStates) of
|
||||
ok ->
|
||||
S1 = State#{voice_states => UpdatedVoiceStates},
|
||||
S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1),
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, VoiceState, null),
|
||||
guild_voice_broadcast:broadcast_voice_state_update(VoiceState, S2, ChannelIdBin),
|
||||
{reply, #{success => true}, S2};
|
||||
{error, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State}
|
||||
end.
|
||||
|
||||
-spec channel_invariant_violation(voice_state(), map()) -> ok | {error, atom()}.
|
||||
channel_invariant_violation(VoiceState, VoiceStates) ->
|
||||
voice_p2p:invariant_violation(
|
||||
maps:values(
|
||||
voice_state_utils:channel_voice_states(
|
||||
voice_state_utils:voice_state_channel_id(VoiceState), VoiceStates
|
||||
)
|
||||
)
|
||||
).
|
||||
|
||||
-spec try_restore_from_recently_disconnected(binary(), guild_state()) ->
|
||||
{reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
@@ -126,10 +141,20 @@ try_restore_from_recently_disconnected(ConnectionId, State) ->
|
||||
end.
|
||||
|
||||
-spec restore_recently_disconnected(binary(), voice_state(), map(), guild_state()) ->
|
||||
{reply, map(), guild_state()}.
|
||||
{reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
restore_recently_disconnected(ConnectionId, VoiceState, Cache, State) ->
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
UpdatedVoiceStates = VoiceStates#{ConnectionId => VoiceState},
|
||||
case channel_invariant_violation(VoiceState, UpdatedVoiceStates) of
|
||||
ok ->
|
||||
finalize_restore(ConnectionId, VoiceState, UpdatedVoiceStates, Cache, State);
|
||||
{error, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State}
|
||||
end.
|
||||
|
||||
-spec finalize_restore(binary(), voice_state(), map(), map(), guild_state()) ->
|
||||
{reply, map(), guild_state()}.
|
||||
finalize_restore(ConnectionId, VoiceState, UpdatedVoiceStates, Cache, State) ->
|
||||
NewCache = maps:remove(ConnectionId, Cache),
|
||||
S0 = State#{voice_states => UpdatedVoiceStates},
|
||||
S1 = S0#{recently_disconnected_voice_states => NewCache},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([handle_new_connection/5]).
|
||||
-export([handle_new_connection/5, activate_p2p_voice_state/3]).
|
||||
|
||||
-export_type([
|
||||
guild_state/0,
|
||||
@@ -154,36 +154,81 @@ request_token_and_build(Context, Member, ParsedViewerStreamKey, State, GuildId,
|
||||
TokenNonce = voice_utils:generate_token_nonce(),
|
||||
Latitude = maps:get(latitude, Context, undefined),
|
||||
Longitude = maps:get(longitude, Context, undefined),
|
||||
Build = #{
|
||||
context => Context,
|
||||
member => Member,
|
||||
viewer_stream_keys => ParsedViewerStreamKey,
|
||||
state => State,
|
||||
guild_id => GuildId,
|
||||
guild_id_bin => GuildIdBin,
|
||||
voice_permissions => VoicePermissions,
|
||||
token_nonce => TokenNonce,
|
||||
latitude => Latitude,
|
||||
longitude => Longitude
|
||||
},
|
||||
case
|
||||
guild_voice_connection_token:request_voice_token(
|
||||
GuildId,
|
||||
ChannelIdValue,
|
||||
UserId,
|
||||
null,
|
||||
VoicePermissions,
|
||||
TokenNonce,
|
||||
Latitude,
|
||||
Longitude
|
||||
voice_p2p:guild_join_decision(
|
||||
guild_voice_connection_util:p2p_agreed(Context), ChannelIdValue, State
|
||||
)
|
||||
of
|
||||
{ok, TokenData} ->
|
||||
build_new_voice_state(#{
|
||||
context => Context,
|
||||
member => Member,
|
||||
viewer_stream_keys => ParsedViewerStreamKey,
|
||||
state => State,
|
||||
guild_id => GuildId,
|
||||
guild_id_bin => GuildIdBin,
|
||||
token_data => TokenData,
|
||||
voice_permissions => VoicePermissions,
|
||||
token_nonce => TokenNonce,
|
||||
latitude => Latitude,
|
||||
longitude => Longitude
|
||||
});
|
||||
{error, _Reason} ->
|
||||
{reply, gateway_errors:error(voice_token_failed), State}
|
||||
{reject, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
Decision ->
|
||||
TokenResult = guild_voice_connection_token:request_voice_token(
|
||||
GuildId,
|
||||
ChannelIdValue,
|
||||
UserId,
|
||||
null,
|
||||
VoicePermissions,
|
||||
TokenNonce,
|
||||
Latitude,
|
||||
Longitude,
|
||||
Decision,
|
||||
maps:get(country_code, Context, undefined)
|
||||
),
|
||||
build_join_reply(Decision, TokenResult, Build)
|
||||
end.
|
||||
|
||||
-spec build_join_reply(voice_p2p:join_decision(), {ok, term()} | {error, term()}, map()) ->
|
||||
{reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
build_join_reply(_Decision, {error, _Reason}, #{state := State}) ->
|
||||
{reply, gateway_errors:error(voice_token_failed), State};
|
||||
build_join_reply(Decision, {ok, TokenData}, #{state := State} = Build) ->
|
||||
case voice_p2p:admit(Decision, TokenData) of
|
||||
{reject, ErrorAtom} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{p2p, ConnectionId, IceServers, _MaxParticipants} ->
|
||||
VoiceBuild = voice_build_fields(Build#{token_data => #{}}),
|
||||
activate_p2p_voice_state(
|
||||
create_and_decorate_voice_state(VoiceBuild#{connection_id => ConnectionId}),
|
||||
IceServers,
|
||||
State
|
||||
);
|
||||
sfu ->
|
||||
build_new_voice_state(Build#{token_data => TokenData})
|
||||
end.
|
||||
|
||||
-spec activate_p2p_voice_state(voice_state(), [map()], guild_state()) ->
|
||||
{reply, map(), guild_state()}.
|
||||
activate_p2p_voice_state(VoiceState0, IceServers, State) ->
|
||||
VoiceState = VoiceState0#{<<"p2p">> => true},
|
||||
ConnectionId = maps:get(<<"connection_id">>, VoiceState),
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
S1 = State#{voice_states => VoiceStates#{ConnectionId => VoiceState}},
|
||||
S2 = guild_voice_connection_util:clear_virtual_access_flags(VoiceState, S1),
|
||||
guild_voice_broadcast:broadcast_voice_state_update(
|
||||
VoiceState, S2, maps:get(<<"channel_id">>, VoiceState)
|
||||
),
|
||||
{reply,
|
||||
#{
|
||||
success => true,
|
||||
p2p => true,
|
||||
ice_servers => IceServers,
|
||||
connection_id => ConnectionId,
|
||||
voice_state => voice_state_utils:external_voice_state(VoiceState)
|
||||
},
|
||||
S2}.
|
||||
|
||||
-spec build_new_voice_state(
|
||||
map()
|
||||
) -> {reply, map(), guild_state()}.
|
||||
|
||||
@@ -66,10 +66,7 @@ do_request_move_token(Context, ChannelIdValue, Member, SessionId, MoveState, Gui
|
||||
TokenNonce = voice_utils:generate_token_nonce(),
|
||||
Lat = maps:get(latitude, Context, undefined),
|
||||
Lng = maps:get(longitude, Context, undefined),
|
||||
TokenResult = guild_voice_connection_token:request_voice_token(
|
||||
GuildId, ChannelIdValue, UserId, ConnId, VoicePerms, TokenNonce, Lat, Lng
|
||||
),
|
||||
handle_move_token_result(TokenResult, #{
|
||||
Build = #{
|
||||
context => Context,
|
||||
channel_id => ChannelIdValue,
|
||||
member => Member,
|
||||
@@ -83,18 +80,60 @@ do_request_move_token(Context, ChannelIdValue, Member, SessionId, MoveState, Gui
|
||||
token_nonce => TokenNonce,
|
||||
latitude => Lat,
|
||||
longitude => Lng
|
||||
}).
|
||||
},
|
||||
case
|
||||
voice_p2p:guild_join_decision(
|
||||
guild_voice_connection_util:p2p_agreed(Context), ChannelIdValue, State
|
||||
)
|
||||
of
|
||||
{reject, ErrorAtom} ->
|
||||
move_error_reply(ErrorAtom, Build);
|
||||
Decision ->
|
||||
TokenResult = guild_voice_connection_token:request_voice_token(
|
||||
GuildId,
|
||||
ChannelIdValue,
|
||||
UserId,
|
||||
ConnId,
|
||||
VoicePerms,
|
||||
TokenNonce,
|
||||
Lat,
|
||||
Lng,
|
||||
Decision,
|
||||
maps:get(country_code, Context, undefined)
|
||||
),
|
||||
handle_move_token_result(Decision, TokenResult, Build)
|
||||
end.
|
||||
|
||||
-spec handle_move_token_result(
|
||||
{ok, map()} | {error, term()}, map()
|
||||
voice_p2p:join_decision(), {ok, term()} | {error, term()}, map()
|
||||
) -> {reply, map(), guild_state()} | {reply, {error, atom(), atom()}, guild_state()}.
|
||||
handle_move_token_result({ok, TokenData}, Build) ->
|
||||
build_move_result(Build#{token_data => TokenData});
|
||||
handle_move_token_result({error, _Reason}, Build) ->
|
||||
#{state := State, context := Context, channel_id := ChannelIdValue} = Build,
|
||||
handle_move_token_result(_Decision, {error, _Reason}, Build) ->
|
||||
move_error_reply(voice_token_failed, Build);
|
||||
handle_move_token_result(Decision, {ok, TokenData}, Build) ->
|
||||
case voice_p2p:admit(Decision, TokenData) of
|
||||
{reject, ErrorAtom} ->
|
||||
move_error_reply(ErrorAtom, Build);
|
||||
{p2p, ConnectionId, IceServers, _MaxParticipants} ->
|
||||
build_p2p_move_result(ConnectionId, IceServers, Build#{token_data => #{}});
|
||||
sfu ->
|
||||
build_move_result(Build#{token_data => TokenData})
|
||||
end.
|
||||
|
||||
-spec move_error_reply(atom(), map()) -> {reply, {error, atom(), atom()}, guild_state()}.
|
||||
move_error_reply(ErrorAtom, #{state := State, context := Context, channel_id := ChannelIdValue}) ->
|
||||
UserId = maps:get(user_id, Context),
|
||||
{reply, gateway_errors:error(voice_token_failed),
|
||||
clear_move_flags(UserId, ChannelIdValue, State)}.
|
||||
{reply, gateway_errors:error(ErrorAtom), clear_move_flags(UserId, ChannelIdValue, State)}.
|
||||
|
||||
-spec build_p2p_move_result(binary(), [map()], map()) -> {reply, map(), guild_state()}.
|
||||
build_p2p_move_result(ConnectionId, IceServers, Build) ->
|
||||
State1 = disconnect_old_connection(Build),
|
||||
State1Cleaned = cleanup_stale_virtual_access(Build, State1),
|
||||
MoveBuild = move_build_fields(Build),
|
||||
guild_voice_connection_join:activate_p2p_voice_state(
|
||||
build_move_voice_state(MoveBuild#{connection_id => ConnectionId}),
|
||||
IceServers,
|
||||
State1Cleaned
|
||||
).
|
||||
|
||||
-spec build_move_result(
|
||||
map()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([request_voice_token/8]).
|
||||
-export([request_voice_token/10]).
|
||||
|
||||
-spec request_voice_token(
|
||||
integer(),
|
||||
@@ -26,15 +27,62 @@ request_voice_token(
|
||||
Latitude,
|
||||
Longitude
|
||||
) ->
|
||||
Req = voice_utils:build_voice_token_rpc_request(
|
||||
GuildId,
|
||||
ChannelId,
|
||||
UserId,
|
||||
ConnectionId,
|
||||
Latitude,
|
||||
Longitude,
|
||||
VoicePermissions,
|
||||
TokenNonce
|
||||
case
|
||||
request_voice_token(
|
||||
GuildId,
|
||||
ChannelId,
|
||||
UserId,
|
||||
ConnectionId,
|
||||
VoicePermissions,
|
||||
TokenNonce,
|
||||
Latitude,
|
||||
Longitude,
|
||||
sfu,
|
||||
undefined
|
||||
)
|
||||
of
|
||||
{ok, TokenData} when is_map(TokenData) -> {ok, TokenData};
|
||||
{ok, _Grant} -> {error, voice_token_failed};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
-spec request_voice_token(
|
||||
integer(),
|
||||
integer(),
|
||||
integer(),
|
||||
binary() | null,
|
||||
map(),
|
||||
binary() | null,
|
||||
binary() | undefined | null,
|
||||
binary() | undefined | null,
|
||||
voice_p2p:join_decision(),
|
||||
binary() | undefined
|
||||
) -> {ok, map() | voice_p2p:token_grant()} | {error, term()}.
|
||||
request_voice_token(
|
||||
GuildId,
|
||||
ChannelId,
|
||||
UserId,
|
||||
ConnectionId,
|
||||
VoicePermissions,
|
||||
TokenNonce,
|
||||
Latitude,
|
||||
Longitude,
|
||||
Decision,
|
||||
CountryCode
|
||||
) ->
|
||||
Req = voice_p2p:add_to_token_request(
|
||||
voice_utils:build_voice_token_rpc_request(
|
||||
GuildId,
|
||||
ChannelId,
|
||||
UserId,
|
||||
ConnectionId,
|
||||
Latitude,
|
||||
Longitude,
|
||||
VoicePermissions,
|
||||
TokenNonce
|
||||
),
|
||||
Decision,
|
||||
CountryCode
|
||||
),
|
||||
log_token_start(GuildId, ChannelId, UserId, ConnectionId, TokenNonce),
|
||||
case rpc_client:call(Req) of
|
||||
@@ -58,8 +106,16 @@ token_start_log_message() ->
|
||||
"guild_voice_request_token_start: guild_id=~p channel_id=~p "
|
||||
"user_id=~p connection_id=~p token_nonce=~p".
|
||||
|
||||
-spec handle_token_success(integer(), integer(), integer(), map()) -> {ok, map()}.
|
||||
-spec handle_token_success(integer(), integer(), integer(), map()) ->
|
||||
{ok, map() | voice_p2p:token_grant()}.
|
||||
handle_token_success(GuildId, ChannelId, UserId, Data) ->
|
||||
case voice_p2p:grant(Data) of
|
||||
sfu -> handle_sfu_token_success(GuildId, ChannelId, UserId, Data);
|
||||
Grant -> {ok, Grant}
|
||||
end.
|
||||
|
||||
-spec handle_sfu_token_success(integer(), integer(), integer(), map()) -> {ok, map()}.
|
||||
handle_sfu_token_success(GuildId, ChannelId, UserId, Data) ->
|
||||
logger:debug(
|
||||
token_success_log_message(),
|
||||
[
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
normalize_session_id/1,
|
||||
normalize_optional_binary/1,
|
||||
maybe_error_reply/5,
|
||||
p2p_agreed/1,
|
||||
clear_virtual_access_flags/2
|
||||
]).
|
||||
|
||||
@@ -61,9 +62,15 @@ build_context(Request0) ->
|
||||
latitude => Coord(maps:get(latitude, Request, undefined)),
|
||||
longitude => Coord(maps:get(longitude, Request, undefined)),
|
||||
e2ee_capable => Norm(maps:get(e2ee_capable, Request, false)),
|
||||
bot => Norm(maps:get(bot, Request, false))
|
||||
bot => Norm(maps:get(bot, Request, false)),
|
||||
p2p => maps:get(p2p, Request, undefined),
|
||||
country_code => maps:get(country_code, Request, undefined)
|
||||
}.
|
||||
|
||||
-spec p2p_agreed(context()) -> boolean().
|
||||
p2p_agreed(Context) ->
|
||||
voice_p2p:agreed(maps:get(p2p, Context, undefined), maps:get(bot, Context, false)).
|
||||
|
||||
-spec voice_flags_for_permissions(context(), voice_utils:voice_permissions()) -> voice_flags().
|
||||
voice_flags_for_permissions(Context, VoicePermissions) ->
|
||||
voice_utils:apply_voice_permissions_to_flags(
|
||||
|
||||
@@ -160,10 +160,17 @@ check_perms_and_execute(Conns, ChannelId, UserId, ModId, VoiceStates, State) ->
|
||||
ModPerms = guild_permissions:get_member_permissions(ModId, ChannelId, State),
|
||||
ModHasConnect = permission_bits:has(ModPerms, ConnectPerm),
|
||||
ModHasView = permission_bits:has(ModPerms, ViewPerm),
|
||||
case ModHasConnect andalso ModHasView of
|
||||
false ->
|
||||
case
|
||||
{
|
||||
ModHasConnect andalso ModHasView,
|
||||
voice_p2p:guild_join_decision(false, ChannelId, State)
|
||||
}
|
||||
of
|
||||
{false, _Decision} ->
|
||||
{reply, gateway_errors:error(voice_moderator_missing_connect), State};
|
||||
true ->
|
||||
{true, {reject, ErrorAtom}} ->
|
||||
{reply, gateway_errors:error(ErrorAtom), State};
|
||||
{true, _Decision} ->
|
||||
execute_move(Conns, ChannelId, UserId, VoiceStates, State)
|
||||
end.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
-export([switch_voice_region_handler/2]).
|
||||
-export([switch_voice_region/3]).
|
||||
-export([switch_to_sfu/2]).
|
||||
|
||||
-export_type([
|
||||
guild_state/0,
|
||||
@@ -28,16 +29,60 @@ switch_voice_region_handler(Request, State) ->
|
||||
undefined ->
|
||||
{reply, gateway_errors:error(voice_channel_not_found), State};
|
||||
_ ->
|
||||
switch_voice_region_channel_reply(Channel, State)
|
||||
switch_voice_region_channel_reply(Channel, ChannelId, State)
|
||||
end.
|
||||
|
||||
-spec switch_voice_region_channel_reply(map(), guild_state()) -> guild_reply(map()).
|
||||
switch_voice_region_channel_reply(Channel, State) ->
|
||||
-spec switch_voice_region_channel_reply(map(), integer(), guild_state()) -> guild_reply(map()).
|
||||
switch_voice_region_channel_reply(Channel, ChannelId, State) ->
|
||||
case map_utils:get_integer(Channel, <<"type">>, undefined) of
|
||||
2 -> {reply, #{success => true}, State};
|
||||
2 -> {reply, #{success => true}, convert_to_sfu(Channel, ChannelId, State)};
|
||||
_ -> {reply, gateway_errors:error(voice_channel_not_voice), State}
|
||||
end.
|
||||
|
||||
-spec switch_to_sfu(integer(), guild_state()) -> guild_state().
|
||||
switch_to_sfu(ChannelId, State) ->
|
||||
Channel = guild_voice_member:find_channel_by_id(ChannelId, State),
|
||||
NewState = convert_to_sfu(Channel, ChannelId, State),
|
||||
ok = spawn_switch_if_converted(ChannelId, State, NewState),
|
||||
NewState.
|
||||
|
||||
-spec spawn_switch_if_converted(integer(), guild_state(), guild_state()) -> ok.
|
||||
spawn_switch_if_converted(_ChannelId, State, State) ->
|
||||
ok;
|
||||
spawn_switch_if_converted(ChannelId, _State, #{id := GuildId} = NewState) ->
|
||||
GuildPid = maps:get(guild_pid, NewState, self()),
|
||||
spawn(fun() -> switch_voice_region(GuildId, ChannelId, GuildPid) end),
|
||||
ok.
|
||||
|
||||
-spec convert_to_sfu(map() | undefined, integer(), guild_state()) -> guild_state().
|
||||
convert_to_sfu(Channel, ChannelId, State) ->
|
||||
VoiceStates = voice_state_utils:voice_states(State),
|
||||
P2pVoiceStates = maps:filter(
|
||||
fun(_ConnectionId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end,
|
||||
voice_state_utils:channel_voice_states(ChannelId, VoiceStates)
|
||||
),
|
||||
case voice_p2p:pinned(Channel) orelse maps:size(P2pVoiceStates) =:= 0 of
|
||||
true -> State;
|
||||
false -> mark_voice_states_sfu(P2pVoiceStates, VoiceStates, State)
|
||||
end.
|
||||
|
||||
-spec mark_voice_states_sfu(map(), map(), guild_state()) -> guild_state().
|
||||
mark_voice_states_sfu(P2pVoiceStates, VoiceStates, State) ->
|
||||
Converted = maps:map(
|
||||
fun(_ConnectionId, VoiceState) -> voice_p2p:to_sfu(VoiceState) end,
|
||||
P2pVoiceStates
|
||||
),
|
||||
NewState = State#{voice_states => maps:merge(VoiceStates, Converted)},
|
||||
maps:foreach(
|
||||
fun(_ConnectionId, VoiceState) ->
|
||||
guild_voice_broadcast:broadcast_voice_state_update(
|
||||
VoiceState, NewState, maps:get(<<"channel_id">>, VoiceState, null)
|
||||
)
|
||||
end,
|
||||
Converted
|
||||
),
|
||||
NewState.
|
||||
|
||||
-spec switch_voice_region(integer(), integer(), pid()) -> ok.
|
||||
switch_voice_region(GuildId, ChannelId, GuildPid) ->
|
||||
case guild_voice_server_state:guild_state_call(GuildPid, 10000) of
|
||||
@@ -103,10 +148,12 @@ collect_user_in_channel(ConnectionId, VoiceState, ChannelId, Acc) ->
|
||||
|
||||
-spec collect_user_voice_state(binary(), voice_state(), list()) -> list().
|
||||
collect_user_voice_state(ConnectionId, VoiceState, Acc) ->
|
||||
case voice_state_utils:voice_state_user_id(VoiceState) of
|
||||
undefined ->
|
||||
case {voice_p2p:is_p2p(VoiceState), voice_state_utils:voice_state_user_id(VoiceState)} of
|
||||
{true, _UserId} ->
|
||||
Acc;
|
||||
UserId ->
|
||||
{false, undefined} ->
|
||||
Acc;
|
||||
{false, UserId} ->
|
||||
SessionId = maps:get(<<"session_id">>, VoiceState, undefined),
|
||||
[{UserId, SessionId, ConnectionId, VoiceState} | Acc]
|
||||
end.
|
||||
|
||||
@@ -124,6 +124,7 @@ init(#{guild_id := GuildId, guild_pid := GuildPid} = Args) ->
|
||||
guild_voice_server_sync:ensure_registry(),
|
||||
ets:insert(?REGISTRY_TABLE, {GuildId, self()}),
|
||||
erlang:send_after(?SWEEP_INTERVAL_MS, self(), sweep_pending_joins),
|
||||
erlang:send_after(?SEEDED_SESSION_CHECK_DELAY_MS, self(), check_p2p_sessions),
|
||||
InitialVoiceStates = voice_state_utils:ensure_voice_states(
|
||||
maps:get(initial_voice_states, Args, #{})
|
||||
),
|
||||
@@ -254,6 +255,12 @@ handle_info(sweep_pending_joins, State) ->
|
||||
NewGuildState = guild_voice_connection:sweep_expired_pending_joins(GuildState),
|
||||
{noreply, guild_voice_server_state:apply_guild_state(NewGuildState, State2)}
|
||||
end;
|
||||
handle_info(check_p2p_sessions, State) ->
|
||||
erlang:send_after(?SEEDED_SESSION_CHECK_DELAY_MS, self(), check_p2p_sessions),
|
||||
ok = spawn_seeded_session_check(
|
||||
first, seeded_sessions(p2p_connection_ids(State), State), State
|
||||
),
|
||||
{noreply, State};
|
||||
handle_info({check_seeded_sessions, Round, ConnectionIds}, State) ->
|
||||
ok = spawn_seeded_session_check(Round, seeded_sessions(ConnectionIds, State), State),
|
||||
{noreply, State};
|
||||
@@ -313,6 +320,15 @@ schedule_seeded_session_check(Round, ConnectionIds) ->
|
||||
),
|
||||
ok.
|
||||
|
||||
-spec p2p_connection_ids(server_state()) -> [binary()].
|
||||
p2p_connection_ids(State) ->
|
||||
maps:keys(
|
||||
maps:filter(
|
||||
fun(_ConnectionId, VoiceState) -> voice_p2p:is_p2p(VoiceState) end,
|
||||
maps:get(voice_states, State, #{})
|
||||
)
|
||||
).
|
||||
|
||||
-spec seeded_sessions([binary()], server_state()) -> #{binary() => {integer(), binary()}}.
|
||||
seeded_sessions(ConnectionIds, State) ->
|
||||
VoiceStates = maps:get(voice_states, State, #{}),
|
||||
@@ -575,6 +591,24 @@ seeded_session_check_reports_only_gone_sessions_test() ->
|
||||
after 1000 -> ?assert(false)
|
||||
end.
|
||||
|
||||
p2p_session_check_reports_only_gone_p2p_sessions_test() ->
|
||||
State0 = seeded_test_state(self()),
|
||||
#{<<"ghost">> := Ghost, <<"live">> := Live} = VoiceStates = maps:get(voice_states, State0),
|
||||
State = State0#{
|
||||
voice_states => VoiceStates#{
|
||||
<<"ghost">> => Ghost#{<<"p2p">> => true},
|
||||
<<"live">> => Live#{<<"p2p">> => true},
|
||||
<<"sfu-ghost">> => Ghost#{<<"connection_id">> => <<"sfu-ghost">>}
|
||||
},
|
||||
test_session_gone_fun => fun(SessionId) -> SessionId =:= <<"dead">> end
|
||||
},
|
||||
{noreply, State} = handle_info(check_p2p_sessions, State),
|
||||
receive
|
||||
{seeded_sessions_gone, first, Gone} ->
|
||||
?assertEqual(#{<<"ghost">> => {5, <<"dead">>}}, Gone)
|
||||
after 1000 -> ?assert(false)
|
||||
end.
|
||||
|
||||
gone_seeded_connection_is_removed_test() ->
|
||||
TestFun = fun(_, _, _, _) -> {ok, #{success => true}} end,
|
||||
GuildPid = spawn(fun() -> guild_state_reply_loop(TestFun) end),
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(voice_p2p).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
agreed/2,
|
||||
declined/1,
|
||||
is_p2p/1,
|
||||
pinned/1,
|
||||
channel_mode/1,
|
||||
join_decision/4,
|
||||
guild_join_decision/3,
|
||||
admit/2,
|
||||
invariant_violation/1,
|
||||
invariant_violation/2,
|
||||
reports_rejection/1,
|
||||
to_sfu/1,
|
||||
add_to_token_request/3,
|
||||
grant/1,
|
||||
voice_server_update/4,
|
||||
signal_route/3,
|
||||
signal_payload/4
|
||||
]).
|
||||
|
||||
-export_type([
|
||||
voice_state/0, channel_mode/0, rejection/0, join_decision/0, grant/0, token_grant/0
|
||||
]).
|
||||
|
||||
-define(MAX_PARTICIPANTS, 4).
|
||||
|
||||
-type voice_state() :: map().
|
||||
-type channel_mode() :: empty | p2p | sfu | mixed.
|
||||
-type rejection() :: {reject, atom()}.
|
||||
-type join_decision() :: {p2p, initiator | joiner, pos_integer()} | sfu | rejection().
|
||||
-type grant() :: {p2p, binary(), [map()], pos_integer()}.
|
||||
-type token_grant() :: grant() | {declined, voice_channel_full}.
|
||||
|
||||
-spec agreed(term(), term()) -> boolean().
|
||||
agreed(true, true) -> false;
|
||||
agreed(true, _Bot) -> true;
|
||||
agreed(_P2p, _Bot) -> false.
|
||||
|
||||
-spec declined(term()) -> boolean().
|
||||
declined(false) -> true;
|
||||
declined(_) -> false.
|
||||
|
||||
-spec is_p2p(voice_state()) -> boolean().
|
||||
is_p2p(#{<<"p2p">> := true}) -> true;
|
||||
is_p2p(_VoiceState) -> false.
|
||||
|
||||
-spec pinned(map() | undefined) -> boolean().
|
||||
pinned(#{<<"rtc_p2p">> := true}) -> true;
|
||||
pinned(_Channel) -> false.
|
||||
|
||||
-spec channel_mode([voice_state()]) -> channel_mode().
|
||||
channel_mode([]) ->
|
||||
empty;
|
||||
channel_mode(VoiceStates) ->
|
||||
case {lists:all(fun is_p2p/1, VoiceStates), lists:any(fun is_p2p/1, VoiceStates)} of
|
||||
{true, _} -> p2p;
|
||||
{false, true} -> mixed;
|
||||
{false, false} -> sfu
|
||||
end.
|
||||
|
||||
-spec join_decision(boolean(), [voice_state()], boolean(), boolean()) -> join_decision().
|
||||
join_decision(Agreed, VoiceStates, HasPendingJoins, Pinned) ->
|
||||
case {channel_mode(VoiceStates), HasPendingJoins} of
|
||||
{p2p, _} -> join_p2p_decision(Agreed, length(VoiceStates) + 1);
|
||||
{empty, false} -> start_decision(Agreed, Pinned);
|
||||
_ -> sfu
|
||||
end.
|
||||
|
||||
-spec join_p2p_decision(boolean(), pos_integer()) -> join_decision().
|
||||
join_p2p_decision(false, _ParticipantCount) ->
|
||||
{reject, voice_p2p_consent_required};
|
||||
join_p2p_decision(true, ParticipantCount) when ParticipantCount > ?MAX_PARTICIPANTS ->
|
||||
{reject, voice_channel_full};
|
||||
join_p2p_decision(true, ParticipantCount) ->
|
||||
{p2p, joiner, ParticipantCount}.
|
||||
|
||||
-spec start_decision(boolean(), boolean()) -> join_decision().
|
||||
start_decision(true, _Pinned) -> {p2p, initiator, 1};
|
||||
start_decision(false, _Pinned) -> sfu.
|
||||
|
||||
-spec guild_join_decision(boolean(), integer(), map()) -> join_decision().
|
||||
guild_join_decision(Agreed, ChannelId, State) ->
|
||||
ChannelVoiceStates = maps:values(
|
||||
voice_state_utils:channel_voice_states(ChannelId, voice_state_utils:voice_states(State))
|
||||
),
|
||||
HasPendingJoins = lists:any(
|
||||
fun(Pending) -> maps:get(channel_id, Pending, undefined) =:= ChannelId end,
|
||||
maps:values(guild_voice_connection_pending:pending_voice_connections(State))
|
||||
),
|
||||
Pinned = pinned(guild_voice_member:find_channel_by_id(ChannelId, State)),
|
||||
join_decision(Agreed, ChannelVoiceStates, HasPendingJoins, Pinned).
|
||||
|
||||
-spec admit(join_decision(), token_grant() | term()) -> grant() | sfu | rejection().
|
||||
admit({p2p, _Role, _Count}, {p2p, _ConnectionId, _IceServers, _MaxParticipants} = Grant) ->
|
||||
Grant;
|
||||
admit({p2p, _Role, _Count}, {declined, ErrorAtom}) ->
|
||||
{reject, ErrorAtom};
|
||||
admit({p2p, initiator, _Count}, _Token) ->
|
||||
sfu;
|
||||
admit({p2p, joiner, _Count}, _Token) ->
|
||||
{reject, voice_p2p_unavailable};
|
||||
admit(sfu, {p2p, _ConnectionId, _IceServers, _MaxParticipants}) ->
|
||||
{reject, voice_token_failed};
|
||||
admit(sfu, {declined, _ErrorAtom}) ->
|
||||
{reject, voice_token_failed};
|
||||
admit(sfu, _Token) ->
|
||||
sfu.
|
||||
|
||||
-spec invariant_violation([voice_state()]) -> ok | {error, atom()}.
|
||||
invariant_violation(VoiceStates) ->
|
||||
invariant_violation(VoiceStates, undefined).
|
||||
|
||||
-spec invariant_violation([voice_state()], term()) -> ok | {error, atom()}.
|
||||
invariant_violation(VoiceStates, MaxParticipants) ->
|
||||
case channel_mode(VoiceStates) of
|
||||
mixed ->
|
||||
{error, voice_p2p_consent_required};
|
||||
p2p ->
|
||||
case length(VoiceStates) > participant_cap(MaxParticipants) of
|
||||
true -> {error, voice_channel_full};
|
||||
false -> ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec participant_cap(term()) -> pos_integer().
|
||||
participant_cap(MaxParticipants) when
|
||||
is_integer(MaxParticipants), MaxParticipants >= 1, MaxParticipants =< ?MAX_PARTICIPANTS
|
||||
->
|
||||
MaxParticipants;
|
||||
participant_cap(_MaxParticipants) ->
|
||||
?MAX_PARTICIPANTS.
|
||||
|
||||
-spec reports_rejection(term()) -> boolean().
|
||||
reports_rejection(voice_p2p_consent_required) -> true;
|
||||
reports_rejection(voice_p2p_unavailable) -> true;
|
||||
reports_rejection(voice_channel_full) -> true;
|
||||
reports_rejection(_) -> false.
|
||||
|
||||
-spec to_sfu(voice_state()) -> voice_state().
|
||||
to_sfu(VoiceState) ->
|
||||
VoiceState#{
|
||||
<<"p2p">> => false,
|
||||
<<"version">> => voice_state_utils:voice_state_version(VoiceState) + 1
|
||||
}.
|
||||
|
||||
-spec add_to_token_request(map(), join_decision(), binary() | undefined) -> map().
|
||||
add_to_token_request(Request, {p2p, Role, ParticipantCount}, CountryCode) ->
|
||||
WithP2p = Request#{
|
||||
<<"p2p">> => true,
|
||||
<<"p2p_initiator">> => Role =:= initiator,
|
||||
<<"p2p_participant_count">> => ParticipantCount
|
||||
},
|
||||
case CountryCode of
|
||||
Code when is_binary(Code) -> WithP2p#{<<"country_code">> => Code};
|
||||
_ -> WithP2p
|
||||
end;
|
||||
add_to_token_request(Request, _Decision, _CountryCode) ->
|
||||
Request.
|
||||
|
||||
-spec grant(map()) -> token_grant() | sfu.
|
||||
grant(
|
||||
#{<<"p2p">> := true, <<"connectionId">> := ConnectionId, <<"iceServers">> := IceServers} =
|
||||
Data
|
||||
) when
|
||||
is_binary(ConnectionId), is_list(IceServers)
|
||||
->
|
||||
{p2p, ConnectionId,
|
||||
[
|
||||
maps:with([<<"urls">>], Server)
|
||||
|| Server <- IceServers, is_map(Server)
|
||||
],
|
||||
participant_cap(maps:get(<<"maxParticipants">>, Data, undefined))};
|
||||
grant(#{<<"p2p">> := false, <<"declineReason">> := <<"channel_full">>}) ->
|
||||
{declined, voice_channel_full};
|
||||
grant(_Data) ->
|
||||
sfu.
|
||||
|
||||
-spec voice_server_update(binary(), integer(), integer() | null, [map()]) -> map().
|
||||
voice_server_update(ConnectionId, ChannelId, GuildId, IceServers) ->
|
||||
Update = #{
|
||||
<<"p2p">> => true,
|
||||
<<"ice_servers">> => IceServers,
|
||||
<<"connection_id">> => ConnectionId,
|
||||
<<"channel_id">> => integer_to_binary(ChannelId)
|
||||
},
|
||||
put_guild_id(Update, GuildId).
|
||||
|
||||
-spec signal_route([voice_state()], binary(), binary()) ->
|
||||
{ok, voice_state(), voice_state()} | error.
|
||||
signal_route(VoiceStates, SessionId, To) ->
|
||||
Peers = [VoiceState || VoiceState <- VoiceStates, is_p2p(VoiceState)],
|
||||
Senders = [
|
||||
Peer
|
||||
|| Peer <- Peers, maps:get(<<"session_id">>, Peer, undefined) =:= SessionId
|
||||
],
|
||||
Targets = [Peer || Peer <- Peers, maps:get(<<"connection_id">>, Peer, undefined) =:= To],
|
||||
case {Senders, Targets} of
|
||||
{[Sender | _], [Target | _]} when Sender =/= Target -> {ok, Sender, Target};
|
||||
_ -> error
|
||||
end.
|
||||
|
||||
-spec signal_payload(integer() | null, integer(), voice_state(), map()) -> map().
|
||||
signal_payload(GuildId, ChannelId, Sender, Data) ->
|
||||
Payload = #{
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"from">> => maps:get(<<"connection_id">>, Sender),
|
||||
<<"user_id">> => maps:get(<<"user_id">>, Sender),
|
||||
<<"data">> => Data
|
||||
},
|
||||
put_guild_id(Payload, GuildId).
|
||||
|
||||
-spec put_guild_id(map(), integer() | null) -> map().
|
||||
put_guild_id(Payload, null) -> Payload;
|
||||
put_guild_id(Payload, GuildId) -> Payload#{<<"guild_id">> => integer_to_binary(GuildId)}.
|
||||
@@ -12,6 +12,7 @@
|
||||
voice_state_channel_id/1,
|
||||
voice_state_guild_id/1,
|
||||
voice_state_version/1,
|
||||
channel_voice_states/2,
|
||||
initial_voice_state_version/0,
|
||||
complete_voice_state/1,
|
||||
external_voice_state/1,
|
||||
@@ -81,6 +82,13 @@ voice_state_channel_id(VoiceState) ->
|
||||
voice_state_guild_id(VoiceState) ->
|
||||
snowflake_field(VoiceState, <<"guild_id">>).
|
||||
|
||||
-spec channel_voice_states(integer() | undefined, voice_state_map()) -> voice_state_map().
|
||||
channel_voice_states(ChannelId, VoiceStates) ->
|
||||
maps:filter(
|
||||
fun(_ConnId, VoiceState) -> voice_state_channel_id(VoiceState) =:= ChannelId end,
|
||||
VoiceStates
|
||||
).
|
||||
|
||||
-spec voice_state_version(voice_state()) -> non_neg_integer().
|
||||
voice_state_version(VoiceState) ->
|
||||
case map_utils:get_integer(VoiceState, <<"version">>, undefined) of
|
||||
@@ -117,6 +125,7 @@ default_voice_state_fields() ->
|
||||
<<"suppress">> => false,
|
||||
<<"viewer_stream_keys">> => [],
|
||||
<<"e2ee_capable">> => false,
|
||||
<<"p2p">> => false,
|
||||
<<"region_id">> => null,
|
||||
<<"server_id">> => null,
|
||||
<<"version">> => ?INITIAL_VOICE_STATE_VERSION
|
||||
@@ -389,6 +398,7 @@ complete_voice_state_fills_missing_fields_test() ->
|
||||
?assertEqual(false, maps:get(<<"suppress">>, Completed)),
|
||||
?assertEqual([], maps:get(<<"viewer_stream_keys">>, Completed)),
|
||||
?assertEqual(false, maps:get(<<"e2ee_capable">>, Completed)),
|
||||
?assertEqual(false, maps:get(<<"p2p">>, Completed)),
|
||||
?assertEqual(null, maps:get(<<"region_id">>, Completed)),
|
||||
?assertEqual(null, maps:get(<<"server_id">>, Completed)),
|
||||
?assertEqual(0, maps:get(<<"version">>, Completed)).
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
bot => boolean(),
|
||||
shard => gateway_sharding:shard() | undefined,
|
||||
e2ee_capable => boolean(),
|
||||
geoip_country_code => binary() | undefined,
|
||||
ignored_events => #{binary() => true},
|
||||
initial_guild_id => guild_id() | undefined,
|
||||
collected_guild_states => [map()],
|
||||
@@ -146,6 +147,12 @@ handle_cast({dispatch, Event, Data}, State) when
|
||||
session_dispatch:handle_dispatch(Event, Data, State);
|
||||
handle_cast({initial_global_presences, Presences}, State) ->
|
||||
handle_cast_presences(Presences, State);
|
||||
handle_cast({voice_rejected, ErrorAtom}, State) when is_atom(ErrorAtom) ->
|
||||
ok = session_voice:report_rejection(ErrorAtom, State),
|
||||
{noreply, State};
|
||||
handle_cast({voice_signal, Data}, State) when is_map(Data) ->
|
||||
ok = session_voice:handle_voice_signal(Data, State),
|
||||
{noreply, State};
|
||||
handle_cast(Msg, State) ->
|
||||
handle_cast_guild_or_lifecycle(Msg, State).
|
||||
|
||||
|
||||
@@ -246,7 +246,8 @@ extract_core_fields(
|
||||
bot => Bot,
|
||||
shard => maps:get(shard, D, undefined),
|
||||
is_staff => IsStaff,
|
||||
e2ee_capable => maps:get(e2ee_capable, D, false)
|
||||
e2ee_capable => maps:get(e2ee_capable, D, false),
|
||||
geoip_country_code => maps:get(geoip_country_code, D, undefined)
|
||||
}.
|
||||
|
||||
-spec extract_extra_fields(map(), map() | undefined) -> map().
|
||||
|
||||
@@ -604,6 +604,7 @@ serialize_state(State) ->
|
||||
shard => maps:get(shard, State, undefined),
|
||||
e2ee_capable => maps:get(e2ee_capable, State, false),
|
||||
thread_channels_capable => maps:get(thread_channels_capable, State, false),
|
||||
geoip_country_code => maps:get(geoip_country_code, State, undefined),
|
||||
guilds => maps:get(guilds, State, #{}),
|
||||
active_guilds => maps:get(active_guilds, State, sets:new()),
|
||||
collected_guild_states => maps:get(collected_guild_states, State),
|
||||
@@ -638,6 +639,7 @@ serialize_transfer_identity(State) ->
|
||||
bot => maps:get(bot, State, false),
|
||||
shard => maps:get(shard, State, undefined),
|
||||
e2ee_capable => maps:get(e2ee_capable, State, false),
|
||||
geoip_country_code => maps:get(geoip_country_code, State, undefined),
|
||||
ignored_events => maps:keys(maps:get(ignored_events, State, #{})),
|
||||
initial_guild_id => maps:get(initial_guild_id, State, undefined),
|
||||
active_guilds => maps:get(active_guilds, State, sets:new()),
|
||||
|
||||
@@ -199,6 +199,7 @@ build_session_data(Data, IdentifyData, Version, SocketPid, SessionId, UserDataMa
|
||||
),
|
||||
bot => map_utils:get_safe(UserDataMap, <<"bot">>, false),
|
||||
e2ee_capable => extract_e2ee_capable(Properties),
|
||||
geoip_country_code => extract_geoip_country_code(Data),
|
||||
ignored_events => term_detach:detach(IgnoredEvents),
|
||||
initial_guild_id => map_utils:get_safe(IdentifyData, initial_guild_id, undefined),
|
||||
shard => Shard,
|
||||
@@ -386,6 +387,13 @@ extract_e2ee_capable(Properties) when is_map(Properties) ->
|
||||
extract_e2ee_capable(_) ->
|
||||
false.
|
||||
|
||||
-spec extract_geoip_country_code(map()) -> binary() | undefined.
|
||||
extract_geoip_country_code(Data) ->
|
||||
case maps:get(<<"geoip_country_code">>, Data, null) of
|
||||
CountryCode when is_binary(CountryCode), byte_size(CountryCode) > 0 -> CountryCode;
|
||||
_ -> undefined
|
||||
end.
|
||||
|
||||
-spec fetch_rpc_data(map(), term()) -> {ok, map()} | {error, term()}.
|
||||
fetch_rpc_data(Request, PeerIP) ->
|
||||
StartTime = erlang:system_time(millisecond),
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
init_voice_queue/0,
|
||||
process_voice_queue/1,
|
||||
handle_voice_state_update/2,
|
||||
report_rejection/2,
|
||||
handle_voice_signal/2,
|
||||
handle_voice_disconnect/1
|
||||
]).
|
||||
|
||||
@@ -37,7 +39,7 @@ process_voice_queue_item(Item, State) ->
|
||||
case maps:get(type, Item, undefined) of
|
||||
voice_state_update ->
|
||||
Data = maps:get(data, Item),
|
||||
{reply, _, NewState} = session_voice_connect:handle_voice_state_update(Data, State),
|
||||
{reply, _, NewState} = handle_voice_state_update(Data, State),
|
||||
NewState;
|
||||
_ ->
|
||||
State
|
||||
@@ -45,7 +47,64 @@ process_voice_queue_item(Item, State) ->
|
||||
|
||||
-spec handle_voice_state_update(map(), session_state()) -> voice_state_reply().
|
||||
handle_voice_state_update(Data, State) ->
|
||||
session_voice_connect:handle_voice_state_update(Data, State).
|
||||
Result = session_voice_connect:handle_voice_state_update(Data, State),
|
||||
ok = report_rejected_reply(Result, State),
|
||||
Result.
|
||||
|
||||
-spec report_rejected_reply(voice_state_reply(), session_state()) -> ok.
|
||||
report_rejected_reply({reply, {error, _Category, ErrorAtom}, _NewState}, State) ->
|
||||
report_rejection(ErrorAtom, State);
|
||||
report_rejected_reply(_Result, _State) ->
|
||||
ok.
|
||||
|
||||
-spec report_rejection(term(), session_state()) -> ok.
|
||||
report_rejection(ErrorAtom, State) ->
|
||||
case {voice_p2p:reports_rejection(ErrorAtom), maps:get(socket_pid, State, undefined)} of
|
||||
{true, SocketPid} when is_pid(SocketPid) ->
|
||||
SocketPid ! {gateway_error, ErrorAtom},
|
||||
ok;
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec handle_voice_signal(map(), session_state()) -> ok.
|
||||
handle_voice_signal(Data, State) ->
|
||||
GuildIdResult = validation:validate_optional_snowflake(
|
||||
maps:get(<<"guild_id">>, Data, null)
|
||||
),
|
||||
ChannelIdResult = validation:validate_snowflake(maps:get(<<"channel_id">>, Data, null)),
|
||||
To = maps:get(<<"to">>, Data, undefined),
|
||||
Signal = maps:get(<<"data">>, Data, undefined),
|
||||
case {GuildIdResult, ChannelIdResult} of
|
||||
{{ok, GuildId}, {ok, ChannelId}} when is_binary(To), is_map(Signal) ->
|
||||
route_voice_signal(GuildId, ChannelId, To, Signal, State);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec route_voice_signal(integer() | null, integer(), binary(), map(), session_state()) -> ok.
|
||||
route_voice_signal(null, ChannelId, To, Signal, State) ->
|
||||
case maps:get(ChannelId, maps:get(calls, State, #{}), undefined) of
|
||||
{CallPid, _Ref} when is_pid(CallPid) ->
|
||||
gen_server:cast(CallPid, {voice_signal, maps:get(id, State), To, Signal});
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
route_voice_signal(GuildId, ChannelId, To, Signal, State) ->
|
||||
case maps:get(GuildId, maps:get(guilds, State, #{}), undefined) of
|
||||
{GuildPid, _Ref} when is_pid(GuildPid) ->
|
||||
gen_server:cast(
|
||||
GuildPid,
|
||||
{voice_signal, #{
|
||||
session_id => maps:get(id, State),
|
||||
channel_id => ChannelId,
|
||||
to => To,
|
||||
data => Signal
|
||||
}}
|
||||
);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec handle_voice_disconnect(session_state()) -> voice_state_reply().
|
||||
handle_voice_disconnect(State) ->
|
||||
@@ -88,6 +147,82 @@ init_voice_queue_test() ->
|
||||
?assert(queue:is_empty(maps:get(voice_queue, Result))),
|
||||
ok.
|
||||
|
||||
report_rejection_sends_p2p_rejections_to_the_socket_test() ->
|
||||
State = #{socket_pid => self()},
|
||||
ok = report_rejection(voice_p2p_consent_required, State),
|
||||
ok = report_rejection(voice_p2p_unavailable, State),
|
||||
ok = report_rejection(voice_channel_full, State),
|
||||
ok = report_rejection(voice_token_failed, State),
|
||||
ok = report_rejection(voice_channel_full, #{socket_pid => undefined}),
|
||||
?assertEqual(
|
||||
[voice_p2p_consent_required, voice_p2p_unavailable, voice_channel_full],
|
||||
drain_gateway_errors()
|
||||
).
|
||||
|
||||
drain_gateway_errors() ->
|
||||
receive
|
||||
{gateway_error, ErrorAtom} -> [ErrorAtom | drain_gateway_errors()]
|
||||
after 50 ->
|
||||
[]
|
||||
end.
|
||||
|
||||
voice_signal_routes_to_the_guild_test() ->
|
||||
State = #{id => <<"sess">>, guilds => #{42 => {self(), make_ref()}}, calls => #{}},
|
||||
Data = #{
|
||||
<<"guild_id">> => <<"42">>,
|
||||
<<"channel_id">> => <<"100">>,
|
||||
<<"to">> => <<"conn-b">>,
|
||||
<<"data">> => #{<<"type">> => <<"offer">>}
|
||||
},
|
||||
ok = handle_voice_signal(Data, State),
|
||||
receive
|
||||
{'$gen_cast', {voice_signal, Request}} ->
|
||||
?assertEqual(
|
||||
#{
|
||||
session_id => <<"sess">>,
|
||||
channel_id => 100,
|
||||
to => <<"conn-b">>,
|
||||
data => #{<<"type">> => <<"offer">>}
|
||||
},
|
||||
Request
|
||||
)
|
||||
after 1000 ->
|
||||
error(voice_signal_not_routed)
|
||||
end.
|
||||
|
||||
voice_signal_routes_to_the_call_test() ->
|
||||
State = #{id => <<"sess">>, guilds => #{}, calls => #{100 => {self(), make_ref()}}},
|
||||
Data = #{<<"channel_id">> => <<"100">>, <<"to">> => <<"conn-b">>, <<"data">> => #{}},
|
||||
ok = handle_voice_signal(Data, State),
|
||||
receive
|
||||
{'$gen_cast', {voice_signal, <<"sess">>, <<"conn-b">>, #{}}} -> ok
|
||||
after 1000 ->
|
||||
error(voice_signal_not_routed)
|
||||
end.
|
||||
|
||||
voice_signal_drops_unroutable_frames_test() ->
|
||||
State = #{id => <<"sess">>, guilds => #{42 => {self(), make_ref()}}, calls => #{}},
|
||||
Valid = #{
|
||||
<<"guild_id">> => <<"42">>,
|
||||
<<"channel_id">> => <<"100">>,
|
||||
<<"to">> => <<"conn-b">>,
|
||||
<<"data">> => #{}
|
||||
},
|
||||
Dropped = [
|
||||
Valid#{<<"guild_id">> => <<"43">>},
|
||||
Valid#{<<"guild_id">> => null},
|
||||
Valid#{<<"channel_id">> => null},
|
||||
Valid#{<<"to">> => 5},
|
||||
Valid#{<<"data">> => <<"sdp">>},
|
||||
#{}
|
||||
],
|
||||
[?assertEqual(ok, handle_voice_signal(Data, State)) || Data <- Dropped],
|
||||
receive
|
||||
{'$gen_cast', Unexpected} -> error({unexpected_voice_signal, Unexpected})
|
||||
after 50 ->
|
||||
ok
|
||||
end.
|
||||
|
||||
process_voice_queue_empty_test() ->
|
||||
State = #{voice_queue => queue:new()},
|
||||
Result = process_voice_queue(State),
|
||||
|
||||
@@ -47,6 +47,7 @@ handle_voice_state_update(Data, State) ->
|
||||
user_id => UserId,
|
||||
e2ee_capable => E2EECapable,
|
||||
bot => Bot,
|
||||
country_code => maps:get(geoip_country_code, State, undefined),
|
||||
guilds => Guilds
|
||||
},
|
||||
dispatch_validated(GuildIdResult, ChannelIdResult, Ctx, State).
|
||||
@@ -64,7 +65,8 @@ extract_voice_params(Data) ->
|
||||
viewer_stream_keys => maps:get(<<"viewer_stream_keys">>, Data, undefined),
|
||||
is_mobile => maps:get(<<"is_mobile">>, Data, false),
|
||||
latitude => maps:get(<<"latitude">>, Data, null),
|
||||
longitude => maps:get(<<"longitude">>, Data, null)
|
||||
longitude => maps:get(<<"longitude">>, Data, null),
|
||||
p2p => maps:get(<<"p2p">>, Data, undefined)
|
||||
}.
|
||||
|
||||
-spec dispatch_validated(
|
||||
@@ -148,13 +150,14 @@ handle_dm_channel(ChId, Ctx, State) ->
|
||||
user_id := UserId,
|
||||
params := Params,
|
||||
e2ee_capable := E2EE,
|
||||
bot := Bot
|
||||
bot := Bot,
|
||||
country_code := CountryCode
|
||||
} = Ctx,
|
||||
ConnId = maps:get(connection_id, Params),
|
||||
case is_binary(ConnId) orelse ConnId =:= null of
|
||||
true ->
|
||||
handle_dm_connect(
|
||||
ChId, Params, SId, UserId, E2EE, Bot, State
|
||||
ChId, Params, SId, UserId, E2EE, Bot, CountryCode, State
|
||||
);
|
||||
false ->
|
||||
invalid_params_reply(UserId, SId, State)
|
||||
@@ -257,9 +260,10 @@ log_dm_disconnect_err(UserId, SId, ConnId, Cat, Err) ->
|
||||
user_id(),
|
||||
boolean(),
|
||||
boolean(),
|
||||
binary() | undefined,
|
||||
session_state()
|
||||
) -> voice_state_reply().
|
||||
handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, State) ->
|
||||
handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, CountryCode, State) ->
|
||||
ConnId = maps:get(connection_id, Params),
|
||||
Request = #{
|
||||
user_id => UserId,
|
||||
@@ -275,7 +279,9 @@ handle_dm_connect(ChId, Params, SId, UserId, E2EE, Bot, State) ->
|
||||
latitude => maps:get(latitude, Params),
|
||||
longitude => maps:get(longitude, Params),
|
||||
e2ee_capable => E2EE,
|
||||
bot => Bot
|
||||
bot => Bot,
|
||||
p2p => maps:get(p2p, Params),
|
||||
country_code => CountryCode
|
||||
},
|
||||
StWithPid = State#{session_pid => self()},
|
||||
Result = dm_voice:voice_state_update(Request, StWithPid),
|
||||
@@ -403,11 +409,12 @@ guild_voice_queue(GuildPid, GId, ChId, Ctx, State) ->
|
||||
user_id := UserId,
|
||||
params := Params,
|
||||
e2ee_capable := E2EE,
|
||||
bot := Bot
|
||||
bot := Bot,
|
||||
country_code := CountryCode
|
||||
} = Ctx,
|
||||
ConnId = maps:get(connection_id, Params),
|
||||
log_guild_info("guild_queue", UserId, SId, GId, ChId, ConnId),
|
||||
Req = build_guild_request(ChId, Params, UserId, SId, E2EE, Bot),
|
||||
Req = build_guild_request(ChId, Params, UserId, SId, E2EE, Bot, CountryCode),
|
||||
VoiceCtx = #{
|
||||
guild_pid => GuildPid,
|
||||
guild_id => GId,
|
||||
@@ -485,9 +492,10 @@ log_guild_warning(Tag, UserId, SId, GId, ChId, ConnId) ->
|
||||
user_id(),
|
||||
binary(),
|
||||
boolean(),
|
||||
boolean()
|
||||
boolean(),
|
||||
binary() | undefined
|
||||
) -> map().
|
||||
build_guild_request(ChId, Params, UserId, SId, E2EE, Bot) ->
|
||||
build_guild_request(ChId, Params, UserId, SId, E2EE, Bot, CountryCode) ->
|
||||
#{
|
||||
user_id => UserId,
|
||||
session_id => SId,
|
||||
@@ -502,5 +510,7 @@ build_guild_request(ChId, Params, UserId, SId, E2EE, Bot) ->
|
||||
latitude => maps:get(latitude, Params),
|
||||
longitude => maps:get(longitude, Params),
|
||||
e2ee_capable => E2EE,
|
||||
bot => Bot
|
||||
bot => Bot,
|
||||
p2p => maps:get(p2p, Params),
|
||||
country_code => CountryCode
|
||||
}.
|
||||
@@ -76,7 +76,8 @@ handle_guild_call_result({error, timeout}, Ctx, _SessionPid) ->
|
||||
log_voice_warning("voice_state_update_guild_call_timeout", Ctx);
|
||||
handle_guild_call_result({error, noproc}, Ctx, _SessionPid) ->
|
||||
log_voice_warning("voice_state_update_guild_call_noproc", Ctx);
|
||||
handle_guild_call_result({error, Cat, Err}, Ctx, _SessionPid) ->
|
||||
handle_guild_call_result({error, Cat, Err}, Ctx, SessionPid) ->
|
||||
gen_server:cast(SessionPid, {voice_rejected, Err}),
|
||||
log_voice_warning_extra(Ctx, " category=~p error=~p", [Cat, Err]);
|
||||
handle_guild_call_result({error, Reason}, Ctx, _SessionPid) ->
|
||||
log_voice_warning_extra(Ctx, " reason=~p", [Reason]).
|
||||
@@ -107,6 +108,15 @@ handle_guild_reply_ok(Reply, Ctx, SessionPid) ->
|
||||
-spec maybe_dispatch_voice_server_update(
|
||||
map(), guild_id(), channel_id() | null, pid()
|
||||
) -> ok.
|
||||
maybe_dispatch_voice_server_update(
|
||||
#{p2p := true, ice_servers := IceServers, connection_id := ConnId}, GId, ChId, SessionPid
|
||||
) when is_integer(ChId) ->
|
||||
dispatch_to_session(
|
||||
SessionPid,
|
||||
voice_server_update,
|
||||
voice_p2p:voice_server_update(ConnId, ChId, GId, IceServers),
|
||||
GId
|
||||
);
|
||||
maybe_dispatch_voice_server_update(Reply, GId, ChId, SessionPid) ->
|
||||
Token = maps:get(token, Reply, undefined),
|
||||
Endpoint = maps:get(endpoint, Reply, undefined),
|
||||
@@ -371,6 +381,37 @@ join_reply_dispatches_voice_server_update_test() ->
|
||||
?assertEqual(<<"conn-1">>, maps:get(<<"connection_id">>, Payload)),
|
||||
?assertNot(maps:is_key(<<"e2ee_key">>, Payload)).
|
||||
|
||||
guild_rejection_is_cast_to_the_session_test() ->
|
||||
ok = handle_guild_call_result(
|
||||
{error, permission_denied, voice_p2p_consent_required}, test_voice_ctx(456), self()
|
||||
),
|
||||
receive
|
||||
{'$gen_cast', {voice_rejected, voice_p2p_consent_required}} -> ok
|
||||
after 1000 ->
|
||||
error(voice_rejection_not_cast)
|
||||
end.
|
||||
|
||||
p2p_join_reply_dispatches_ice_servers_without_a_token_test() ->
|
||||
IceServers = [#{<<"urls">> => [<<"stun:stun.example:3478">>]}],
|
||||
Reply = #{
|
||||
success => true,
|
||||
p2p => true,
|
||||
ice_servers => IceServers,
|
||||
connection_id => <<"conn-1">>,
|
||||
voice_state => #{<<"channel_id">> => <<"456">>}
|
||||
},
|
||||
ok = handle_guild_reply_ok(Reply, test_voice_ctx(456), self()),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"p2p">> => true,
|
||||
<<"ice_servers">> => IceServers,
|
||||
<<"guild_id">> => <<"42">>,
|
||||
<<"channel_id">> => <<"456">>,
|
||||
<<"connection_id">> => <<"conn-1">>
|
||||
},
|
||||
receive_dispatch(voice_server_update)
|
||||
).
|
||||
|
||||
join_reply_attaches_e2ee_key_test() ->
|
||||
Reply = #{
|
||||
success => true,
|
||||
|
||||
@@ -47,6 +47,7 @@ gateway_opcode(9) -> invalid_session;
|
||||
gateway_opcode(10) -> hello;
|
||||
gateway_opcode(11) -> heartbeat_ack;
|
||||
gateway_opcode(12) -> gateway_error;
|
||||
gateway_opcode(13) -> voice_signal;
|
||||
gateway_opcode(14) -> lazy_request;
|
||||
gateway_opcode(15) -> request_guild_counts;
|
||||
gateway_opcode(16) -> request_channel_member_counts;
|
||||
@@ -67,6 +68,7 @@ opcode_to_num(invalid_session) -> 9;
|
||||
opcode_to_num(hello) -> 10;
|
||||
opcode_to_num(heartbeat_ack) -> 11;
|
||||
opcode_to_num(gateway_error) -> 12;
|
||||
opcode_to_num(voice_signal) -> 13;
|
||||
opcode_to_num(lazy_request) -> 14;
|
||||
opcode_to_num(request_guild_counts) -> 15;
|
||||
opcode_to_num(request_channel_member_counts) -> 16;
|
||||
@@ -108,7 +110,7 @@ status_type_atom(invisible) -> <<"invisible">>;
|
||||
status_type_atom(offline) -> <<"offline">>.
|
||||
|
||||
-spec max_payload_size() -> pos_integer().
|
||||
max_payload_size() -> 4096.
|
||||
max_payload_size() -> 16384.
|
||||
|
||||
-spec heartbeat_interval() -> pos_integer().
|
||||
heartbeat_interval() -> 41250.
|
||||
@@ -210,7 +212,7 @@ status_type_atom_atom_to_binary_test() ->
|
||||
?assertEqual(<<"idle">>, status_type_atom(idle)).
|
||||
|
||||
constants_values_test() ->
|
||||
?assertEqual(4096, max_payload_size()),
|
||||
?assertEqual(16384, max_payload_size()),
|
||||
?assertEqual(41250, heartbeat_interval()),
|
||||
?assertEqual(45000, heartbeat_timeout()),
|
||||
?assertEqual(60000, resume_timeout()),
|
||||
|
||||
Reference in new issue
Block a user