1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

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
This commit is contained in:
NG (Graham)
2026-06-25 01:49:11 +00:00
committed by NGnius
parent e65d0eeb80
commit 995bafa70c
22 changed files with 522 additions and 15 deletions

View File

@@ -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 <C: Clone + Send> super::ConfigProvider<C> 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<super::Federation> {
self.federation.enabled.then(|| super::Federation {
aliases: self.federation.aliases.clone(),
defederated: self.federation.defederated.clone(),
})
}
}

View File

@@ -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;

View File

@@ -46,6 +46,8 @@ pub trait ConfigProvider<C: Clone> {
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<Federation>;
}
pub struct DevMessageProvider<C: Clone> {
@@ -579,3 +581,8 @@ pub struct PlatformConfig {
pub trait RedactedClone {
fn redacted_clone(&self) -> Self;
}
pub struct Federation {
pub aliases: std::collections::HashMap<String, String>,
pub defederated: Vec<String>,
}

View File

@@ -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<String, String>,
#[serde(default = "default_defederated")]
pub defederated: Vec<String>,
}
pub fn default_federation() -> FederationConfig {
FederationConfig {
enabled: false,
aliases: default_aliases(),
defederated: default_defederated(),
}
}
fn default_aliases() -> std::collections::HashMap<String, String> {
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<String> {
vec![
"robocraftgame.com".to_owned(),
]
}

View File

@@ -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,

View File

@@ -17,6 +17,8 @@ pub struct AccountProvider {
pub(super) intercom_http_client: std::sync::Arc<reqwest::Client>,
pub(super) secret: std::sync::Arc<Vec<u8>>,
db: std::sync::Arc<oj_rc_database::Database>,
#[allow(dead_code)]
federation: Option<crate::persist::config::Federation>,
}
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 = <crate::persist::config::ConfigImpl as ConfigProvider<()>>::federation(conf);
Ok(Self {
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)),
garage_upgrades: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::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,
})
}

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,16 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Federation {
pub enabled: bool,
pub defederated: Vec<String>,
}
impl std::default::Default for Federation {
fn default() -> Self {
Self {
enabled: true,
defederated: Vec::default(),
}
}
}

View File

@@ -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";

View File

@@ -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<u64, polariton_server::operations::SimpleOpError>;
async fn fedi_get(&self) -> super::Federation;
async fn fedi_set(&self, fedi: super::Federation) -> bool;
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]