feat(push): ring Android calls and harden the relay (#2915)

This commit is contained in:
Hampus
2026-09-24 00:07:15 +02:00
committed by GitHub
parent b16989d567
commit 5fde6eb484
25 changed files with 1050 additions and 120 deletions
+25 -10
View File
@@ -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.
+27
View File
@@ -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.
+79 -26
View File
@@ -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
+58
View File
@@ -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.
+43 -16
View File
@@ -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),
+117 -12
View File
@@ -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),