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

rc_society dashboard display (#131)

### Description

Closes #130

### 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/131
This commit is contained in:
NG (Graham)
2026-06-20 21:48:09 +00:00
committed by NGnius
parent d2626bb47f
commit b597e1337d
19 changed files with 741 additions and 109 deletions

View File

@@ -28,6 +28,16 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory {
}
}
async fn count(&self, query: libfj::robocraft::ListQuery) -> Result<usize, Box<dyn std::error::Error>> {
match self {
Self::Arc(x) => x.count(query).await,
Self::Primary(x) => x.count(query).await,
Self::Web(x) => x.count(query).await,
Self::Custom(x) => x.count(query).await,
Self::None => Ok(0),
}
}
async fn upload(&self, vehicle: oj_rc_factory::VehicleUploadInfo) -> Result<oj_rc_factory::VehicleThumbnailInfo, Box<dyn std::error::Error>> {
match self {
Self::Arc(x) => x.upload(vehicle).await,

View File

@@ -323,6 +323,7 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
cdn_url: self.settings.server.cdn_url.trim_end_matches('/').to_owned(),
auth_url: self.settings.server.auth_url.trim_end_matches('/').to_owned(),
intercom_url: self.settings.server.intercom_url.trim_end_matches('/').to_owned(),
factory_url: self.settings.server.factory_url.trim_end_matches('/').to_owned(),
minimum_version: self.settings.server.min_version as i32,
dos_protect: self.settings.server.dos_protection,
maintenance_message: self.settings.server.maintenance_message.clone(),

View File

@@ -100,6 +100,7 @@ pub struct ServerConfig {
pub cdn_url: String,
pub auth_url: String,
pub intercom_url: String,
pub factory_url: String,
pub minimum_version: i32,
pub dos_protect: bool,
pub maintenance_message: Option<String>,

View File

@@ -96,6 +96,8 @@ pub struct ServerSettings {
pub auth_url: String,
#[serde(default = "default_intercom_root_url")]
pub intercom_url: String,
#[serde(default = "default_factory_url")]
pub factory_url: String,
#[serde(default = "default_feedback_url")]
pub feedback_url: String,
#[serde(default = "default_support_url")]
@@ -132,6 +134,7 @@ fn default_server_conf() -> ServerSettings {
cdn_url: default_cdn_root_url(),
auth_url: default_auth_root_url(),
intercom_url: default_intercom_root_url(),
factory_url: default_factory_url(),
feedback_url: default_feedback_url(),
support_url: default_support_url(),
wiki_url: default_wiki_url(),
@@ -157,6 +160,10 @@ fn default_intercom_root_url() -> String {
"ws://127.0.0.1:8001".to_owned()
}
fn default_factory_url() -> String {
"http://127.0.0.1:8012".to_owned()
}
fn default_feedback_url() -> String {
"https://mstdn.ca/@ngram".to_owned()
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo, GarageWebStats, SanctionWebStats, AccountWebStats, SocialWebStats};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;

View File

@@ -468,7 +468,7 @@ pub trait CommonUser: Send + Sync {
async fn currency(&self, ty: CurrencyType, op: CurrencyOp) -> Result<u64, polariton_server::operations::SimpleOpError>;
}
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
pub enum CurrencyType {
Free,
Paid,
@@ -680,6 +680,10 @@ pub trait WebUser: CommonUser {
async fn garage_by_id(&self, id: i32) -> Result<Option<VehicleData>, Box<dyn std::error::Error>>;
async fn save_garage(&self, vehicle: crate::persist::user::VehicleData, garage_id: Option<i32>, cpu_counter: &crate::cubes::CpuListParser, weapon_orderer: &crate::cubes::WeaponListParser) -> Result<(), Box<dyn std::error::Error>>;
async fn garage_id_selected(&self) -> Result<Option<i32>, Box<dyn std::error::Error>>;
async fn garage_stats(&self) -> Result<GarageWebStats, Box<dyn std::error::Error>>;
async fn account_stats(&self) -> Result<AccountWebStats, Box<dyn std::error::Error>>;
async fn sanction_stats(&self) -> Result<SanctionWebStats, Box<dyn std::error::Error>>;
async fn social_stats(&self) -> Result<SocialWebStats, Box<dyn std::error::Error>>;
}
pub struct GarageWebInfo {
@@ -691,6 +695,41 @@ pub struct GarageWebInfo {
pub creation_time: i64,
}
pub struct GarageWebStats {
pub vehicle_total: u64,
pub regular_vehicle_total: u64,
pub mega_vehicle_total: u64,
pub empty_vehicle_total: u64,
pub factory_vehicle_total: u64,
pub storage_bytes_total: Option<u64>,
pub selected_garage: i32,
}
pub struct AccountWebStats {
pub currencies: std::collections::HashMap<CurrencyType, u64>,
pub games_played: u64,
pub avatar_id: Option<i32>,
pub premium_until: i64,
pub rank: u32,
}
pub struct SanctionWebStats {
pub total: u64,
pub pending_total: u64,
pub warn_total: u64,
pub mute_total: u64,
pub muted_until: Option<i64>,
}
pub struct SocialWebStats {
pub clan: Option<ClanData>,
/// Friend source is user
pub friends_total: u64,
/// Friend target is user
pub friends_of_total: u64,
pub chats: Vec<String>,
}
#[async_trait::async_trait]
pub trait Userless: Send + Sync {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error>;

View File

@@ -106,4 +106,100 @@ impl super::WebUser for super::account_json::UserData {
Ok(self.db.garage_selected(self.account.id).await?
.map(|x| x.id))
}
async fn garage_stats(&self) -> Result<super::GarageWebStats, Box<dyn std::error::Error>> {
let vehicle_count = self.db.garage_count_by_user_id(self.account.id).await?;
let regular_count = self.db.garage_count_by_user_id_between(self.account.id, 2_000, 1).await?;
let mega_count = self.db.garage_count_by_user_id_between(self.account.id, u32::MAX as u64, 2_000).await?;
let empty_count = self.db.garage_count_by_user_id_between(self.account.id, 0, 0).await?;
let factory_count = self.db.garage_count_by_user_id_factory(self.account.id).await?;
let storage_size = self.db.garage_storage_by_user_id(self.account.id).await?;
let selected_garage_slot = self.db.garage_selected(self.account.id).await?.map(|g| g.slot).unwrap_or_default();
Ok(super::GarageWebStats {
vehicle_total: vehicle_count,
regular_vehicle_total: regular_count,
mega_vehicle_total: mega_count,
empty_vehicle_total: empty_count,
factory_vehicle_total: factory_count,
storage_bytes_total: storage_size,
selected_garage: selected_garage_slot,
})
}
async fn account_stats(&self) -> Result<super::AccountWebStats, Box<dyn std::error::Error>> {
let user_aux_data = self.db.user_aux_by_user_id(self.account.id).await?;
let mut currency_map = std::collections::HashMap::with_capacity(8);
let mut avatar_id = None;
let mut premium_until = 0;
let mut user_rank = 0;
for row in user_aux_data.iter() {
match row.descriptor {
oj_rc_database::schema::user_aux::Descriptor::UserXP => {
currency_map.insert(super::CurrencyType::Experience, row.data.parse().unwrap_or_default());
},
oj_rc_database::schema::user_aux::Descriptor::TechPoints => {
currency_map.insert(super::CurrencyType::TechPoints, row.data.parse().unwrap_or_default());
},
oj_rc_database::schema::user_aux::Descriptor::UserFreeCurrency => {
currency_map.insert(super::CurrencyType::Free, row.data.parse().unwrap_or_default());
},
oj_rc_database::schema::user_aux::Descriptor::UserPaidCurrency => {
currency_map.insert(super::CurrencyType::Paid, row.data.parse().unwrap_or_default());
},
oj_rc_database::schema::user_aux::Descriptor::AvatarId => {
avatar_id = row.data.parse::<i32>().ok();
},
oj_rc_database::schema::user_aux::Descriptor::PremiumExpiry => {
premium_until = row.data.parse().unwrap_or_default();
},
oj_rc_database::schema::user_aux::Descriptor::UserRank => {
user_rank = row.data.parse().unwrap_or_default();
},
_ => {},
}
}
let game_count = self.db.count_games_by_user_id(self.account.id).await?;
Ok(super::AccountWebStats {
currencies: currency_map,
games_played: game_count,
avatar_id,
premium_until,
rank: user_rank,
})
}
async fn sanction_stats(&self) -> Result<super::SanctionWebStats, Box<dyn std::error::Error>> {
let all_count = self.db.count_sanctions_by_user_id(self.account.id).await?;
let pending_count = self.db.count_sanctions_by_user_id_and_ack(self.account.id, false).await?;
let warn_count = self.db.count_sanctions_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::sanction::Descriptor::Warn).await?;
let mute_count = self.db.count_sanctions_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::sanction::Descriptor::Mute).await?;
let first_active_mute = self.db.sanction_by_user_id_and_descriptor_and_active(self.account.id, oj_rc_database::schema::sanction::Descriptor::Mute).await?;
Ok(super::SanctionWebStats {
total: all_count,
pending_total: pending_count,
warn_total: warn_count,
mute_total: mute_count,
muted_until: first_active_mute.map(|s| s.creation_time + s.duration.unwrap_or_default()),
})
}
async fn social_stats(&self) -> Result<super::SocialWebStats, Box<dyn std::error::Error>> {
let clan_opt = self.db.clan_by_user_id(self.account.id).await?
.map(|(clan, _myself)| super::ClanData {
name: clan.name,
description: clan.description,
ty: super::ClanType::db_to_core(&clan.variant),
size: 0,
});
let friends_count = self.db.count_friends_by_user_id(self.account.id, oj_rc_database::schema::friend::FriendStatus::Accepted).await?;
let friends_of_count = self.db.count_friends_target_by_user_id(self.account.id, oj_rc_database::schema::friend::FriendStatus::Accepted).await?;
let subbed_chats_aux = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::SubscribedChannels).await?;
let chat_list: Vec<String> = subbed_chats_aux.and_then(|aux| serde_json::from_str(&aux.data).ok()).unwrap_or_default();
Ok(super::SocialWebStats {
clan: clan_opt,
friends_total: friends_count,
friends_of_total: friends_of_count,
chats: chat_list,
})
}
}