fix(push): drop Fluxer badge on self-hosted instances (#3400)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 15:56:53 +02:00
1 parent be314788b3
commit af7724dbf6
5 files changed
+104 -20

No files matched your search

@@ -28,6 +28,7 @@ load() ->
env_config() -> env_config() ->
#{ #{
<<"env">> => env_binary("FLUXER_ENV", <<"development">>), <<"env">> => env_binary("FLUXER_ENV", <<"development">>),
<<"instance">> => #{<<"self_hosted">> => env_bool("FLUXER_SELF_HOSTED", false)},
<<"internal">> => env_internal_config(), <<"internal">> => env_internal_config(),
<<"public">> => env_public_config(), <<"public">> => env_public_config(),
<<"proxy">> => env_proxy_config(), <<"proxy">> => env_proxy_config(),
@@ -147,9 +148,10 @@ build_config(RawConfig) ->
Nats = get_map(RawConfig, [<<"services">>, <<"nats">>]), Nats = get_map(RawConfig, [<<"services">>, <<"nats">>]),
Proxy = get_map(RawConfig, [<<"proxy">>]), Proxy = get_map(RawConfig, [<<"proxy">>]),
Public = get_map(RawConfig, [<<"public">>]), Public = get_map(RawConfig, [<<"public">>]),
Instance = get_map(RawConfig, [<<"instance">>]),
lists:foldl(fun maps:merge/2, #{}, [ lists:foldl(fun maps:merge/2, #{}, [
build_core_config(Service, Internal, Nats, Proxy), build_core_config(Service, Internal, Nats, Proxy),
build_push_config(Service, Public), build_push_config(Service, Public, Instance),
build_sharding_config(Service), build_sharding_config(Service),
build_http_config(Service), build_http_config(Service),
build_cluster_config(Service, Public), build_cluster_config(Service, Public),
@@ -176,10 +178,11 @@ build_core_config(Service, Internal, Nats, Proxy) ->
) )
}. }.
-spec build_push_config(map(), map()) -> config(). -spec build_push_config(map(), map(), map()) -> config().
build_push_config(Service, Public) -> build_push_config(Service, Public, Instance) ->
#{ #{
push_enabled => get_bool(Service, <<"push_enabled">>, true), push_enabled => get_bool(Service, <<"push_enabled">>, true),
self_hosted => get_bool(Instance, <<"self_hosted">>, false),
static_cdn_endpoint => public_endpoint( static_cdn_endpoint => public_endpoint(
get_binary(Service, <<"static_cdn_endpoint">>, <<"http://localhost:8088">>), Public get_binary(Service, <<"static_cdn_endpoint">>, <<"http://localhost:8088">>), Public
), ),
+48 -8
View File
@@ -231,7 +231,7 @@ notification_fields(
), ),
ChannelIdBin = integer_to_binary(ChannelId), ChannelIdBin = integer_to_binary(ChannelId),
MessageIdBin = integer_to_binary(MessageId), MessageIdBin = integer_to_binary(MessageId),
#{ with_badge(#{
<<"title">> => push_notification:build_notification_title( <<"title">> => push_notification:build_notification_title(
AuthorName, MessageData, GuildId, GuildName, ChannelName AuthorName, MessageData, GuildId, GuildName, ChannelName
), ),
@@ -239,14 +239,24 @@ notification_fields(
MessageData, MarkdownContext MessageData, MarkdownContext
), ),
<<"icon">> => push_notification_format:resolve_author_avatar_url(AuthorData), <<"icon">> => push_notification_format:resolve_author_avatar_url(AuthorData),
<<"badge">> => push_utils:construct_static_asset_url(
<<"marketing/branding/symbol-white.svg">>
),
<<"tag">> => <<"channel:", ChannelIdBin/binary, ":", MessageIdBin/binary>>, <<"tag">> => <<"channel:", ChannelIdBin/binary, ":", MessageIdBin/binary>>,
<<"notification_tag">> => <<"channel:", ChannelIdBin/binary>>, <<"notification_tag">> => <<"channel:", ChannelIdBin/binary>>,
<<"url">> => push_notification_format:build_url(GuildId, ChannelId, MessageId), <<"url">> => push_notification_format:build_url(GuildId, ChannelId, MessageId),
<<"image_url">> => nullable(push_notification_format:extract_image_url(MessageData)) <<"image_url">> => nullable(push_notification_format:extract_image_url(MessageData))
}. }).
-spec with_badge(map()) -> map().
with_badge(Fields) ->
case fluxer_gateway_env:get(self_hosted) of
true ->
Fields;
_ ->
Fields#{
<<"badge">> => push_utils:construct_static_asset_url(
<<"marketing/branding/symbol-white.svg">>
)
}
end.
-spec thread_job_fields(map(), binary() | undefined) -> map(). -spec thread_job_fields(map(), binary() | undefined) -> map().
thread_job_fields(MessageData, ChannelName) -> thread_job_fields(MessageData, ChannelName) ->
@@ -329,9 +339,15 @@ outbox_job(Subject, Job, Body, Meta) ->
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
with_endpoint_env(Fun) -> with_endpoint_env(Fun) ->
with_endpoint_env(false, Fun).
with_endpoint_env(SelfHosted, Fun) ->
ok = meck:new(fluxer_gateway_env, [passthrough, no_link]), ok = meck:new(fluxer_gateway_env, [passthrough, no_link]),
try try
ok = meck:expect(fluxer_gateway_env, get, fun endpoint_env_meck/1), ok = meck:expect(fluxer_gateway_env, get, fun
(self_hosted) -> SelfHosted;
(Key) -> endpoint_env_meck(Key)
end),
Fun() Fun()
after after
meck:unload(fluxer_gateway_env) meck:unload(fluxer_gateway_env)
@@ -397,7 +413,9 @@ encoded_message_job(MessageData, ChannelName) ->
push_notification_format, resolve_author_avatar_url, fun(_) -> <<"icon">> end push_notification_format, resolve_author_avatar_url, fun(_) -> <<"icon">> end
), ),
ok = meck:expect(push_utils, construct_static_asset_url, fun(_) -> <<"badge">> end), ok = meck:expect(push_utils, construct_static_asset_url, fun(_) -> <<"badge">> end),
Job = message_job(MessageData, #{}, 5, 6, 7, <<"Guild">>, ChannelName), Job = with_endpoint_env(fun() ->
message_job(MessageData, #{}, 5, 6, 7, <<"Guild">>, ChannelName)
end),
json:decode(iolist_to_binary(json:encode(Job))) json:decode(iolist_to_binary(json:encode(Job)))
after after
meck:unload(push_utils), meck:unload(push_utils),
@@ -507,12 +525,34 @@ notification_fields_omit_sensitive_attachment_image_test() ->
Fields = test_notification_fields(MessageData, 123, <<"Server">>, <<"general">>), Fields = test_notification_fields(MessageData, 123, <<"Server">>, <<"general">>),
?assertEqual(null, maps:get(<<"image_url">>, Fields)). ?assertEqual(null, maps:get(<<"image_url">>, Fields)).
notification_fields_include_the_fluxer_badge_test() ->
MessageData = #{<<"content">> => <<"Hi">>, <<"mentions">> => []},
Fields = test_notification_fields(MessageData, 123, <<"Server">>, <<"general">>),
?assertEqual(
<<"https://static.example/marketing/branding/symbol-white.svg">>,
maps:get(<<"badge">>, Fields)
).
notification_fields_omit_the_badge_on_self_hosted_instances_test() ->
MessageData = #{<<"content">> => <<"Hi">>, <<"mentions">> => []},
Fields = test_notification_fields(
MessageData, 123, <<"Server">>, <<"general">>, #{}, true
),
?assertNot(maps:is_key(<<"badge">>, Fields)).
test_notification_fields(MessageData, GuildId, GuildName, ChannelName) -> test_notification_fields(MessageData, GuildId, GuildName, ChannelName) ->
test_notification_fields(MessageData, GuildId, GuildName, ChannelName, #{}). test_notification_fields(MessageData, GuildId, GuildName, ChannelName, #{}).
test_notification_fields(MessageData, GuildId, GuildName, ChannelName, MarkdownContext) -> test_notification_fields(MessageData, GuildId, GuildName, ChannelName, MarkdownContext) ->
test_notification_fields(
MessageData, GuildId, GuildName, ChannelName, MarkdownContext, false
).
test_notification_fields(
MessageData, GuildId, GuildName, ChannelName, MarkdownContext, SelfHosted
) ->
Author = #{<<"id">> => <<"42">>, <<"username">> => <<"Alice">>, <<"avatar">> => null}, Author = #{<<"id">> => <<"42">>, <<"username">> => <<"Alice">>, <<"avatar">> => null},
with_endpoint_env(fun() -> with_endpoint_env(SelfHosted, fun() ->
notification_fields( notification_fields(
MessageData#{<<"author">> => Author}, MessageData#{<<"author">> => Author},
MarkdownContext, MarkdownContext,
@@ -74,6 +74,14 @@ blank_env_values_fall_back_to_the_defaults_test() ->
end end
). ).
self_hosted_follows_the_fluxer_self_hosted_env_test() ->
with_env("FLUXER_SELF_HOSTED", "", fun() ->
?assertEqual(false, maps:get(self_hosted, fluxer_gateway_config:load()))
end),
with_env("FLUXER_SELF_HOSTED", "true", fun() ->
?assertEqual(true, maps:get(self_hosted, fluxer_gateway_config:load()))
end).
pinned_guild_ids_reject_non_snowflakes_test() -> pinned_guild_ids_reject_non_snowflakes_test() ->
with_env("FLUXER_GATEWAY_PINNED_GUILD_IDS", "1100000000000000001,ab", fun() -> with_env("FLUXER_GATEWAY_PINNED_GUILD_IDS", "1100000000000000001,ab", fun() ->
?assertError({invalid_pinned_guild_id, "ab"}, fluxer_gateway_config:load()) ?assertError({invalid_pinned_guild_id, "ab"}, fluxer_gateway_config:load())
+11 -1
View File
@@ -36,7 +36,8 @@ pub struct NotificationFields {
pub title: String, pub title: String,
pub body: String, pub body: String,
pub icon: String, pub icon: String,
pub badge: String, #[serde(default)]
pub badge: Option<String>,
pub tag: String, pub tag: String,
pub notification_tag: String, pub notification_tag: String,
pub url: String, pub url: String,
@@ -165,6 +166,15 @@ mod tests {
assert_eq!(job.channel_name, None); assert_eq!(job.channel_name, None);
} }
#[test]
fn a_job_without_a_badge_decodes() {
let job = decode_message(
br#"{"v":1,"guild_id":"5","channel_id":"6","message_id":"7","notification":{"title":"t","body":"b","icon":"i","tag":"x","notification_tag":"y","url":"/u"},"user_ids":["8"]}"#,
)
.expect("the job decodes");
assert_eq!(job.notification.badge, None);
}
#[test] #[test]
fn a_thread_job_decodes_its_optional_fields() { fn a_thread_job_decodes_its_optional_fields() {
let job = decode_message( let job = decode_message(
+31 -8
View File
@@ -65,27 +65,28 @@ pub fn web_push_message(job: &MessageJob, target_user_id: &str, badge_count: u32
"has_media": image_url.is_some(), "has_media": image_url.is_some(),
}); });
merge_thread_fields(&mut data, job); merge_thread_fields(&mut data, job);
let notification = json!({ let badge = fields.badge.as_deref();
let mut notification = json!({
"title": fields.title, "title": fields.title,
"body": fields.body, "body": fields.body,
"icon": fields.icon, "icon": fields.icon,
"badge": fields.badge,
"tag": fields.tag, "tag": fields.tag,
"navigate": fields.url, "navigate": fields.url,
"app_badge": badge_count.to_string(), "app_badge": badge_count.to_string(),
"data": data, "data": data,
}); });
insert_present(&mut notification, "badge", badge);
let mut envelope = json!({ let mut envelope = json!({
"web_push": WEB_PUSH_MARKER, "web_push": WEB_PUSH_MARKER,
"notification": notification, "notification": notification,
"title": fields.title, "title": fields.title,
"body": fields.body, "body": fields.body,
"icon": fields.icon, "icon": fields.icon,
"badge": fields.badge,
"tag": fields.tag, "tag": fields.tag,
"data": data, "data": data,
}); });
merge_image_fields(&mut envelope, image_url); insert_present(&mut envelope, "badge", badge);
insert_present(&mut envelope, "image_url", image_url);
envelope envelope
} }
@@ -617,11 +618,11 @@ fn first_text<'a>(envelope: &'a Value, key: &str) -> Option<&'a str> {
.or_else(|| envelope.get("notification")?.get(key).and_then(non_empty)) .or_else(|| envelope.get("notification")?.get(key).and_then(non_empty))
} }
fn merge_image_fields(target: &mut Value, image_url: Option<&str>) { fn insert_present(target: &mut Value, key: &str, value: Option<&str>) {
let (Some(image_url), Some(object)) = (image_url, target.as_object_mut()) else { let (Some(value), Some(object)) = (value, target.as_object_mut()) else {
return; return;
}; };
object.insert("image_url".to_owned(), image_url.into()); object.insert(key.to_owned(), value.into());
} }
fn non_empty(value: &Value) -> Option<&str> { fn non_empty(value: &Value) -> Option<&str> {
@@ -651,7 +652,7 @@ mod tests {
title: "Elias".to_owned(), title: "Elias".to_owned(),
body: "see you tomorrow".to_owned(), body: "see you tomorrow".to_owned(),
icon: "https://media.fluxer.app/avatars/1/a.webp".to_owned(), icon: "https://media.fluxer.app/avatars/1/a.webp".to_owned(),
badge: "https://media.fluxer.app/badge.png".to_owned(), badge: Some("https://media.fluxer.app/badge.png".to_owned()),
tag: format!("channel:{CHANNEL_ID}:{MESSAGE_ID}"), tag: format!("channel:{CHANNEL_ID}:{MESSAGE_ID}"),
notification_tag: format!("channel:{CHANNEL_ID}"), notification_tag: format!("channel:{CHANNEL_ID}"),
url: format!("/channels/@me/{CHANNEL_ID}"), url: format!("/channels/@me/{CHANNEL_ID}"),
@@ -726,6 +727,28 @@ mod tests {
); );
} }
#[test]
fn a_message_with_a_badge_sends_it_in_both_places() {
let envelope = web_push_message(&message_job(None), USER_ID, 3);
assert_eq!(
envelope["badge"],
json!("https://media.fluxer.app/badge.png")
);
assert_eq!(
envelope["notification"]["badge"],
json!("https://media.fluxer.app/badge.png")
);
}
#[test]
fn a_message_without_a_badge_leaves_the_key_out() {
let mut job = message_job(None);
job.notification.badge = None;
let envelope = web_push_message(&job, USER_ID, 3);
assert!(envelope.get("badge").is_none());
assert!(envelope["notification"].get("badge").is_none());
}
#[test] #[test]
fn a_message_outside_a_thread_carries_no_thread_keys() { fn a_message_outside_a_thread_carries_no_thread_keys() {
let envelope = web_push_message(&message_job(None), USER_ID, 3); let envelope = web_push_message(&message_job(None), USER_ID, 3);