From 995bafa70ceba2ab01dd7440c524d4a292adbf49 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Thu, 25 Jun 2026 01:49:11 +0000 Subject: [PATCH] Add server-wide and user-only federation controls (#133) ### Description Completes #122 ### Please confirm - [x] I am the legal owner or represent the legal owner of all work submitted (including LLM-generated code, if any) - [x] I consent to my changes being added to this FOSS project - [x] I have confirmed that this does not add new errors or warnings with `utils/clippy.sh` - [ ] This PR used LLMs to generate some or all of the code changes Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/133 --- .../templates/rc_society/dashboard.html.hbs | 14 +- .../rc_society/garage_import.html.hbs | 10 +- assets/templates/rc_society/index.html.hbs | 26 +++- assets/templates/rc_society/login.html.hbs | 3 + .../templates/rc_society/stylesheet.html.hbs | 42 +++++ .../rc_society/user_federation.html.hbs | 90 +++++++++++ rc_core/src/persist/config/cubes_json.rs | 12 +- rc_core/src/persist/config/mod.rs | 2 +- rc_core/src/persist/config/traits.rs | 7 + rc_core/src/persist/federation.rs | 33 ++++ rc_core/src/persist/mod.rs | 4 + rc_core/src/persist/user/account_json.rs | 4 + rc_core/src/persist/user/common.rs | 64 ++++++++ rc_core/src/persist/user/federation.rs | 16 ++ rc_core/src/persist/user/mod.rs | 3 + rc_core/src/persist/user/traits.rs | 2 + rc_database/src/schema/user_aux.rs | 1 + rc_society/src/main.rs | 8 + rc_society/src/web/dashboard.rs | 26 +++- rc_society/src/web/index.rs | 26 +++- rc_society/src/web/mod.rs | 1 + rc_society/src/web/user_federation.rs | 143 ++++++++++++++++++ 22 files changed, 522 insertions(+), 15 deletions(-) create mode 100644 assets/templates/rc_society/user_federation.html.hbs create mode 100644 rc_core/src/persist/federation.rs create mode 100644 rc_core/src/persist/user/federation.rs create mode 100644 rc_society/src/web/user_federation.rs diff --git a/assets/templates/rc_society/dashboard.html.hbs b/assets/templates/rc_society/dashboard.html.hbs index cc5eefb..852e9f4 100644 --- a/assets/templates/rc_society/dashboard.html.hbs +++ b/assets/templates/rc_society/dashboard.html.hbs @@ -200,10 +200,18 @@

Federation

-
TODO
-
TODO
+
Enabled
+
{{form.fediverse.enabled}}
+
Blocked
+
+ {{#each form.fediverse.defederated}} + {{this}}
+ {{else}} + [None] + {{/each}} +
- +
Manage My Federation
diff --git a/assets/templates/rc_society/garage_import.html.hbs b/assets/templates/rc_society/garage_import.html.hbs index a393782..3a4eb04 100644 --- a/assets/templates/rc_society/garage_import.html.hbs +++ b/assets/templates/rc_society/garage_import.html.hbs @@ -50,7 +50,7 @@ {{else}} {{/if}} -
- + Garages - Openjam + + + + + + + + + + + + + + + + + + + + {{> stylesheet}} + + +
+
+ Welcome {{form.display_name}}! | + Home +
+

{{form.display_name}} Federation Settings

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} +
+
+ {{#if form.enabled}} +

Disable Federation

+
+
Turn Off
+
+
+ {{else}} +

Enable Federation

+
+
Turn On
+
+
+ {{/if}} +
+
+

Defederated Domains

+
+ {{#each form.defederated}} +
+
{{this}}
+
+ +
+
+ {{else}} +
No Domains
+
To Remove
+ {{/each}} +
+
+
+

Defederated A Domain

+
+
+
+
+
+
+ + +
+ + diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index e81d1a7..a3a4874 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -8,7 +8,7 @@ use polariton::serdes::TypePrefix; use crate::persist::config::SelfValidator; -use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig}; +use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig, FederationConfig}; const CUBE_CONFIG_FILENAME: &str = "config.json"; @@ -22,6 +22,8 @@ pub struct CubeConfig { factory: FactoryConfig, shop: ItemShopConfig, settings: Settings, + #[serde(default = "super::super::default_federation")] + federation: FederationConfig, } impl CubeConfig { @@ -623,7 +625,15 @@ impl super::ConfigProvider for CubeConfig { factory: self.factory.redacted_clone(), shop: self.shop.clone(), settings: self.settings.redacted_clone(), + federation: self.federation.clone(), }; serde_json::to_string(&redacted_self).expect("Failed to re-serialize config.json") } + + fn federation(&self) -> Option { + self.federation.enabled.then(|| super::Federation { + aliases: self.federation.aliases.clone(), + defederated: self.federation.defederated.clone(), + }) + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index dffb01c..98fe047 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -2,7 +2,7 @@ mod cubes_json; pub use cubes_json::CubeConfig; mod traits; -pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators, TeamChoosers, FactoryConfig, PlatformConfig}; +pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators, TeamChoosers, FactoryConfig, PlatformConfig, Federation}; pub(super) use traits::RedactedClone; mod validation; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index cb83e0e..b791f7d 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -46,6 +46,8 @@ pub trait ConfigProvider { fn garage_slot_limit(&self) -> i32; fn team_choosers(&self) -> TeamChoosers; fn redacted_json(&self) -> String; + /// None when federation is not enabled + fn federation(&self) -> Option; } pub struct DevMessageProvider { @@ -579,3 +581,8 @@ pub struct PlatformConfig { pub trait RedactedClone { fn redacted_clone(&self) -> Self; } + +pub struct Federation { + pub aliases: std::collections::HashMap, + pub defederated: Vec, +} diff --git a/rc_core/src/persist/federation.rs b/rc_core/src/persist/federation.rs new file mode 100644 index 0000000..716dafa --- /dev/null +++ b/rc_core/src/persist/federation.rs @@ -0,0 +1,33 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FederationConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_aliases")] + pub aliases: std::collections::HashMap, + #[serde(default = "default_defederated")] + pub defederated: Vec, +} + +pub fn default_federation() -> FederationConfig { + FederationConfig { + enabled: false, + aliases: default_aliases(), + defederated: default_defederated(), + } +} + +fn default_aliases() -> std::collections::HashMap { + let mut alias_map = std::collections::HashMap::with_capacity(3); + alias_map.insert("rc.ngram.ca".to_owned(), "society.rc.ngram.ca".to_owned()); + alias_map.insert("robocraft.online".to_owned(), "society.robocraft.online".to_owned()); + alias_map.insert("robocraftgame.co.uk".to_owned(), "society.robocraftgame.co.uk".to_owned()); + alias_map +} + +fn default_defederated() -> Vec { + vec![ + "robocraftgame.com".to_owned(), + ] +} diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index 7150cd9..f4fd348 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -53,6 +53,10 @@ pub use vehicle_validator::VehicleValidator; pub(crate) mod conversion; pub use conversion::{CubeConversionData, FromConversionData, ToConversionData}; +mod federation; +pub use federation::FederationConfig; +pub(crate) use federation::default_federation; + const VALID_ROBOT: &[u8] = &[64, 0, 0, diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 25542e5..a6ebf9f 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -17,6 +17,8 @@ pub struct AccountProvider { pub(super) intercom_http_client: std::sync::Arc, pub(super) secret: std::sync::Arc>, db: std::sync::Arc, + #[allow(dead_code)] + federation: Option, } impl AccountProvider { @@ -29,6 +31,7 @@ impl AccountProvider { log::debug!("Connecting to user database URI: {}", database_uri); let db = oj_rc_database::Database::init(&database_uri).await .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotConnected, e))?; + let federation_conf = >::federation(conf); Ok(Self { cubes: std::sync::Arc::new(>::ids(conf)), garage_upgrades: std::sync::Arc::new(>::garage_upgrades(conf)), @@ -42,6 +45,7 @@ impl AccountProvider { intercom_http_client: std::sync::Arc::new(reqwest::Client::new()), secret: std::sync::Arc::new(secret), db: std::sync::Arc::new(db), + federation: federation_conf, }) } diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs index 8203bcf..f9280aa 100644 --- a/rc_core/src/persist/user/common.rs +++ b/rc_core/src/persist/user/common.rs @@ -65,5 +65,69 @@ impl super::CommonUser for UserData { )) } + async fn fedi_get(&self) -> super::Federation { + match self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::Federation).await { + Ok(Some(fed)) => { + match serde_json::from_str(&fed.data) { + Ok(f) => f, + Err(e) => { + log::error!("Failed to deserialize user_aux Federation for user {}: {} (fedi_get)", self.account.id, e); + super::Federation::default() + } + } + }, + Ok(None) => { + // DB upgrade path; return default and also insert it into database + let default_fedi = super::Federation::default(); + let data_str = serde_json::to_string_pretty(&default_fedi).expect("Failed to serialize user_aux Federation data (fedi_get)"); + let model = oj_rc_database::schema::user_aux::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id), + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(chrono::Utc::now().timestamp()), + descriptor: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::user_aux::Descriptor::Federation), + data: oj_rc_database::sea_orm::ActiveValue::Set(data_str), + }; + if let Err(e) = self.db.insert_user_aux(vec![model]).await { + log::error!("Failed to insert user_aux Federation for user {}: {} (fedi_get)", self.account.id, e); + } + default_fedi + }, + Err(e) => { + log::error!("Failed to retrieve user_aux Federation for user {}: {} (fedi_get)", self.account.id, e); + super::Federation::default() + } + } + } + async fn fedi_set(&self, fedi: super::Federation) -> bool { + let data_str = serde_json::to_string_pretty(&fedi).expect("Failed to serialize user_aux Federation data (fedi_set)"); + let model = oj_rc_database::schema::user_aux::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + user_id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::NotSet, + descriptor: oj_rc_database::sea_orm::ActiveValue::NotSet, + data: oj_rc_database::sea_orm::ActiveValue::Set(data_str), + }; + match self.db.update_user_aux_by_user_id_and_descriptor(model.clone(), self.account.id, oj_rc_database::schema::user_aux::Descriptor::Federation).await { + Ok(Some(_fed)) => true, + Ok(None) => { + // DB upgrade path; insert it into database instead + let mut model = model; + model.user_id = oj_rc_database::sea_orm::ActiveValue::Set(self.account.id); + model.creation_time = oj_rc_database::sea_orm::ActiveValue::Set(chrono::Utc::now().timestamp()); + model.descriptor = oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::user_aux::Descriptor::Federation); + match self.db.insert_user_aux(vec![model]).await { + Ok(_) => true, + Err(e) => { + log::error!("Failed to insert user_aux Federation for user {}: {} (fedi_get)", self.account.id, e); + false + } + } + }, + Err(e) => { + log::error!("Failed to update user_aux Federation for user {}: {} (fedi_get)", self.account.id, e); + false + } + } + } } diff --git a/rc_core/src/persist/user/federation.rs b/rc_core/src/persist/user/federation.rs new file mode 100644 index 0000000..936f5f1 --- /dev/null +++ b/rc_core/src/persist/user/federation.rs @@ -0,0 +1,16 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Federation { + pub enabled: bool, + pub defederated: Vec, +} + +impl std::default::Default for Federation { + fn default() -> Self { + Self { + enabled: true, + defederated: Vec::default(), + } + } +} diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 0227a47..85fe672 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -28,6 +28,9 @@ mod team; pub use team::{TeamChooser, StandardTeamChooser}; mod web; +mod federation; +pub use federation::Federation; + pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; pub const USERS_DIR: &str = "accounts"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index e417e82..1e85d85 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -467,6 +467,8 @@ pub trait CommonUser: Send + Sync { async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics; async fn db_counters(&self) -> Vec<(&'static str, i64)>; async fn currency(&self, ty: CurrencyType, op: CurrencyOp) -> Result; + async fn fedi_get(&self) -> super::Federation; + async fn fedi_set(&self, fedi: super::Federation) -> bool; } #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] diff --git a/rc_database/src/schema/user_aux.rs b/rc_database/src/schema/user_aux.rs index f9c9e79..330ad35 100644 --- a/rc_database/src/schema/user_aux.rs +++ b/rc_database/src/schema/user_aux.rs @@ -44,4 +44,5 @@ pub enum Descriptor { SubscribedChannels, // Vec, JSON AvatarId, // u32, u32::MAX means custom avatar RedeemedPromoCodes, // Vec, JSON + Federation, // oj_rc_Core::persist::user::Federation, JSON } diff --git a/rc_society/src/main.rs b/rc_society/src/main.rs index 0947db8..f579e42 100644 --- a/rc_society/src/main.rs +++ b/rc_society/src/main.rs @@ -28,6 +28,7 @@ async fn main() -> std::io::Result<()> { let server_settings = actix_web::web::Data::new(>::server_config(&config)); let server_links = actix_web::web::Data::new(>::url_links(&config)); + let server_fed = actix_web::web::Data::new(>::federation(&config)); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); @@ -77,6 +78,7 @@ async fn main() -> std::io::Result<()> { .app_data(handlebars_ref.clone()) .app_data(server_settings.clone()) .app_data(server_links.clone()) + .app_data(server_fed.clone()) .app_data(auth_ref.clone()) .app_data(importers_ref.clone()) .app_data(parsers_ref.clone()) @@ -95,6 +97,12 @@ async fn main() -> std::io::Result<()> { .service(web::garage::import::get_new) .service(web::garage::import::post) .service(web::garage::selected::get) + .service(web::user_federation::get) + .service(web::user_federation::post) + .service(web::user_federation::post_remove) + .service(web::user_federation::post_add) + .service(web::user_federation::post_off) + .service(web::user_federation::post_on) .service(api::config::get) }) .bind((cli_args.ip, cli_args.port))? diff --git a/rc_society/src/web/dashboard.rs b/rc_society/src/web/dashboard.rs index 658e8b4..8a01f82 100644 --- a/rc_society/src/web/dashboard.rs +++ b/rc_society/src/web/dashboard.rs @@ -15,6 +15,7 @@ struct RenderData { account: AccountData, sanction: SanctionData, social: SocialData, + fediverse: FederationData, } #[derive(Serialize, Deserialize)] @@ -87,6 +88,12 @@ struct SocialData { chats: Vec, } +#[derive(Serialize, Deserialize)] +struct FederationData { + enabled: bool, + defederated: Vec, +} + pub async fn dashboard_impl(handlebars_ref: Data>, auth: Data>, factory: Data, server_config: Data, user_opt: Option, req: HttpRequest) -> Result { match super::try_auth_user(user_opt, auth.as_ref(), &req).await? { super::LoginReturn::AuthFail(resp) => Ok(resp), @@ -111,6 +118,10 @@ pub async fn dashboard_impl(handlebars_ref: Data>, au Ok(x) => x, Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)), }; + let fedi_stats = match build_fedi_data(user.as_ref()).await { + Ok(x) => x, + Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)), + }; Ok(super::render_ok( RenderData { display_name: user.display_name().to_owned(), @@ -128,6 +139,7 @@ pub async fn dashboard_impl(handlebars_ref: Data>, au account: account_stats, sanction: sanction_stats, social: social_stats, + fediverse: fedi_stats, }, handlebars_ref.as_ref(), FORM_NAME, @@ -193,7 +205,11 @@ fn fallback_render(error: Box, user: &dyn oj_rc_core::per clan: None, friends: 0, friends_of: 0, - chats: Vec::default() + chats: Vec::default(), + }, + fediverse: FederationData { + enabled: false, + defederated: Vec::default(), } }, format!("Dashboard loading failed: {}", error), @@ -304,6 +320,14 @@ async fn build_social_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Res }) } +async fn build_fedi_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Result> { + let info = user.fedi_get().await; + Ok(FederationData { + enabled: info.enabled, + defederated: info.defederated, + }) +} + #[get("/dashboard")] pub async fn get(handlebars_ref: Data>, auth: Data>, factory: Data, server_config: Data, user_opt: Option, req: HttpRequest) -> Result { dashboard_impl(handlebars_ref, auth, factory, server_config, user_opt, req).await diff --git a/rc_society/src/web/index.rs b/rc_society/src/web/index.rs index e95be25..f9e4f89 100644 --- a/rc_society/src/web/index.rs +++ b/rc_society/src/web/index.rs @@ -10,6 +10,7 @@ struct RenderData { display_name: Option, server: ServerDetails, links: LinkDetails, + fediverse: FederationDetails, } #[derive(Serialize, Deserialize)] @@ -57,10 +58,31 @@ fn links_details(links: &oj_rc_core::persist::config::LinksConfig) -> LinkDetail } } +#[derive(Serialize, Deserialize)] +struct FederationDetails { + enabled: bool, + alias: std::collections::HashMap, + defederated: Vec, +} + +fn fedi_details(fedi: &Option) -> FederationDetails { + fedi.as_ref().map(|f| FederationDetails { + enabled: true, + alias: f.aliases.clone(), + defederated: f.defederated.clone(), + }) + .unwrap_or_else(|| FederationDetails { + enabled: false, + alias: Default::default(), + defederated: Default::default(), + }) +} + #[get("/")] -pub async fn get(handlebars_ref: Data>, auth: Data>, server_config: Data, server_links: Data, user_opt: Option, req: HttpRequest) -> Result { +pub async fn get(handlebars_ref: Data>, auth: Data>, server_config: Data, server_links: Data, server_fedi: Data>, user_opt: Option, req: HttpRequest) -> Result { let server_info = server_details(server_config.as_ref()); let links_info = links_details(server_links.as_ref()); + let federation_info = fedi_details(server_fedi.as_ref()); if let Some(user) = user_opt { match super::try_auth_user(Some(user), auth.as_ref(), &req).await? { super::LoginReturn::AuthFail(resp) => Ok(resp), @@ -71,6 +93,7 @@ pub async fn get(handlebars_ref: Data>, auth: Data>, auth: Data, +} + +async fn list_impl(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let fedi = user.fedi_get().await; + let html = render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + enabled: fedi.enabled, + defederated: fedi.defederated, + }, + handlebars_ref.as_ref(), + FORM_NAME, + ); + Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} + +#[get("/federation/list")] +pub async fn get(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + list_impl(handlebars_ref, auth, user_opt, req).await +} + +#[post("/federation/list")] +pub async fn post(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + list_impl(handlebars_ref, auth, user_opt, req).await +} + +#[post("/federation/list/remove/{domain}")] +pub async fn post_remove(auth: Data>, user_opt: Option, req: HttpRequest, domain: Path) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let mut fedi = user.fedi_get().await; + if let Some((i, _domain_name)) = fedi.defederated.iter().enumerate().find(|(_i, domain_name)| *domain_name == &*domain) { + fedi.defederated.remove(i); + user.fedi_set(fedi).await; + } else { + log::warn!("Failed to find domain {} in defederated list for user {}", &*domain, user.public_id()); + } + let resp = Redirect::to("/federation/list") + .respond_to(&req) + .map_into_boxed_body(); + Ok( + resp + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} + +#[derive(Serialize, Deserialize)] +struct AddForm { + domain: String, +} + +#[post("/federation/list/add")] +pub async fn post_add(auth: Data>, user_opt: Option, req: HttpRequest, form: Form) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let mut fedi = user.fedi_get().await; + let sanitized_domain = form.domain.trim().to_lowercase(); + if fedi.defederated.contains(&sanitized_domain) { + log::warn!("Domain {} already in defederated list for user {}", sanitized_domain, user.public_id()); + } else { + fedi.defederated.push(sanitized_domain); + user.fedi_set(fedi).await; + } + let resp = Redirect::to("/federation/list") + .respond_to(&req) + .map_into_boxed_body(); + Ok( + resp + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} + +#[post("/federation/off")] +pub async fn post_off(auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let mut fedi = user.fedi_get().await; + fedi.enabled = false; + user.fedi_set(fedi).await; + let resp = Redirect::to("/federation/list") + .respond_to(&req) + .map_into_boxed_body(); + Ok( + resp + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} + +#[post("/federation/on")] +pub async fn post_on(auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let mut fedi = user.fedi_get().await; + fedi.enabled = true; + user.fedi_set(fedi).await; + let resp = Redirect::to("/federation/list") + .respond_to(&req) + .map_into_boxed_body(); + Ok( + resp + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +}