mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(push): ring Android calls and harden the relay (#2915)
This commit is contained in:
@@ -12,7 +12,10 @@
|
||||
message_id := integer(),
|
||||
region := binary() | undefined,
|
||||
ringing := [integer()],
|
||||
recipients := [integer()]
|
||||
recipients := [integer()],
|
||||
caller_id => integer() | undefined,
|
||||
caller_name => binary() | undefined,
|
||||
caller_avatar => binary() | undefined
|
||||
}.
|
||||
-type call_request() ::
|
||||
{get_state}
|
||||
@@ -28,7 +31,9 @@
|
||||
| {update_voice_state, integer(), map()}
|
||||
| {get_sessions}
|
||||
| {get_pending_connections}.
|
||||
-type cast_request() :: {join_async, integer(), map(), binary(), pid()}.
|
||||
-type cast_request() ::
|
||||
{join_async, integer(), map(), binary(), pid()}
|
||||
| {set_caller, map()}.
|
||||
-type info_message() ::
|
||||
{'DOWN', reference(), process, pid(), term()}
|
||||
| {ring_timeout, integer()}
|
||||
@@ -58,20 +63,25 @@ init(CallData) ->
|
||||
{ok, FinalState}.
|
||||
|
||||
-spec build_initial_state(call_data()) -> map().
|
||||
build_initial_state(#{
|
||||
channel_id := ChannelId,
|
||||
message_id := MessageId,
|
||||
region := Region,
|
||||
ringing := Ringing,
|
||||
recipients := Recipients
|
||||
}) ->
|
||||
build_initial_state(
|
||||
#{
|
||||
channel_id := ChannelId,
|
||||
message_id := MessageId,
|
||||
region := Region,
|
||||
ringing := Ringing,
|
||||
recipients := Recipients
|
||||
} = CallData
|
||||
) ->
|
||||
State = #{
|
||||
channel_id => ChannelId,
|
||||
message_id => MessageId,
|
||||
region => Region,
|
||||
ringing => [],
|
||||
pending_ringing => Ringing,
|
||||
recipients => Recipients,
|
||||
caller_id => undefined,
|
||||
caller_name => undefined,
|
||||
caller_avatar => undefined,
|
||||
voice_states => #{},
|
||||
sessions => #{},
|
||||
pending_connections => #{},
|
||||
@@ -81,7 +91,8 @@ build_initial_state(#{
|
||||
created_at => erlang:system_time(millisecond),
|
||||
participants_history => sets:new(),
|
||||
last_call_event => undefined
|
||||
}.
|
||||
},
|
||||
call_state:put_caller(CallData, State).
|
||||
|
||||
-spec run_init_pipeline(map()) -> map().
|
||||
run_init_pipeline(State) ->
|
||||
@@ -155,6 +166,8 @@ handle_cast(Request, State) ->
|
||||
case decode_cast_request(Request) of
|
||||
{ok, {join_async, UserId, VoiceState, SessionId, SessionPid}} ->
|
||||
call_voice:handle_join_async(UserId, VoiceState, SessionId, SessionPid, State);
|
||||
{ok, {set_caller, Caller}} ->
|
||||
{noreply, call_state:put_caller(Caller, State)};
|
||||
error ->
|
||||
{noreply, State}
|
||||
end.
|
||||
@@ -308,6 +321,8 @@ decode_cast_request({join_async, UserId, VoiceState, SessionId, SessionPid}) whe
|
||||
is_integer(UserId), is_map(VoiceState), is_binary(SessionId), is_pid(SessionPid)
|
||||
->
|
||||
{ok, {join_async, UserId, VoiceState, SessionId, SessionPid}};
|
||||
decode_cast_request({set_caller, Caller}) when is_map(Caller) ->
|
||||
{ok, {set_caller, Caller}};
|
||||
decode_cast_request(_) ->
|
||||
error.
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ export_state(State) ->
|
||||
ringing => maps:get(ringing, State, []),
|
||||
pending_ringing => maps:get(pending_ringing, State, []),
|
||||
recipients => maps:get(recipients, State, []),
|
||||
caller_id => maps:get(caller_id, State, undefined),
|
||||
caller_name => maps:get(caller_name, State, undefined),
|
||||
caller_avatar => maps:get(caller_avatar, State, undefined),
|
||||
voice_states => maps:get(voice_states, State, #{}),
|
||||
sessions => export_sessions(maps:get(sessions, State, #{})),
|
||||
pending_connections => maps:get(pending_connections, State, #{}),
|
||||
@@ -40,6 +43,9 @@ restore_state(TransferState) ->
|
||||
ringing => maps:get(ringing, TransferState, []),
|
||||
pending_ringing => maps:get(pending_ringing, TransferState, []),
|
||||
recipients => maps:get(recipients, TransferState, []),
|
||||
caller_id => maps:get(caller_id, TransferState, undefined),
|
||||
caller_name => maps:get(caller_name, TransferState, undefined),
|
||||
caller_avatar => maps:get(caller_avatar, TransferState, undefined),
|
||||
voice_states => VoiceStates,
|
||||
sessions => Sessions,
|
||||
pending_connections => maps:get(pending_connections, TransferState, #{}),
|
||||
@@ -192,4 +198,25 @@ restore_state_remonitors_live_sessions_test() ->
|
||||
?assertEqual(#{1 => VoiceState}, maps:get(voice_states, Restored)),
|
||||
SessionPid ! stop.
|
||||
|
||||
export_and_restore_round_trips_the_caller_test() ->
|
||||
State = #{
|
||||
channel_id => 123,
|
||||
message_id => 456,
|
||||
caller_id => 7,
|
||||
caller_name => <<"Ada">>,
|
||||
caller_avatar => <<"a1b2c3d4">>
|
||||
},
|
||||
Restored = restore_state(export_state(State)),
|
||||
?assertEqual(
|
||||
#{caller_id => 7, caller_name => <<"Ada">>, caller_avatar => <<"a1b2c3d4">>},
|
||||
call_state:caller_from_state(Restored)
|
||||
).
|
||||
|
||||
restore_state_reads_a_transfer_without_a_caller_as_undefined_test() ->
|
||||
Restored = restore_state(#{channel_id => 123, message_id => 456}),
|
||||
?assertEqual(
|
||||
#{caller_id => undefined, caller_name => undefined, caller_avatar => undefined},
|
||||
call_state:caller_from_state(Restored)
|
||||
).
|
||||
|
||||
-endif.
|
||||
|
||||
@@ -89,41 +89,51 @@ apply_pending_ringing(PendingUnique, State, DispatchUpdates) ->
|
||||
publish_rings([], _State) ->
|
||||
ok;
|
||||
publish_rings(UserIds, State) ->
|
||||
ChannelId = maps:get(channel_id, State),
|
||||
MessageId = maps:get(message_id, State),
|
||||
Recipients = maps:get(recipients, State),
|
||||
StartedAt = erlang:system_time(millisecond),
|
||||
ExpiresAt = StartedAt + ?RING_TIMEOUT_MS,
|
||||
_ = proc_lib:spawn(fun() ->
|
||||
publish_ring_jobs(UserIds, Recipients, ChannelId, MessageId, StartedAt, ExpiresAt)
|
||||
end),
|
||||
Ring = build_ring_context(State),
|
||||
_ = proc_lib:spawn(fun() -> publish_ring_jobs(UserIds, Recipients, Ring) end),
|
||||
ok.
|
||||
|
||||
-spec publish_ring_jobs(
|
||||
[integer()], [integer()], integer(), integer(), integer(), integer()
|
||||
) -> ok.
|
||||
publish_ring_jobs(UserIds, Recipients, ChannelId, MessageId, StartedAt, ExpiresAt) ->
|
||||
-spec build_ring_context(map()) -> map().
|
||||
build_ring_context(State) ->
|
||||
StartedAt = erlang:system_time(millisecond),
|
||||
#{
|
||||
channel_id => maps:get(channel_id, State),
|
||||
message_id => maps:get(message_id, State),
|
||||
started_at_ms => StartedAt,
|
||||
expires_at_ms => StartedAt + ?RING_TIMEOUT_MS,
|
||||
caller => call_state:caller_from_state(State)
|
||||
}.
|
||||
|
||||
-spec publish_ring_jobs([integer()], [integer()], map()) -> ok.
|
||||
publish_ring_jobs(UserIds, Recipients, Ring) ->
|
||||
lists:foreach(
|
||||
fun(UserId) ->
|
||||
publish_ring_job(UserId, Recipients, ChannelId, MessageId, StartedAt, ExpiresAt)
|
||||
end,
|
||||
fun(UserId) -> publish_ring_job(UserId, Recipients, Ring) end,
|
||||
UserIds
|
||||
).
|
||||
|
||||
-spec publish_ring_job(
|
||||
integer(), [integer()], integer(), integer(), integer(), integer()
|
||||
) -> ok.
|
||||
publish_ring_job(UserId, Recipients, ChannelId, MessageId, StartedAt, ExpiresAt) ->
|
||||
-spec publish_ring_job(integer(), [integer()], map()) -> ok.
|
||||
publish_ring_job(UserId, Recipients, Ring) ->
|
||||
case ring_suppressed(UserId, Recipients) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
_ = push_job_publisher:publish_ring(
|
||||
UserId, ChannelId, MessageId, StartedAt, ExpiresAt
|
||||
),
|
||||
ok
|
||||
dispatch_ring_job(UserId, Ring)
|
||||
end.
|
||||
|
||||
-spec dispatch_ring_job(integer(), map()) -> ok.
|
||||
dispatch_ring_job(UserId, #{
|
||||
channel_id := ChannelId,
|
||||
message_id := MessageId,
|
||||
started_at_ms := StartedAt,
|
||||
expires_at_ms := ExpiresAt,
|
||||
caller := Caller
|
||||
}) ->
|
||||
_ = push_job_publisher:publish_ring(
|
||||
UserId, ChannelId, MessageId, StartedAt, ExpiresAt, Caller
|
||||
),
|
||||
ok.
|
||||
|
||||
-spec ring_suppressed(integer(), [integer()]) -> boolean().
|
||||
ring_suppressed(UserId, Recipients) ->
|
||||
all_blocked(UserId, [Other || Other <- Recipients, Other =/= UserId]).
|
||||
@@ -382,11 +392,14 @@ published_ring_args() ->
|
||||
[Args || {_Pid, {push_job_publisher, publish_ring, Args}, _Result} <- History].
|
||||
|
||||
published_ring_users() ->
|
||||
lists:usort([UserId || [UserId, _C, _M, _S, _E] <- published_ring_args()]).
|
||||
lists:usort([UserId || [UserId, _C, _M, _S, _E, _Caller] <- published_ring_args()]).
|
||||
|
||||
published_ring_callers() ->
|
||||
[Caller || [_U, _C, _M, _S, _E, Caller] <- published_ring_args()].
|
||||
|
||||
mock_ring_publisher() ->
|
||||
ok = meck:new(push_job_publisher, [passthrough, no_link]),
|
||||
meck:expect(push_job_publisher, publish_ring, fun(_U, _C, _M, _S, _E) -> ok end).
|
||||
meck:expect(push_job_publisher, publish_ring, fun(_U, _C, _M, _S, _E, _Caller) -> ok end).
|
||||
|
||||
apply_pending_ringing_publishes_one_ring_per_new_user_test() ->
|
||||
ok = mock_ring_publisher(),
|
||||
@@ -405,7 +418,7 @@ apply_pending_ringing_publishes_the_frozen_ring_fields_test() ->
|
||||
State = ring_state([1, 2], [], [2], #{}),
|
||||
_ = maybe_dispatch_pending_ringing(State, false),
|
||||
ok = meck:wait(1, push_job_publisher, publish_ring, '_', 2000),
|
||||
[[UserId, ChannelId, MessageId, StartedAt, ExpiresAt]] = published_ring_args(),
|
||||
[[UserId, ChannelId, MessageId, StartedAt, ExpiresAt, _Caller]] = published_ring_args(),
|
||||
?assertEqual({2, 100, 200}, {UserId, ChannelId, MessageId}),
|
||||
?assertEqual(?RING_TIMEOUT_MS, ExpiresAt - StartedAt)
|
||||
after
|
||||
@@ -447,13 +460,53 @@ publish_ring_jobs_suppresses_a_fully_blocked_ring_test() ->
|
||||
ok = meck:new(push_eligibility, [passthrough, no_link]),
|
||||
try
|
||||
ok = meck:expect(push_eligibility, is_user_blocked, fun(_U, _O) -> true end),
|
||||
ok = publish_ring_jobs([2], [1, 2], 100, 200, 1, 2),
|
||||
ok = publish_ring_jobs(
|
||||
[2], [1, 2], build_ring_context(ring_state([1, 2], [], [], #{}))
|
||||
),
|
||||
?assertEqual([], published_ring_args())
|
||||
after
|
||||
meck:unload(push_eligibility),
|
||||
meck:unload(push_job_publisher)
|
||||
end.
|
||||
|
||||
apply_pending_ringing_publishes_the_caller_held_on_state_test() ->
|
||||
ok = mock_ring_publisher(),
|
||||
try
|
||||
State = (ring_state([1, 2], [], [2], #{}))#{
|
||||
caller_id => 1,
|
||||
caller_name => <<"Ada">>,
|
||||
caller_avatar => <<"a1b2c3d4">>
|
||||
},
|
||||
_ = maybe_dispatch_pending_ringing(State, false),
|
||||
ok = meck:wait(1, push_job_publisher, publish_ring, '_', 2000),
|
||||
?assertEqual(
|
||||
[
|
||||
#{
|
||||
caller_id => 1,
|
||||
caller_name => <<"Ada">>,
|
||||
caller_avatar => <<"a1b2c3d4">>
|
||||
}
|
||||
],
|
||||
published_ring_callers()
|
||||
)
|
||||
after
|
||||
meck:unload(push_job_publisher)
|
||||
end.
|
||||
|
||||
apply_pending_ringing_publishes_an_unresolved_caller_when_state_has_none_test() ->
|
||||
ok = mock_ring_publisher(),
|
||||
try
|
||||
State = ring_state([1, 2], [], [2], #{}),
|
||||
_ = maybe_dispatch_pending_ringing(State, false),
|
||||
ok = meck:wait(1, push_job_publisher, publish_ring, '_', 2000),
|
||||
?assertEqual(
|
||||
[#{caller_id => undefined, caller_name => undefined, caller_avatar => undefined}],
|
||||
published_ring_callers()
|
||||
)
|
||||
after
|
||||
meck:unload(push_job_publisher)
|
||||
end.
|
||||
|
||||
ring_is_suppressed_when_every_other_recipient_is_blocked_test() ->
|
||||
ok = meck:new(push_eligibility, [passthrough, no_link]),
|
||||
try
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
-export([
|
||||
build_call_event/1,
|
||||
caller_from_state/1,
|
||||
put_caller/2,
|
||||
format_voice_state/1,
|
||||
format_voice_states/1,
|
||||
format_pending_connections/1,
|
||||
@@ -28,6 +30,26 @@ build_call_event(State) ->
|
||||
voice_states => format_voice_states(maps:get(voice_states, State))
|
||||
}.
|
||||
|
||||
-spec caller_from_state(map()) -> map().
|
||||
caller_from_state(State) ->
|
||||
#{
|
||||
caller_id => maps:get(caller_id, State, undefined),
|
||||
caller_name => maps:get(caller_name, State, undefined),
|
||||
caller_avatar => maps:get(caller_avatar, State, undefined)
|
||||
}.
|
||||
|
||||
-spec put_caller(map(), map()) -> map().
|
||||
put_caller(#{caller_id := Id, caller_name := Name} = Caller, State) when
|
||||
is_integer(Id), is_binary(Name), byte_size(Name) > 0
|
||||
->
|
||||
State#{
|
||||
caller_id => Id,
|
||||
caller_name => Name,
|
||||
caller_avatar => maps:get(caller_avatar, Caller, undefined)
|
||||
};
|
||||
put_caller(_Caller, State) ->
|
||||
State.
|
||||
|
||||
-spec integer_list_to_binaries([integer()]) -> [binary()].
|
||||
integer_list_to_binaries(Values) ->
|
||||
lists:map(fun integer_to_binary/1, Values).
|
||||
@@ -245,3 +267,39 @@ voice_state_connection_id(VoiceState) ->
|
||||
VoiceState,
|
||||
maps:get(connection_id, VoiceState, undefined)
|
||||
).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
put_caller_stores_a_resolved_caller_test() ->
|
||||
Caller = #{caller_id => 7, caller_name => <<"Ada">>, caller_avatar => <<"a1b2c3d4">>},
|
||||
?assertEqual(Caller, caller_from_state(put_caller(Caller, #{}))).
|
||||
|
||||
put_caller_overwrites_an_earlier_caller_test() ->
|
||||
First = #{caller_id => 7, caller_name => <<"Ada">>, caller_avatar => <<"a1b2c3d4">>},
|
||||
Second = #{caller_id => 9, caller_name => <<"Bo">>, caller_avatar => undefined},
|
||||
State = put_caller(Second, put_caller(First, #{})),
|
||||
?assertEqual(Second, caller_from_state(State)).
|
||||
|
||||
put_caller_keeps_the_stored_caller_when_the_new_one_is_unresolved_test() ->
|
||||
Caller = #{caller_id => 7, caller_name => <<"Ada">>, caller_avatar => <<"a1b2c3d4">>},
|
||||
State = put_caller(Caller, #{}),
|
||||
Unresolved = #{
|
||||
caller_id => undefined, caller_name => undefined, caller_avatar => undefined
|
||||
},
|
||||
?assertEqual(Caller, caller_from_state(put_caller(Unresolved, State))).
|
||||
|
||||
put_caller_rejects_a_caller_without_a_name_test() ->
|
||||
Caller = #{caller_id => 7, caller_name => <<>>, caller_avatar => undefined},
|
||||
?assertEqual(
|
||||
#{caller_id => undefined, caller_name => undefined, caller_avatar => undefined},
|
||||
caller_from_state(put_caller(Caller, #{}))
|
||||
).
|
||||
|
||||
caller_from_state_reads_an_absent_caller_as_undefined_test() ->
|
||||
?assertEqual(
|
||||
#{caller_id => undefined, caller_name => undefined, caller_avatar => undefined},
|
||||
caller_from_state(#{})
|
||||
).
|
||||
|
||||
-endif.
|
||||
|
||||
@@ -19,10 +19,11 @@ execute_method(<<"call.update_region">>, #{
|
||||
<<"channel_id">> := ChannelIdBin, <<"region">> := Region
|
||||
}) ->
|
||||
handle_call_update_region(ChannelIdBin, Region);
|
||||
execute_method(<<"call.ring">>, #{
|
||||
<<"channel_id">> := ChannelIdBin, <<"recipients">> := RecipientsBin
|
||||
}) ->
|
||||
handle_call_ring(ChannelIdBin, RecipientsBin);
|
||||
execute_method(
|
||||
<<"call.ring">>,
|
||||
#{<<"channel_id">> := ChannelIdBin, <<"recipients">> := RecipientsBin} = Params
|
||||
) ->
|
||||
handle_call_ring(ChannelIdBin, RecipientsBin, Params);
|
||||
execute_method(<<"call.stop_ringing">>, #{
|
||||
<<"channel_id">> := ChannelIdBin, <<"recipients">> := RecipientsBin
|
||||
}) ->
|
||||
@@ -117,15 +118,32 @@ handle_call_create(Params) ->
|
||||
MessageId = validation:snowflake_or_throw(<<"message_id">>, MessageIdBin),
|
||||
Ringing = validation:snowflake_list_or_throw(<<"ringing">>, RingingBins),
|
||||
Recipients = validation:snowflake_list_or_throw(<<"recipients">>, RecipientsBins),
|
||||
CallData = #{
|
||||
channel_id => ChannelId,
|
||||
message_id => MessageId,
|
||||
region => Region,
|
||||
ringing => Ringing,
|
||||
recipients => Recipients
|
||||
},
|
||||
CallData = maps:merge(
|
||||
#{
|
||||
channel_id => ChannelId,
|
||||
message_id => MessageId,
|
||||
region => Region,
|
||||
ringing => Ringing,
|
||||
recipients => Recipients
|
||||
},
|
||||
caller_params(Params)
|
||||
),
|
||||
do_call_create(ChannelId, CallData).
|
||||
|
||||
-spec caller_params(map()) -> map().
|
||||
caller_params(Params) ->
|
||||
#{
|
||||
caller_id => snowflake_id:parse_maybe(maps:get(<<"caller_id">>, Params, undefined)),
|
||||
caller_name => optional_binary(maps:get(<<"caller_name">>, Params, undefined)),
|
||||
caller_avatar => optional_binary(maps:get(<<"caller_avatar">>, Params, undefined))
|
||||
}.
|
||||
|
||||
-spec optional_binary(term()) -> binary() | undefined.
|
||||
optional_binary(Value) when is_binary(Value), byte_size(Value) > 0 ->
|
||||
Value;
|
||||
optional_binary(_Value) ->
|
||||
undefined.
|
||||
|
||||
-spec handle_call_update_region(binary(), binary()) -> term().
|
||||
handle_call_update_region(ChannelIdBin, Region) ->
|
||||
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
|
||||
@@ -142,16 +160,18 @@ update_region(Pid, Region) ->
|
||||
_ -> gateway_rpc_error:raise(<<"update_region_error">>)
|
||||
end.
|
||||
|
||||
-spec handle_call_ring(binary(), list()) -> term().
|
||||
handle_call_ring(ChannelIdBin, RecipientsBin) ->
|
||||
-spec handle_call_ring(binary(), list(), map()) -> term().
|
||||
handle_call_ring(ChannelIdBin, RecipientsBin, Params) ->
|
||||
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
|
||||
Recipients = validation:snowflake_list_or_throw(<<"recipients">>, RecipientsBin),
|
||||
Caller = caller_params(Params),
|
||||
gateway_rpc_call_lookup:with_call(ChannelId, fun(Pid) ->
|
||||
ring_recipients(Pid, Recipients)
|
||||
ring_recipients(Pid, Recipients, Caller)
|
||||
end).
|
||||
|
||||
-spec ring_recipients(pid(), [integer()]) -> true.
|
||||
ring_recipients(Pid, Recipients) ->
|
||||
-spec ring_recipients(pid(), [integer()], map()) -> true.
|
||||
ring_recipients(Pid, Recipients, Caller) ->
|
||||
ok = set_call_caller(Pid, Caller),
|
||||
case
|
||||
gateway_rpc_call_lookup:safe_gen_server_call(
|
||||
Pid, {ring_recipients, Recipients}, ?CALL_LOOKUP_TIMEOUT
|
||||
@@ -161,6 +181,13 @@ ring_recipients(Pid, Recipients) ->
|
||||
_ -> gateway_rpc_error:raise(<<"ring_recipients_error">>)
|
||||
end.
|
||||
|
||||
-spec set_call_caller(pid(), map()) -> ok.
|
||||
set_call_caller(_Pid, #{caller_id := undefined}) ->
|
||||
ok;
|
||||
set_call_caller(Pid, Caller) ->
|
||||
gen_server:cast(Pid, {set_caller, Caller}),
|
||||
ok.
|
||||
|
||||
-spec handle_call_stop_ringing(binary(), list()) -> term().
|
||||
handle_call_stop_ringing(ChannelIdBin, RecipientsBin) ->
|
||||
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, ChannelIdBin),
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([publish_message/8, publish_message/10, publish_clear/3, publish_clear/5]).
|
||||
-export([publish_ring/5, request/3]).
|
||||
-export([publish_ring/6, request/3]).
|
||||
|
||||
-define(SUBJECT_MESSAGE, <<"push.job.message">>).
|
||||
-define(SUBJECT_CLEAR, <<"push.job.clear">>).
|
||||
-define(SUBJECT_RING, <<"push.job.ring">>).
|
||||
-define(JOB_VERSION, 1).
|
||||
-define(NATS_MAX_PAYLOAD_BYTES, 1048576).
|
||||
-define(MAX_CALLER_NAME_BYTES, 128).
|
||||
|
||||
-type meta() :: #{
|
||||
kind := message | clear | ring,
|
||||
@@ -134,18 +135,21 @@ publish_clear(UserId, ChannelId, MessageId, ConfigVersion, Fallback) ->
|
||||
fallback => Fallback
|
||||
}).
|
||||
|
||||
-spec publish_ring(integer(), integer(), integer(), integer(), integer()) ->
|
||||
-spec publish_ring(integer(), integer(), integer(), integer(), integer(), map()) ->
|
||||
ok | {error, term()}.
|
||||
publish_ring(UserId, ChannelId, MessageId, StartedAtMs, ExpiresAtMs) ->
|
||||
Job = #{
|
||||
<<"v">> => ?JOB_VERSION,
|
||||
<<"config_version">> => push_delivery_config:config_version(),
|
||||
<<"user_id">> => integer_to_binary(UserId),
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"message_id">> => integer_to_binary(MessageId),
|
||||
<<"started_at_ms">> => StartedAtMs,
|
||||
<<"expires_at_ms">> => ExpiresAtMs
|
||||
},
|
||||
publish_ring(UserId, ChannelId, MessageId, StartedAtMs, ExpiresAtMs, Caller) ->
|
||||
Job = maps:merge(
|
||||
#{
|
||||
<<"v">> => ?JOB_VERSION,
|
||||
<<"config_version">> => push_delivery_config:config_version(),
|
||||
<<"user_id">> => integer_to_binary(UserId),
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"message_id">> => integer_to_binary(MessageId),
|
||||
<<"started_at_ms">> => StartedAtMs,
|
||||
<<"expires_at_ms">> => ExpiresAtMs
|
||||
},
|
||||
caller_fields(Caller)
|
||||
),
|
||||
publish(?SUBJECT_RING, Job, #{
|
||||
kind => ring,
|
||||
user_ids => [UserId],
|
||||
@@ -154,6 +158,33 @@ publish_ring(UserId, ChannelId, MessageId, StartedAtMs, ExpiresAtMs) ->
|
||||
fallback => fun ignore_fallback/1
|
||||
}).
|
||||
|
||||
-spec caller_fields(map()) -> map().
|
||||
caller_fields(#{caller_id := CallerId, caller_name := Name} = Caller) when
|
||||
is_integer(CallerId), is_binary(Name), byte_size(Name) > 0
|
||||
->
|
||||
CallerIdBin = integer_to_binary(CallerId),
|
||||
#{
|
||||
<<"caller_id">> => CallerIdBin,
|
||||
<<"caller_name">> => push_notification_format:truncate_bytes(
|
||||
Name, ?MAX_CALLER_NAME_BYTES
|
||||
),
|
||||
<<"caller_avatar_url">> => caller_avatar_url(
|
||||
CallerIdBin, maps:get(caller_avatar, Caller, undefined)
|
||||
)
|
||||
};
|
||||
caller_fields(_Caller) ->
|
||||
#{}.
|
||||
|
||||
-spec caller_avatar_url(binary(), term()) -> binary().
|
||||
caller_avatar_url(CallerIdBin, Hash) when is_binary(Hash), byte_size(Hash) > 0 ->
|
||||
push_notification_format:resolve_author_avatar_url(#{
|
||||
<<"id">> => CallerIdBin, <<"avatar">> => Hash
|
||||
});
|
||||
caller_avatar_url(CallerIdBin, _Hash) ->
|
||||
push_notification_format:resolve_author_avatar_url(#{
|
||||
<<"id">> => CallerIdBin, <<"avatar">> => null
|
||||
}).
|
||||
|
||||
-spec request(binary(), binary(), pos_integer()) -> ok | {error, term()}.
|
||||
request(Subject, Body, Timeout) ->
|
||||
case gateway_nats_pool_conn:get_pool_conn() of
|
||||
@@ -269,3 +300,77 @@ outbox_job(Subject, Job, Body, Meta) ->
|
||||
message_id => MessageId,
|
||||
fallback => Fallback
|
||||
}.
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
unresolved_caller() ->
|
||||
#{caller_id => undefined, caller_name => undefined, caller_avatar => undefined}.
|
||||
|
||||
with_endpoint_env(Fun) ->
|
||||
ok = meck:new(fluxer_gateway_env, [passthrough, no_link]),
|
||||
try
|
||||
ok = meck:expect(fluxer_gateway_env, get, fun endpoint_env_meck/1),
|
||||
Fun()
|
||||
after
|
||||
meck:unload(fluxer_gateway_env)
|
||||
end.
|
||||
|
||||
endpoint_env_meck(media_proxy_endpoint) -> <<"https://media.example">>;
|
||||
endpoint_env_meck(static_cdn_endpoint) -> <<"https://static.example">>;
|
||||
endpoint_env_meck(Key) -> meck:passthrough([Key]).
|
||||
|
||||
caller_fields_omits_every_key_when_the_caller_is_unresolved_test() ->
|
||||
?assertEqual(#{}, caller_fields(unresolved_caller())).
|
||||
|
||||
caller_fields_omits_every_key_when_only_the_name_resolved_test() ->
|
||||
?assertEqual(
|
||||
#{},
|
||||
caller_fields(#{
|
||||
caller_id => undefined, caller_name => <<"Ada">>, caller_avatar => undefined
|
||||
})
|
||||
).
|
||||
|
||||
caller_fields_builds_the_avatar_url_from_the_hash_test() ->
|
||||
Fields = with_endpoint_env(fun() ->
|
||||
caller_fields(#{
|
||||
caller_id => 1234567890123456789,
|
||||
caller_name => <<"Ada">>,
|
||||
caller_avatar => <<"a1b2c3d4">>
|
||||
})
|
||||
end),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"caller_id">> => <<"1234567890123456789">>,
|
||||
<<"caller_name">> => <<"Ada">>,
|
||||
<<"caller_avatar_url">> =>
|
||||
<<"https://media.example/avatars/1234567890123456789/a1b2c3d4.png">>
|
||||
},
|
||||
Fields
|
||||
).
|
||||
|
||||
caller_fields_falls_back_to_the_default_avatar_test() ->
|
||||
Fields = with_endpoint_env(fun() ->
|
||||
caller_fields(#{
|
||||
caller_id => 1234567890123456789,
|
||||
caller_name => <<"Ada">>,
|
||||
caller_avatar => undefined
|
||||
})
|
||||
end),
|
||||
?assertMatch(
|
||||
#{<<"caller_avatar_url">> := <<"https://static.example/avatars/", _/binary>>},
|
||||
Fields
|
||||
).
|
||||
|
||||
caller_fields_caps_the_caller_name_test() ->
|
||||
Name = binary:copy(<<"a">>, ?MAX_CALLER_NAME_BYTES + 32),
|
||||
Fields = with_endpoint_env(fun() ->
|
||||
caller_fields(#{
|
||||
caller_id => 1234567890123456789, caller_name => Name, caller_avatar => undefined
|
||||
})
|
||||
end),
|
||||
?assertEqual(
|
||||
?MAX_CALLER_NAME_BYTES, byte_size(maps:get(<<"caller_name">>, Fields))
|
||||
).
|
||||
|
||||
-endif.
|
||||
|
||||
@@ -737,7 +737,7 @@ header_value(Name, Headers) ->
|
||||
|
||||
capture_web_push(Payload) ->
|
||||
Endpoint = <<
|
||||
"https://push.fluxer.app/relay/v1/apns/stable/production/",
|
||||
"https://push.fluxer.com/relay/v1/apns/stable/production/",
|
||||
(binary:copy(<<"a">>, 64))/binary
|
||||
>>,
|
||||
{PeerPub, _PeerPriv} = crypto:generate_key(ecdh, prime256v1),
|
||||
|
||||
Reference in New Issue
Block a user