diff --git a/fluxer_gateway/src/gateway/fluxer_gateway_config.erl b/fluxer_gateway/src/gateway/fluxer_gateway_config.erl index 9ca9752a1..348969765 100644 --- a/fluxer_gateway/src/gateway/fluxer_gateway_config.erl +++ b/fluxer_gateway/src/gateway/fluxer_gateway_config.erl @@ -28,6 +28,7 @@ load() -> env_config() -> #{ <<"env">> => env_binary("FLUXER_ENV", <<"development">>), + <<"instance">> => #{<<"self_hosted">> => env_bool("FLUXER_SELF_HOSTED", false)}, <<"internal">> => env_internal_config(), <<"public">> => env_public_config(), <<"proxy">> => env_proxy_config(), @@ -147,9 +148,10 @@ build_config(RawConfig) -> Nats = get_map(RawConfig, [<<"services">>, <<"nats">>]), Proxy = get_map(RawConfig, [<<"proxy">>]), Public = get_map(RawConfig, [<<"public">>]), + Instance = get_map(RawConfig, [<<"instance">>]), lists:foldl(fun maps:merge/2, #{}, [ build_core_config(Service, Internal, Nats, Proxy), - build_push_config(Service, Public), + build_push_config(Service, Public, Instance), build_sharding_config(Service), build_http_config(Service), build_cluster_config(Service, Public), @@ -176,10 +178,11 @@ build_core_config(Service, Internal, Nats, Proxy) -> ) }. --spec build_push_config(map(), map()) -> config(). -build_push_config(Service, Public) -> +-spec build_push_config(map(), map(), map()) -> config(). +build_push_config(Service, Public, Instance) -> #{ push_enabled => get_bool(Service, <<"push_enabled">>, true), + self_hosted => get_bool(Instance, <<"self_hosted">>, false), static_cdn_endpoint => public_endpoint( get_binary(Service, <<"static_cdn_endpoint">>, <<"http://localhost:8088">>), Public ), diff --git a/fluxer_gateway/src/push/push_job_publisher.erl b/fluxer_gateway/src/push/push_job_publisher.erl index 386edddf8..b521889a8 100644 --- a/fluxer_gateway/src/push/push_job_publisher.erl +++ b/fluxer_gateway/src/push/push_job_publisher.erl @@ -231,7 +231,7 @@ notification_fields( ), ChannelIdBin = integer_to_binary(ChannelId), MessageIdBin = integer_to_binary(MessageId), - #{ + with_badge(#{ <<"title">> => push_notification:build_notification_title( AuthorName, MessageData, GuildId, GuildName, ChannelName ), @@ -239,14 +239,24 @@ notification_fields( MessageData, MarkdownContext ), <<"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>>, <<"notification_tag">> => <<"channel:", ChannelIdBin/binary>>, <<"url">> => push_notification_format:build_url(GuildId, ChannelId, MessageId), <<"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(). thread_job_fields(MessageData, ChannelName) -> @@ -329,9 +339,15 @@ outbox_job(Subject, Job, Body, Meta) -> -include_lib("eunit/include/eunit.hrl"). with_endpoint_env(Fun) -> + with_endpoint_env(false, Fun). + +with_endpoint_env(SelfHosted, Fun) -> ok = meck:new(fluxer_gateway_env, [passthrough, no_link]), 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() after meck:unload(fluxer_gateway_env) @@ -397,7 +413,9 @@ encoded_message_job(MessageData, ChannelName) -> push_notification_format, resolve_author_avatar_url, fun(_) -> <<"icon">> 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))) after meck:unload(push_utils), @@ -507,12 +525,34 @@ notification_fields_omit_sensitive_attachment_image_test() -> Fields = test_notification_fields(MessageData, 123, <<"Server">>, <<"general">>), ?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, 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}, - with_endpoint_env(fun() -> + with_endpoint_env(SelfHosted, fun() -> notification_fields( MessageData#{<<"author">> => Author}, MarkdownContext, diff --git a/fluxer_gateway/test/fluxer_gateway_config_tests.erl b/fluxer_gateway/test/fluxer_gateway_config_tests.erl index 5a7591fab..3079c3ba2 100644 --- a/fluxer_gateway/test/fluxer_gateway_config_tests.erl +++ b/fluxer_gateway/test/fluxer_gateway_config_tests.erl @@ -74,6 +74,14 @@ blank_env_values_fall_back_to_the_defaults_test() -> 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() -> with_env("FLUXER_GATEWAY_PINNED_GUILD_IDS", "1100000000000000001,ab", fun() -> ?assertError({invalid_pinned_guild_id, "ab"}, fluxer_gateway_config:load()) diff --git a/fluxer_push/src/job.rs b/fluxer_push/src/job.rs index cdfb1b314..125b26c1d 100644 --- a/fluxer_push/src/job.rs +++ b/fluxer_push/src/job.rs @@ -36,7 +36,8 @@ pub struct NotificationFields { pub title: String, pub body: String, pub icon: String, - pub badge: String, + #[serde(default)] + pub badge: Option, pub tag: String, pub notification_tag: String, pub url: String, @@ -165,6 +166,15 @@ mod tests { 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] fn a_thread_job_decodes_its_optional_fields() { let job = decode_message( diff --git a/fluxer_push/src/payload.rs b/fluxer_push/src/payload.rs index 41e98ace2..ccc927033 100644 --- a/fluxer_push/src/payload.rs +++ b/fluxer_push/src/payload.rs @@ -65,27 +65,28 @@ pub fn web_push_message(job: &MessageJob, target_user_id: &str, badge_count: u32 "has_media": image_url.is_some(), }); merge_thread_fields(&mut data, job); - let notification = json!({ + let badge = fields.badge.as_deref(); + let mut notification = json!({ "title": fields.title, "body": fields.body, "icon": fields.icon, - "badge": fields.badge, "tag": fields.tag, "navigate": fields.url, "app_badge": badge_count.to_string(), "data": data, }); + insert_present(&mut notification, "badge", badge); let mut envelope = json!({ "web_push": WEB_PUSH_MARKER, "notification": notification, "title": fields.title, "body": fields.body, "icon": fields.icon, - "badge": fields.badge, "tag": fields.tag, "data": data, }); - merge_image_fields(&mut envelope, image_url); + insert_present(&mut envelope, "badge", badge); + insert_present(&mut envelope, "image_url", image_url); 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)) } -fn merge_image_fields(target: &mut Value, image_url: Option<&str>) { - let (Some(image_url), Some(object)) = (image_url, target.as_object_mut()) else { +fn insert_present(target: &mut Value, key: &str, value: Option<&str>) { + let (Some(value), Some(object)) = (value, target.as_object_mut()) else { return; }; - object.insert("image_url".to_owned(), image_url.into()); + object.insert(key.to_owned(), value.into()); } fn non_empty(value: &Value) -> Option<&str> { @@ -651,7 +652,7 @@ mod tests { title: "Elias".to_owned(), body: "see you tomorrow".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}"), notification_tag: format!("channel:{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] fn a_message_outside_a_thread_carries_no_thread_keys() { let envelope = web_push_message(&message_job(None), USER_ID, 3);