fix(self-hosting): use instance contacts and live product name (#3333)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-09 01:39:35 +02:00
1 parent 454fefdb92
commit a5c1362f4a
308 files changed
+16533 -13512

No files matched your search

+9 -7
View File
@@ -5,7 +5,7 @@
"title": "Fluxer API",
"version": "1.0.0",
"description": "API for Fluxer, a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.",
"contact": {"name": "Fluxer Platform AB", "email": "support@fluxer.app"},
"contact": {"name": "Fluxer Platform AB", "email": "support@fluxer.com"},
"license": {"name": "AGPL-3.0", "url": "https://www.gnu.org/licenses/agpl-3.0.html"}
},
"servers": [{"url": "https://api.fluxer.app/v1", "description": "Production API"}],
@@ -11362,9 +11362,10 @@
"type": "object",
"properties": {
"terms_url": {"nullable": true, "type": "string"},
"privacy_url": {"nullable": true, "type": "string"}
"privacy_url": {"nullable": true, "type": "string"},
"guidelines_url": {"nullable": true, "type": "string"}
},
"required": ["terms_url", "privacy_url"],
"required": ["terms_url", "privacy_url", "guidelines_url"],
"additionalProperties": false
},
"registration": {
@@ -11818,7 +11819,8 @@
"type": "object",
"properties": {
"terms_url": {"nullable": true, "type": "string", "maxLength": 2048},
"privacy_url": {"nullable": true, "type": "string", "maxLength": 2048}
"privacy_url": {"nullable": true, "type": "string", "maxLength": 2048},
"guidelines_url": {"nullable": true, "type": "string", "maxLength": 2048}
}
},
"registration": {
@@ -17474,8 +17476,8 @@
"IP authorization is required.",
"IP authorization resend is on cooldown.",
"IP authorization resend limit exceeded.",
"Your IP address {ipAddress} has been permanently blocked from the Fluxer API by platform administrators. If you believe this is a mistake, contact [email protected] to appeal. Include this IP address in your appeal.",
"Your IP address {ipAddress} has been temporarily blocked from the Fluxer API. The block lifts on its own when it expires. If you think this is a mistake, contact [email protected] and include this IP address.",
"Your IP address {ipAddress} has been permanently blocked from the {product_name} API by platform administrators. {appealEmail, select, null {If you believe this is a mistake, contact the administrators of this instance to appeal.} other {If you believe this is a mistake, contact {appealEmail} to appeal.}} Include this IP address in your appeal.",
"Your IP address {ipAddress} has been temporarily blocked from the {product_name} API. The block lifts on its own when it expires. {appealEmail, select, null {If you think this is a mistake, contact the administrators of this instance and include this IP address.} other {If you think this is a mistake, contact {appealEmail} and include this IP address.}}",
"This IP address cannot be added to the blocklist.",
"You've reached the maximum of {limit, plural, one {# application} other {# applications}}.",
"You've reached the maximum of {count, plural, one {# bookmark} other {# bookmarks}}.",
@@ -17543,7 +17545,7 @@
"Test purchases cannot be applied to this account.",
"Stream key channel mismatch.",
"Stream key scope mismatch.",
"Payment processing encountered an error. Please try again or contact support.",
"{supportEmail, select, null {Payment processing encountered an error. Please try again or contact the administrators of this instance.} other {Payment processing encountered an error. Please try again or contact support.}}",
"Gift code redemption is in progress. Try again in a moment.",
"Invalid product selection.",
"Invalid product configuration.",
@@ -417,6 +417,8 @@ pub struct AppSetupConfigResponse {
pub struct AppLegalConfigResponse {
pub terms_url: Option<String>,
pub privacy_url: Option<String>,
#[serde(default)]
pub guidelines_url: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -927,6 +929,8 @@ pub struct AppLegalConfigUpdateRequest {
pub terms_url: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privacy_url: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guidelines_url: Option<Option<String>>,
}
#[derive(Clone, Debug, Default, Serialize)]
+42
View File
@@ -709,6 +709,7 @@ fn build_app_legal_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
legal: Some(AppLegalConfigUpdateRequest {
terms_url: optional("app_terms_url"),
privacy_url: optional("app_privacy_url"),
guidelines_url: optional("app_guidelines_url"),
}),
registration: None,
}),
@@ -1158,6 +1159,47 @@ pub async fn limit_config_post(
mod tests {
use super::*;
#[test]
fn build_app_legal_update_sends_the_guidelines_url_and_clears_blank_fields() {
let form = MultiValueForm::parse(
b"app_terms_url=+https%3A%2F%2Fexample.com%2Fterms+&app_privacy_url=&app_guidelines_url=https%3A%2F%2Fexample.com%2Frules",
);
let request = build_app_legal_update(&form);
let legal = request
.app_public
.expect("app public update")
.legal
.expect("legal update");
assert_eq!(
legal.terms_url,
Some(Some("https://example.com/terms".to_owned()))
);
assert_eq!(legal.privacy_url, Some(None));
assert_eq!(
legal.guidelines_url,
Some(Some("https://example.com/rules".to_owned()))
);
let body = serde_json::to_value(&legal).expect("serialize legal update");
assert_eq!(
body,
serde_json::json!({
"terms_url": "https://example.com/terms",
"privacy_url": null,
"guidelines_url": "https://example.com/rules"
})
);
let cleared = build_app_legal_update(&MultiValueForm::parse(
b"app_terms_url=&app_privacy_url=&app_guidelines_url=+",
));
let body = serde_json::to_value(cleared.app_public.expect("app public").legal)
.expect("serialize cleared legal update");
assert_eq!(
body,
serde_json::json!({"terms_url": null, "privacy_url": null, "guidelines_url": null})
);
}
#[test]
fn build_integrations_update_leaves_email_alone_when_its_fields_are_hidden() {
let hidden = build_integrations_update(&MultiValueForm::parse(
@@ -167,6 +167,7 @@ pub fn instance_config_page(
csrf_token,
&instance_config.integrations,
instance_config.account_identity.mode,
&instance_config.app_public.branding.product_name,
))
},
))
@@ -568,6 +569,7 @@ fn integrations_config_section(
csrf_token: &str,
integrations: &InstanceIntegrationsResponse,
account_identity: AccountIdentityMode,
product_name: &str,
) -> Markup {
let smtp_port = integrations
.email
@@ -629,7 +631,7 @@ fn integrations_config_section(
"integration_email_from_name",
"From name",
integrations.email.from_name.as_deref().unwrap_or(""),
"Fluxer",
product_name,
))
(text_input(
"integration_smtp_host",
@@ -955,9 +957,18 @@ fn app_public_config_section(
app_public.legal.privacy_url.as_deref().unwrap_or(""),
"https://example.com/privacy",
))
(text_input(
"app_guidelines_url",
"Community Guidelines URL",
app_public.legal.guidelines_url.as_deref().unwrap_or(""),
"https://example.com/guidelines",
))
}
p class="text-xs text-neutral-500" {
"Registration asks for agreement only to the documents configured here. Leave both blank to hide legal consent on self-hosted registration."
"Registration asks for agreement only to the terms and privacy documents configured here. Leave both blank to hide legal consent on self-hosted registration."
}
p class="text-xs text-neutral-500" {
"The community guidelines URL is linked from the app, the report flow and enforcement emails. Use an absolute http or https URL. Leave it blank to hide those links."
}
(form_actions(html! {
(submit_button("Save Legal Documents"))
@@ -1966,6 +1977,7 @@ mod tests {
"csrf",
&integrations,
AccountIdentityMode::Username,
"Fluxer",
)
.into_string();
assert!(!username.contains("Email delivery"));
@@ -1978,6 +1990,7 @@ mod tests {
"csrf",
&integrations,
AccountIdentityMode::Email,
"Fluxer",
)
.into_string();
assert!(email.contains("Email delivery"));
+46 -1
View File
@@ -17,7 +17,7 @@ fn deserialize_admin_users_me_response() {
"premium_flags": 2,
"avatar": "563de430",
"banner": "bd221c57",
"bio": "For support, please contact support@fluxer.app.",
"bio": "For support, please contact support@fluxer.com.",
"pronouns": "he/him",
"accent_color": 2631308,
"email": "[email protected]",
@@ -1327,6 +1327,51 @@ fn deserialize_report_legal_hold_fields_and_hold_response() {
}
}
#[test]
fn instance_legal_config_carries_the_guidelines_url() {
let with: types::AppLegalConfigResponse = serde_json::from_value(serde_json::json!({
"terms_url": "https://example.com/terms",
"privacy_url": null,
"guidelines_url": "https://example.com/rules"
}))
.expect("legal config with guidelines");
assert_eq!(
with.guidelines_url.as_deref(),
Some("https://example.com/rules")
);
let generated: generated_types::InstanceConfigResponseAppPublicLegal =
serde_json::from_value(serde_json::json!({
"terms_url": null,
"privacy_url": null,
"guidelines_url": "https://example.com/rules"
}))
.expect("generated legal config");
assert_eq!(
generated.guidelines_url.as_deref(),
Some("https://example.com/rules")
);
let old: types::AppLegalConfigResponse = serde_json::from_value(serde_json::json!({
"terms_url": "https://example.com/terms",
"privacy_url": "https://example.com/privacy"
}))
.expect("legal config from an older api");
assert!(old.guidelines_url.is_none());
let update = types::AppLegalConfigUpdateRequest {
terms_url: Some(None),
privacy_url: None,
guidelines_url: Some(Some("https://example.com/rules".to_owned())),
};
let body = serde_json::to_value(&update).expect("serialize legal update");
assert_eq!(
body,
serde_json::json!({"terms_url": null, "guidelines_url": "https://example.com/rules"})
);
let accepted: generated_types::InstanceConfigUpdateRequestAppPublicLegal =
serde_json::from_value(body).expect("the update matches the generated request contract");
assert!(accepted.guidelines_url.is_some());
}
#[test]
fn deserialize_webhook_report_with_a_bot_creator_and_a_deleted_creator() {
let bot = decode_report_both_ways(report_json(serde_json::json!({
+84
View File
@@ -812,6 +812,90 @@ async fn report_legal_hold_form_places_and_clears_the_hold() {
assert_eq!(received.lock().unwrap().len(), 3);
}
#[tokio::test]
async fn instance_config_legal_form_round_trips_the_guidelines_url() {
let patches = Arc::new(Mutex::new(Vec::<Value>::new()));
let sink = patches.clone();
let configured = || {
let mut config = instance_config();
config["self_hosted"] = json!(true);
config["app_public"] = json!({
"branding": {"product_name": "Fluxer", "premium_product_name": "Premium"},
"setup": {"configured": true},
"legal": {
"terms_url": "https://example.com/terms",
"privacy_url": null,
"guidelines_url": "https://example.com/community-guidelines"
},
"registration": {"collect_date_of_birth": true}
});
config
};
let api = Router::new()
.route(
"/admin/instance/config",
routing::get(move || async move { json_response(configured()) }).patch(
move |Json(body): Json<Value>| {
let sink = sink.clone();
async move {
sink.lock().unwrap().push(body);
json_response(configured())
}
},
),
)
.fallback(mock_api);
let app = setup_with_api(api).await;
let (headers, page) = get_with_headers(&app, "/instance-config", &[]).await;
assert!(
page.contains(r#"id="app_guidelines_url" name="app_guidelines_url" value="https://example.com/community-guidelines""#),
"{page}"
);
assert!(page.contains("Community Guidelines URL"), "{page}");
let csrf_token = csrf_cookie(&headers)
.unwrap_or_else(|| panic!("instance config page did not set csrf_token cookie\n{page}"));
assert_form_has_csrf(
&page,
"/instance-config?action=update_app_legal",
&csrf_token,
);
let cookie = format!("{}; csrf_token={}", app.session_cookie, csrf_token);
for form in [
format!(
"_csrf={csrf_token}&app_terms_url=https%3A%2F%2Fexample.com%2Fterms&app_privacy_url=&app_guidelines_url=https%3A%2F%2Frules.example.org%2Fguidelines"
),
format!("_csrf={csrf_token}&app_terms_url=&app_privacy_url=&app_guidelines_url="),
] {
let (status, _, body) = post_form_with_headers(
&app,
"/instance-config?action=update_app_legal",
&[("Cookie", &cookie)],
&form,
)
.await;
assert!(
status.is_redirection() || status.is_success(),
"{status} {body}"
);
}
let patches = patches.lock().unwrap();
assert_eq!(
*patches,
vec![
json!({"app_public": {"legal": {
"terms_url": "https://example.com/terms",
"privacy_url": null,
"guidelines_url": "https://rules.example.org/guidelines"
}}}),
json!({"app_public": {"legal": {
"terms_url": null,
"privacy_url": null,
"guidelines_url": null
}}}),
]
);
}
#[tokio::test]
async fn user_fragment_alias_returns_drawer_fragment() {
let app = setup().await;