mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(push): gate relay delivery on operator consent (#2984)
This commit is contained in:
@@ -15225,7 +15225,8 @@
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"relay_consent_accepted": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"VoiceNoiseSuppressionConfigUpdateRequest": {
|
||||
@@ -15357,7 +15358,16 @@
|
||||
"maxItems": 1000,
|
||||
"type": "array",
|
||||
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
|
||||
}
|
||||
},
|
||||
"relay_consent_accepted": {"default": false, "type": "boolean"},
|
||||
"relay_consent_accepted_at": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||
},
|
||||
"relay_consent_accepted_by": {"default": null, "nullable": true, "type": "string", "pattern": "^\\d{1,20}$"}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
@@ -15365,7 +15375,10 @@
|
||||
"rollout_basis_points",
|
||||
"rollout_salt",
|
||||
"included_user_ids",
|
||||
"excluded_user_ids"
|
||||
"excluded_user_ids",
|
||||
"relay_consent_accepted",
|
||||
"relay_consent_accepted_at",
|
||||
"relay_consent_accepted_by"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
@@ -552,6 +552,9 @@ pub struct PushServiceDeliveryConfigResponse {
|
||||
pub rollout_salt: String,
|
||||
pub included_user_ids: Vec<String>,
|
||||
pub excluded_user_ids: Vec<String>,
|
||||
pub relay_consent_accepted: bool,
|
||||
pub relay_consent_accepted_at: Option<String>,
|
||||
pub relay_consent_accepted_by: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PushServiceDeliveryConfigResponse {
|
||||
@@ -563,6 +566,9 @@ impl Default for PushServiceDeliveryConfigResponse {
|
||||
rollout_salt: PUSH_SERVICE_DELIVERY_DEFAULT_SALT.to_owned(),
|
||||
included_user_ids: Vec::new(),
|
||||
excluded_user_ids: Vec::new(),
|
||||
relay_consent_accepted: false,
|
||||
relay_consent_accepted_at: None,
|
||||
relay_consent_accepted_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,6 +585,8 @@ pub struct PushServiceDeliveryConfigUpdateRequest {
|
||||
pub included_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_user_ids: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub relay_consent_accepted: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
||||
@@ -680,6 +680,9 @@ fn build_push_service_delivery_update(
|
||||
.unwrap_or_default(),
|
||||
"Excluded user IDs",
|
||||
)?),
|
||||
relay_consent_accepted: Some(
|
||||
form.bool_value("push_service_delivery_relay_consent_accepted"),
|
||||
),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -1731,6 +1734,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_push_service_delivery_update_reads_the_relay_consent_checkbox() {
|
||||
let unchecked = MultiValueForm::parse(b"_csrf=token");
|
||||
assert_eq!(
|
||||
build_push_service_delivery_update(&unchecked)
|
||||
.expect("valid form")
|
||||
.push_service_delivery
|
||||
.expect("push service delivery update")
|
||||
.relay_consent_accepted,
|
||||
Some(false)
|
||||
);
|
||||
|
||||
let checked =
|
||||
MultiValueForm::parse(b"_csrf=token&push_service_delivery_relay_consent_accepted=true");
|
||||
assert_eq!(
|
||||
build_push_service_delivery_update(&checked)
|
||||
.expect("valid form")
|
||||
.push_service_delivery
|
||||
.expect("push service delivery update")
|
||||
.relay_consent_accepted,
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
|
||||
let form = MultiValueForm::parse(b"_csrf=token");
|
||||
|
||||
@@ -1191,6 +1191,14 @@ fn push_service_delivery_section(
|
||||
};
|
||||
let included_user_ids = push_service_delivery.included_user_ids.join("\n");
|
||||
let excluded_user_ids = push_service_delivery.excluded_user_ids.join("\n");
|
||||
let relay_consent_stamp = match (
|
||||
push_service_delivery.relay_consent_accepted_at.as_deref(),
|
||||
push_service_delivery.relay_consent_accepted_by.as_deref(),
|
||||
) {
|
||||
(Some(at), Some(by)) => Some(format!("Accepted {at} by user {by}")),
|
||||
(Some(at), None) => Some(format!("Accepted {at}")),
|
||||
_ => None,
|
||||
};
|
||||
section_card_with_description(
|
||||
"Push Service Delivery",
|
||||
"Routes push notification delivery for the selected accounts through the push service. \
|
||||
@@ -1219,6 +1227,28 @@ fn push_service_delivery_section(
|
||||
effect at all."
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Managed relay consent" }
|
||||
(checkbox(
|
||||
"push_service_delivery_relay_consent_accepted",
|
||||
"true",
|
||||
"Accept the push relay supplemental privacy notice",
|
||||
push_service_delivery.relay_consent_accepted,
|
||||
true,
|
||||
))
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Required only for the official mobile apps, whose notifications travel \
|
||||
through Fluxer's relay to Apple and Google. Until this is accepted those \
|
||||
notifications are dropped. Self-hosted UnifiedPush and ntfy endpoints \
|
||||
never reach the relay and are unaffected. "
|
||||
a href="https://fluxer.com/push-relay" target="_blank" rel="noreferrer"
|
||||
class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500" {
|
||||
"Read the notice"
|
||||
}
|
||||
}
|
||||
@if let Some(stamp) = relay_consent_stamp {
|
||||
p class="text-xs text-neutral-500" { (stamp) }
|
||||
}
|
||||
|
||||
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
|
||||
(number_field(
|
||||
"push_service_delivery_rollout_basis_points",
|
||||
@@ -2086,6 +2116,29 @@ mod tests {
|
||||
assert!(!markup.contains("at the cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_service_delivery_section_shows_the_relay_consent_toggle() {
|
||||
let accepted = PushServiceDeliveryConfigResponse {
|
||||
relay_consent_accepted: true,
|
||||
relay_consent_accepted_at: Some("2026-09-27T10:11:12.000Z".to_owned()),
|
||||
relay_consent_accepted_by: Some("1130650140672000000".to_owned()),
|
||||
..PushServiceDeliveryConfigResponse::default()
|
||||
};
|
||||
let markup = push_service_delivery_section("/admin", "csrf", &accepted).into_string();
|
||||
assert!(markup.contains("name=\"push_service_delivery_relay_consent_accepted\""));
|
||||
assert!(markup.contains("https://fluxer.com/push-relay"));
|
||||
assert!(markup.contains("Accepted 2026-09-27T10:11:12.000Z by user 1130650140672000000"));
|
||||
|
||||
let unaccepted = push_service_delivery_section(
|
||||
"/admin",
|
||||
"csrf",
|
||||
&PushServiceDeliveryConfigResponse::default(),
|
||||
)
|
||||
.into_string();
|
||||
assert!(unaccepted.contains("name=\"push_service_delivery_relay_consent_accepted\""));
|
||||
assert!(!unaccepted.contains("Accepted "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_noise_suppression_section_flags_a_list_at_its_cap() {
|
||||
let voice_noise_suppression = VoiceNoiseSuppressionConfigResponse {
|
||||
|
||||
@@ -415,7 +415,10 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
"rollout_basis_points": 5000,
|
||||
"rollout_salt": "push-service-delivery-v1",
|
||||
"included_user_ids": ["1500000000000000002"],
|
||||
"excluded_user_ids": []
|
||||
"excluded_user_ids": [],
|
||||
"relay_consent_accepted": true,
|
||||
"relay_consent_accepted_at": "2026-09-27T10:11:12.000Z",
|
||||
"relay_consent_accepted_by": "1130650140672000000"
|
||||
},
|
||||
"domain_migration": {
|
||||
"enabled": true,
|
||||
@@ -564,6 +567,7 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
assert_eq!(resp.domain_migration.included_user_ids.len(), 1);
|
||||
assert_eq!(resp.domain_migration.anonymous_rollout_basis_points, 100);
|
||||
assert!(resp.domain_migration.standalone_forwarding);
|
||||
assert!(resp.push_service_delivery.relay_consent_accepted);
|
||||
assert_eq!(resp.experiment_delivery.poll_interval_seconds, 300);
|
||||
assert!(resp.policy.single_community_guild_id.is_none());
|
||||
assert_eq!(resp.policy.services.gif_enabled, Some(true));
|
||||
@@ -596,6 +600,71 @@ fn deserialize_instance_config_response_with_unknown_keys() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_push_service_delivery_relay_consent() {
|
||||
let accepted: types::PushServiceDeliveryConfigResponse = serde_json::from_str(
|
||||
r#"{
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
"rollout_basis_points": 5000,
|
||||
"rollout_salt": "push-service-delivery-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": [],
|
||||
"relay_consent_accepted": true,
|
||||
"relay_consent_accepted_at": "2026-09-27T10:11:12.000Z",
|
||||
"relay_consent_accepted_by": "1130650140672000000"
|
||||
}"#,
|
||||
)
|
||||
.expect("an accepted relay consent must deserialize");
|
||||
|
||||
assert!(accepted.relay_consent_accepted);
|
||||
assert_eq!(
|
||||
accepted.relay_consent_accepted_at.as_deref(),
|
||||
Some("2026-09-27T10:11:12.000Z")
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.relay_consent_accepted_by.as_deref(),
|
||||
Some("1130650140672000000")
|
||||
);
|
||||
|
||||
let legacy: types::PushServiceDeliveryConfigResponse = serde_json::from_str(
|
||||
r#"{
|
||||
"enabled": true,
|
||||
"config_version": 3,
|
||||
"rollout_basis_points": 5000,
|
||||
"rollout_salt": "push-service-delivery-v1",
|
||||
"included_user_ids": [],
|
||||
"excluded_user_ids": []
|
||||
}"#,
|
||||
)
|
||||
.expect("a response written before relay consent must still deserialize");
|
||||
|
||||
assert!(!legacy.relay_consent_accepted);
|
||||
assert!(legacy.relay_consent_accepted_at.is_none());
|
||||
assert!(legacy.relay_consent_accepted_by.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_push_service_delivery_update_omits_an_unset_relay_consent() {
|
||||
let without = types::PushServiceDeliveryConfigUpdateRequest {
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&without).unwrap(),
|
||||
serde_json::json!({"enabled": true})
|
||||
);
|
||||
|
||||
let with = types::PushServiceDeliveryConfigUpdateRequest {
|
||||
relay_consent_accepted: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&with).unwrap(),
|
||||
serde_json::json!({"relay_consent_accepted": true})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_search_reports_response() {
|
||||
let json = r#"{
|
||||
|
||||
Reference in New Issue
Block a user