diff --git a/Cargo.lock b/Cargo.lock index 0a4f823..a2b540c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3326,6 +3326,7 @@ dependencies = [ "log", "oj_convert", "oj_rc_core", + "oj_rc_factory", "oj_rc_plugins", "serde", "serde_json", diff --git a/assets/templates/rc_society/dashboard.html.hbs b/assets/templates/rc_society/dashboard.html.hbs index 787c057..cc5eefb 100644 --- a/assets/templates/rc_society/dashboard.html.hbs +++ b/assets/templates/rc_society/dashboard.html.hbs @@ -72,8 +72,20 @@

Garages

-
Total
-
{{form.debug.user_id}}/{{form.debug.user_id}}
+
Total
+
{{form.garage.total}}
+
Regulars
+
{{form.garage.regular_total}}
+
Megabots
+
{{form.garage.mega_total}}
+
Empty
+
{{form.garage.empty_total}}
+
Selected Slot
+
#{{form.garage.selected_slot}}
+ {{#if form.garage.bytes_total }} +
Storage
+
{{form.garage.bytes_total}}B
+ {{/if}}
@@ -84,10 +96,12 @@

Account

+
+ {{#if form.account.is_custom_avatar}} +
Avatar
+
+ {{else}} +
Avatar
+
{{form.account.avatar_id}}
+ {{/if}} +
Rank
+
{{form.account.rank}}
+
Currency
+
+ Robits: {{form.account.currencies.robits}}
+ GC: {{form.account.currencies.gc}}
+ TP: {{form.account.currencies.tp}}
+ XP: {{form.account.currencies.xp}}
+
+
Games
+
{{form.account.games}}
+
Premium Until
+
+
Manage My Account @@ -104,8 +144,20 @@

Sanctions

-
Total
-
{{form.debug.user_id}}
+
Total
+
{{form.sanction.total}}
+ {{#if (ne form.sanction.pending 0)}} +
Pending
+
{{form.sanction.pending}}
+ {{/if}} +
Warnings
+
{{form.sanction.warns}}
+
Mutes
+
{{form.sanction.mutes}}
+ {{#if form.sanction.muted_until_iso}} +
Muted Until
+
+ {{/if}}
@@ -116,17 +168,40 @@

Social

-
Clan
-
{{form.display_name}}
-
Friends
-
{{form.debug.user_id}}
+ {{#if form.social.clan}} +
Clan
+
+ {{form.social.clan.name}} + + Clan avatar picture for {{form.social.clan.name}} + + {{form.social.clan.ty}} + {{form.social.clan.description}} +
+ {{else}} +
Clan
+
[no clan]
+ {{/if}} +
Friends
+
+ {{form.social.friends}} + {{#if (ne form.social.friends form.social.friends_of)}} + = {{form.social.friends_of}}[!] + {{/if}} +
+
Chats
+
+ {{#each form.social.chats}} + {{this}}
+ {{/each}} +
- Grid item 2 +

Server Links

+
-
+
diff --git a/assets/templates/rc_society/stylesheet.html.hbs b/assets/templates/rc_society/stylesheet.html.hbs index 88c05bb..c8cd0c7 100644 --- a/assets/templates/rc_society/stylesheet.html.hbs +++ b/assets/templates/rc_society/stylesheet.html.hbs @@ -44,6 +44,11 @@ a:link { color: darkgrey; } a:hover { color: white; } a:visited { color: lightgrey; } a:active { color: white; } +a.box-details-link { text-decoration-line: underline; } +a.box-details-link:link { color: black; } +a.box-details-link:hover { color: white; } +a.box-details-link:visited { color: black; } +a.box-details-link:active { color: white; } form { padding-top: 25px; padding-left: 25px; diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs index 2fec3a1..4273d08 100644 --- a/rc_core/src/factory/adapter_enum.rs +++ b/rc_core/src/factory/adapter_enum.rs @@ -28,6 +28,16 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { } } + async fn count(&self, query: libfj::robocraft::ListQuery) -> Result> { + 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> { match self { Self::Arc(x) => x.upload(vehicle).await, diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index af0e783..f5a920a 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -323,6 +323,7 @@ impl super::ConfigProvider 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(), diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index b4ccd45..bbcb950 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -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, diff --git a/rc_core/src/persist/settings.rs b/rc_core/src/persist/settings.rs index e5383a5..2ff45b6 100644 --- a/rc_core/src/persist/settings.rs +++ b/rc_core/src/persist/settings.rs @@ -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() } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 9a5a43c..0227a47 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -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; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 3d033a7..f307224 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -468,7 +468,7 @@ pub trait CommonUser: Send + Sync { async fn currency(&self, ty: CurrencyType, op: CurrencyOp) -> Result; } -#[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, Box>; async fn save_garage(&self, vehicle: crate::persist::user::VehicleData, garage_id: Option, cpu_counter: &crate::cubes::CpuListParser, weapon_orderer: &crate::cubes::WeaponListParser) -> Result<(), Box>; async fn garage_id_selected(&self) -> Result, Box>; + async fn garage_stats(&self) -> Result>; + async fn account_stats(&self) -> Result>; + async fn sanction_stats(&self) -> Result>; + async fn social_stats(&self) -> Result>; } 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, + pub selected_garage: i32, +} + +pub struct AccountWebStats { + pub currencies: std::collections::HashMap, + pub games_played: u64, + pub avatar_id: Option, + 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, +} + +pub struct SocialWebStats { + pub clan: Option, + /// Friend source is user + pub friends_total: u64, + /// Friend target is user + pub friends_of_total: u64, + pub chats: Vec, +} + #[async_trait::async_trait] pub trait Userless: Send + Sync { async fn lobby_state_listener(&self) -> Result, reqwest_websocket::Error>; diff --git a/rc_core/src/persist/user/web.rs b/rc_core/src/persist/user/web.rs index bdcd593..fcaf94a 100644 --- a/rc_core/src/persist/user/web.rs +++ b/rc_core/src/persist/user/web.rs @@ -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> { + 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> { + 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::().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> { + 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> { + 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 = 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, + }) + } } diff --git a/rc_database/src/factory_wrapper.rs b/rc_database/src/factory_wrapper.rs index 213a929..4517a45 100644 --- a/rc_database/src/factory_wrapper.rs +++ b/rc_database/src/factory_wrapper.rs @@ -33,7 +33,7 @@ impl FactoryDatabase { .and_then(|(vehicle, user_opt)| user_opt.map(|user| (vehicle, user)))) } - async fn list_query(&self, query: libfj::robocraft::ListQuery) -> Result, sea_orm::DbErr> { + fn build_list_query(&self, query: &libfj::robocraft::ListQuery) -> sea_orm::Selector> { let mut query_builder = crate::schema::factory::vehicle::Entity::find() .filter(crate::schema::factory::vehicle::Column::RemovedDate.is_null()) .filter(crate::schema::factory::vehicle::Column::BanDate.is_null()) @@ -91,7 +91,7 @@ impl FactoryDatabase { query_builder = query_builder.filter(crate::schema::factory::vehicle::Column::Cpu.lte(query.maximum_cpu as u32)); } - let query_builder = query_builder + query_builder .find_also_related(crate::schema::user::Entity) .select_only() // WARNING: jank sea_orm query generation (fix) @@ -115,8 +115,11 @@ impl FactoryDatabase { .column_as(crate::schema::factory::vehicle::Column::CubeAmounts, "A_cube_amounts") .column_as(crate::schema::user::Column::PublicId, "B_public_id") .column_as(crate::schema::user::Column::DisplayName, "B_display_name") - .into_model::(); - //.into_partial_model::(); + .into_model::() + } + + async fn list_query(&self, query: libfj::robocraft::ListQuery) -> Result, sea_orm::DbErr> { + let query_builder = self.build_list_query(&query); let pagination = query_builder.paginate(self.orm.as_ref(), query.page_size as u64); Ok(pagination.fetch_page((query.page - 1) as u64).await? .into_iter() @@ -124,6 +127,11 @@ impl FactoryDatabase { .collect()) } + async fn count_list_query(&self, query: libfj::robocraft::ListQuery) -> Result { + let query_builder = self.build_list_query(&query); + query_builder.count(self.orm.as_ref()).await + } + fn thumbnail_url(&self, id: i32) -> String { format!("{}/roboshop/Live/{}", self.cdn, id) } @@ -207,6 +215,11 @@ impl VehicleFactoryAdapter for FactoryDatabase { }).collect()) } + async fn count(&self, query: libfj::robocraft::ListQuery) -> Result> { + let count = self.count_list_query(query).await?; + Ok(count as usize) + } + async fn upload(&self, vehicle: VehicleUploadInfo) -> Result> { let now = chrono::Utc::now(); let entity = crate::schema::factory::vehicle::ActiveModel { diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index 0ac472c..e6ecb80 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -1,5 +1,5 @@ use sea_orm_migration::MigratorTrait; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, sea_query::ExprTrait}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, sea_query::ExprTrait, sqlx::types::chrono}; pub struct Database { orm: std::sync::Arc, @@ -224,6 +224,32 @@ impl Database { .await } + pub async fn garage_count_by_user_id_between(&self, user_id: i32, max_cpu: u64, min_cpu: u64) -> Result { + crate::schema::garage::Entity::find() + .filter(crate::schema::garage::Column::UserId.eq(user_id)) + .filter(crate::schema::garage::Column::TotalRobotCpu.lte(max_cpu)) + .filter(crate::schema::garage::Column::TotalRobotCpu.gte(min_cpu)) + .count(self.orm.as_ref()) + .await + } + + pub async fn garage_count_by_user_id_factory(&self, user_id: i32) -> Result { + crate::schema::garage::Entity::find() + .filter(crate::schema::garage::Column::UserId.eq(user_id)) + .filter(crate::schema::garage::Column::CrfId.is_not_null()) + .count(self.orm.as_ref()) + .await + } + + pub async fn garage_storage_by_user_id(&self, _user_id: i32) -> Result, sea_orm::DbErr> { + let size = match self.orm.as_ref() { + // TODO implement support in other databases + //sea_orm::DatabaseConnection::SqlxPostgresPoolConnection(_) => None, + _ => None, + }; + Ok(size) + } + pub async fn garage_max_slot_by_user_id(&self, user_id: i32) -> Result { let result = crate::schema::garage::Entity::find() .select_only() @@ -373,6 +399,32 @@ impl Database { .await } + pub async fn count_sanctions_by_user_id(&self, user_id: i32) -> Result { + crate::schema::sanction::Entity::find() + .filter(crate::schema::sanction::Column::UserId.eq(user_id)) + //.order_by_asc(crate::schema::sanction::Column::CreationTime) + .count(self.orm.as_ref()) + .await + } + + pub async fn count_sanctions_by_user_id_and_ack(&self, user_id: i32, is_acked: bool) -> Result { + crate::schema::sanction::Entity::find() + .filter(crate::schema::sanction::Column::UserId.eq(user_id)) + .filter(crate::schema::sanction::Column::Acknowledged.eq(is_acked)) + //.order_by_asc(crate::schema::sanction::Column::CreationTime) + .count(self.orm.as_ref()) + .await + } + + pub async fn count_sanctions_by_user_id_and_descriptor(&self, user_id: i32, desc: crate::schema::sanction::Descriptor) -> Result { + crate::schema::sanction::Entity::find() + .filter(crate::schema::sanction::Column::UserId.eq(user_id)) + .filter(crate::schema::sanction::Column::Descriptor.eq(desc)) + //.order_by_asc(crate::schema::sanction::Column::CreationTime) + .count(self.orm.as_ref()) + .await + } + pub async fn sanctions_by_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { crate::schema::sanction::Entity::find() .filter(crate::schema::sanction::Column::UserId.eq(user_id)) @@ -382,6 +434,19 @@ impl Database { .await } + pub async fn sanction_by_user_id_and_descriptor_and_active(&self, user_id: i32, desc: crate::schema::sanction::Descriptor) -> Result, sea_orm::DbErr> { + let now = chrono::Utc::now().timestamp(); + let expiry_expr = sea_orm::sea_query::Expr::col(crate::schema::sanction::Column::CreationTime).add(sea_orm::sea_query::Expr::col(crate::schema::sanction::Column::Duration)); + crate::schema::sanction::Entity::find() + .filter(crate::schema::sanction::Column::UserId.eq(user_id)) + .filter(crate::schema::sanction::Column::Descriptor.eq(desc)) + .filter(crate::schema::sanction::Column::Duration.is_not_null()) + .filter(expiry_expr.clone().gte(now)) + .order_by_asc(crate::schema::sanction::Column::CreationTime) + .one(self.orm.as_ref()) + .await + } + pub async fn insert_sanction(&self, entity: crate::schema::sanction::ActiveModel) -> Result { entity.insert(self.orm.as_ref()).await } @@ -400,6 +465,13 @@ impl Database { .await } + pub async fn count_games_by_user_id(&self, user_id: i32) -> Result { + crate::schema::multiplayer_game_player::Entity::find() + .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) + .count(self.orm.as_ref()) + .await + } + pub async fn game_by_user_id_and_completion(&self, user_id: i32, is_complete: bool) -> Result, sea_orm::DbErr> { Ok(crate::schema::multiplayer_game::Entity::find() .find_also_related(crate::schema::multiplayer_game_player::Entity) @@ -610,6 +682,24 @@ impl Database { .await } + pub async fn count_friends_by_user_id(&self, user_id: i32, status_in: crate::schema::friend::FriendStatus) -> Result { + crate::schema::friend::Entity::find() + .filter(crate::schema::friend::Column::FriendSource.eq(user_id)) + .filter(crate::schema::friend::Column::State.eq(status_in)) + //.order_by_asc(crate::schema::friend::Column::CreationTime) + .count(self.orm.as_ref()) + .await + } + + pub async fn count_friends_target_by_user_id(&self, user_id: i32, status_in: crate::schema::friend::FriendStatus) -> Result { + crate::schema::friend::Entity::find() + .filter(crate::schema::friend::Column::FriendTarget.eq(user_id)) + .filter(crate::schema::friend::Column::State.eq(status_in)) + //.order_by_asc(crate::schema::friend::Column::CreationTime) + .count(self.orm.as_ref()) + .await + } + pub async fn friends_by_user_id(&self, user_id: i32, status_not_in: impl IntoIterator) -> Result, sea_orm::DbErr> { Ok(crate::schema::friend::Entity::find() .find_also_related(crate::schema::user::Entity) diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs index df9c1bb..defcd26 100644 --- a/rc_factory/src/arc/adapter.rs +++ b/rc_factory/src/arc/adapter.rs @@ -66,6 +66,79 @@ impl ArcAdapter { serde_json::to_string(&str_map).unwrap_or_else(|_| "{}".to_string()) } + + fn listquery_to_db(&self, query: &libfj::robocraft::ListQuery) -> sea_orm::Select { + let mut query_builder = super::entities::robot_metadata::Entity::find(); + match query.order { + libfj::robocraft::FactoryOrderType::Suggested => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::RentCount); }, + libfj::robocraft::FactoryOrderType::CombatRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CombatRating); }, + libfj::robocraft::FactoryOrderType::CosmeticRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CosmeticRating); }, + libfj::robocraft::FactoryOrderType::Added => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::AddedDate); }, + libfj::robocraft::FactoryOrderType::CPU => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::Cpu); }, + libfj::robocraft::FactoryOrderType::MostBought => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::BuyCount); }, + } + if !query.text_filter.is_empty() { + let query_text = format!("%{}%", query.text_filter.replace('%', "")); + if query.player_filter { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) + .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text)) + ); + } else { + match query.text_search_field { + libfj::robocraft::FactoryTextSearchField::All => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) + .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text.clone())) + .add(super::entities::robot_metadata::Column::Name.like(query_text.clone())) + .add(super::entities::robot_metadata::Column::Description.like(query_text)) + ); + }, + libfj::robocraft::FactoryTextSearchField::Name => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::Name.like(query_text.clone())) + ); + }, + libfj::robocraft::FactoryTextSearchField::Player => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) + .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text)) + ); + }, + } + } + } + + if query.buyable { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedDate.lt("2025-01-23")) + .add(super::entities::robot_metadata::Column::Buyable.eq(1)) + ); + } + if query.featured_only { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Featured.eq(1)); + } + + // movement filters not supported + // weapon filters not supported + if query.minimum_cpu > 0 { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.gte(query.minimum_cpu as u32)); + } + if query.maximum_cpu < usize::MAX { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.lte(query.maximum_cpu as u32)); + } + if !self.ignore_expiry { + let now_str = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true).trim_end_matches('Z').to_owned(); + log::debug!("Expiry must exceed `{}`", now_str); + query_builder = query_builder.filter(super::entities::robot_metadata::Column::ExpiryDate.gte(now_str)) + } + query_builder + } } #[async_trait::async_trait] @@ -115,76 +188,7 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { log::debug!("Search vehicles with query {:?}", query); - let mut query_builder = super::entities::robot_metadata::Entity::find(); - match query.order { - libfj::robocraft::FactoryOrderType::Suggested => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::RentCount); }, - libfj::robocraft::FactoryOrderType::CombatRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CombatRating); }, - libfj::robocraft::FactoryOrderType::CosmeticRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CosmeticRating); }, - libfj::robocraft::FactoryOrderType::Added => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::AddedDate); }, - libfj::robocraft::FactoryOrderType::CPU => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::Cpu); }, - libfj::robocraft::FactoryOrderType::MostBought => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::BuyCount); }, - } - if !query.text_filter.is_empty() { - let query_text = format!("%{}%", query.text_filter.replace('%', "")); - if query.player_filter { - query_builder = query_builder.filter( - sea_orm::sea_query::Condition::any() - .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) - .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text)) - ); - } else { - match query.text_search_field { - libfj::robocraft::FactoryTextSearchField::All => { - query_builder = query_builder.filter( - sea_orm::sea_query::Condition::any() - .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) - .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text.clone())) - .add(super::entities::robot_metadata::Column::Name.like(query_text.clone())) - .add(super::entities::robot_metadata::Column::Description.like(query_text)) - ); - }, - libfj::robocraft::FactoryTextSearchField::Name => { - query_builder = query_builder.filter( - sea_orm::sea_query::Condition::any() - .add(super::entities::robot_metadata::Column::Name.like(query_text.clone())) - ); - }, - libfj::robocraft::FactoryTextSearchField::Player => { - query_builder = query_builder.filter( - sea_orm::sea_query::Condition::any() - .add(super::entities::robot_metadata::Column::AddedBy.like(query_text.clone())) - .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query_text)) - ); - }, - } - } - } - - if query.buyable { - query_builder = query_builder.filter( - sea_orm::sea_query::Condition::any() - .add(super::entities::robot_metadata::Column::AddedDate.lt("2025-01-23")) - .add(super::entities::robot_metadata::Column::Buyable.eq(1)) - ); - } - if query.featured_only { - query_builder = query_builder.filter(super::entities::robot_metadata::Column::Featured.eq(1)); - } - - // movement filters not supported - // weapon filters not supported - if query.minimum_cpu > 0 { - query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.gte(query.minimum_cpu as u32)); - } - if query.maximum_cpu < usize::MAX { - query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.lte(query.maximum_cpu as u32)); - } - if !self.ignore_expiry { - let now_str = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true).trim_end_matches('Z').to_owned(); - log::debug!("Expiry must exceed `{}`", now_str); - query_builder = query_builder.filter(super::entities::robot_metadata::Column::ExpiryDate.gte(now_str)) - } - let query_params = query_builder; + let query_params = self.listquery_to_db(&query); // FIXME add support for query.prepend_featured_bot let metadata_pages = query_params.paginate(&self.orm, query.page_size as u64); @@ -230,6 +234,14 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { Ok(infos) } + async fn count(&self, query: libfj::robocraft::ListQuery) -> Result> { + log::debug!("Count vehicles with query {:?}", query); + let query_params = self.listquery_to_db(&query); + query_params.count(&self.orm).await + .map(|len| len as usize) + .map_err(|e| Box::new(e) as _) + } + async fn upload(&self, vehicle: crate::VehicleUploadInfo) -> Result> { if self.is_readonly { return Ok(crate::VehicleThumbnailInfo { diff --git a/rc_factory/src/traits.rs b/rc_factory/src/traits.rs index e4720ff..7e9a5ca 100644 --- a/rc_factory/src/traits.rs +++ b/rc_factory/src/traits.rs @@ -2,6 +2,11 @@ pub trait VehicleFactoryAdapter: Send + Sync + 'static { async fn vehicle(&self, id: i32) -> Result, Box>; async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box>; + // used by society web dashboard + async fn count(&self, query: libfj::robocraft::ListQuery) -> Result> { + Ok(self.list(query).await?.len()) + } + async fn upload(&self, vehicle: VehicleUploadInfo) -> Result>; async fn rate_vehicle(&self, id: i32, combat: i32, cosmetic: i32) -> Result<(), Box>; /// Just update any purchase trackers diff --git a/rc_society/Cargo.toml b/rc_society/Cargo.toml index a346b10..b3c0346 100644 --- a/rc_society/Cargo.toml +++ b/rc_society/Cargo.toml @@ -23,6 +23,7 @@ clap.workspace = true handlebars = { version = "6", features = ["dir_source"] } oj_rc_core = { version = "*", path = "../rc_core" } oj_rc_plugins = { version = "*", path = "../rc_plugins" } +oj_rc_factory = { version = "*", path = "../rc_factory" } oj_convert.workspace = true libfj.workspace = true git-version.workspace = true diff --git a/rc_society/src/main.rs b/rc_society/src/main.rs index 230e0f4..02dbb5d 100644 --- a/rc_society/src/main.rs +++ b/rc_society/src/main.rs @@ -26,6 +26,7 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::ConfigImpl::load(&cli_args.assets_robocraft)?; let server_settings = actix_web::web::Data::new(>::server_config(&config)); + let server_links = actix_web::web::Data::new(>::url_links(&config)); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); @@ -36,6 +37,8 @@ async fn main() -> std::io::Result<()> { let parsers_ref = actix_web::web::Data::new(parsers); let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data"); + let factory = >::factory(&config, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config"); + let factory_ref = actix_web::web::Data::new(factory); let auth_ref = actix_web::web::Data::new(Box::new(users)); let cli_args2 = actix_web::web::Data::new(cli_args.clone()); @@ -72,9 +75,11 @@ async fn main() -> std::io::Result<()> { .app_data(loadeds_args.clone()) .app_data(handlebars_ref.clone()) .app_data(server_settings.clone()) + .app_data(server_links.clone()) .app_data(auth_ref.clone()) .app_data(importers_ref.clone()) .app_data(parsers_ref.clone()) + .app_data(factory_ref.clone()) .service(version_info) .service(web::login::form_submit) .service(web::login::form_load) diff --git a/rc_society/src/web/dashboard.rs b/rc_society/src/web/dashboard.rs index eced6c8..658e8b4 100644 --- a/rc_society/src/web/dashboard.rs +++ b/rc_society/src/web/dashboard.rs @@ -6,11 +6,15 @@ const FORM_NAME: &str = "dashboard"; #[derive(Serialize, Deserialize)] struct RenderData { - // TODO display_name: String, public_id: String, debug: DebugData, perms: PermissionData, + garage: GarageData, + factory: FactoryData, + account: AccountData, + sanction: SanctionData, + social: SocialData, } #[derive(Serialize, Deserialize)] @@ -18,6 +22,8 @@ struct DebugData { user_id: i32, creation_time_unix: i64, creation_time_iso: String, + cdn: String, + factory: String, } #[derive(Serialize, Deserialize)] @@ -29,7 +35,59 @@ struct PermissionData { banned: bool, } -pub async fn dashboard_impl(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { +#[derive(Serialize, Deserialize)] +struct GarageData { + total: u64, + regular_total: u64, + mega_total: u64, + empty_total: u64, + bytes_total: Option, // not always supported + selected_slot: i32, +} + +#[derive(Serialize, Deserialize)] +struct FactoryData { + garages_from_factory: u64, + uploads_total: usize, +} + +#[derive(Serialize, Deserialize)] +struct AccountData { + currencies: std::collections::HashMap, + games: u64, + avatar_id: String, + is_custom_avatar: bool, + premium_until_unix: i64, + premium_until_iso: String, + rank: u32, +} + +#[derive(Serialize, Deserialize)] +struct SanctionData { + total: u64, + pending: u64, + warns: u64, + mutes: u64, + muted_until_unix: Option, + muted_until_iso: Option, +} + +#[derive(Serialize, Deserialize)] +struct ClanData { + name: String, + description: String, + ty: String, +} + +#[derive(Serialize, Deserialize)] +struct SocialData { + clan: Option, + friends: u64, + friends_of: u64, + chats: 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), super::LoginReturn::Success(user) => { @@ -37,6 +95,22 @@ pub async fn dashboard_impl(handlebars_ref: Data>, au log::debug!("Rendering user's dashboard"); let creation_time = user.creation(); let creation_time_chrono = chrono::DateTime::::from_timestamp_secs(creation_time).unwrap_or_default(); + let (garage_stats, factory_stats) = match build_garage_data(user.as_ref(), factory.as_ref()).await { + Ok(x) => x, + Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)), + }; + let account_stats = match build_account_data(user.as_ref()).await { + Ok(x) => x, + Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)), + }; + let sanction_stats = match build_sanction_data(user.as_ref()).await { + Ok(x) => x, + Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)), + }; + let social_stats = match build_social_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(), @@ -45,14 +119,15 @@ pub async fn dashboard_impl(handlebars_ref: Data>, au user_id: user.account_id(), creation_time_unix: creation_time, creation_time_iso: creation_time_chrono.to_rfc3339(), + cdn: server_config.cdn_url.clone(), + factory: server_config.factory_url.clone(), }, - perms: PermissionData { - r#mod: user.is_mod(), - admin: user.is_admin(), - dev: user.is_dev(), - royal: user.is_royal(), - banned: user.is_banned(), - } + perms: build_perm_data(user.as_ref()), + garage: garage_stats, + factory: factory_stats, + account: account_stats, + sanction: sanction_stats, + social: social_stats, }, handlebars_ref.as_ref(), FORM_NAME, @@ -64,12 +139,177 @@ pub async fn dashboard_impl(handlebars_ref: Data>, au } } +fn fallback_render(error: Box, user: &dyn oj_rc_core::persist::user::WebUser, handlebars_ref: &handlebars::Handlebars<'_>, req: &HttpRequest) -> actix_web::HttpResponse { + let creation_time = user.creation(); + let creation_time_chrono = chrono::DateTime::::from_timestamp_secs(creation_time).unwrap_or_default(); + super::render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + debug: DebugData { + user_id: user.account_id(), + creation_time_unix: creation_time, + creation_time_iso: creation_time_chrono.to_rfc3339(), + cdn: String::default(), + factory: String::default(), + }, + perms: PermissionData { + r#mod: false, + admin: false, + dev: false, + royal: false, + banned: false, + }, + garage: GarageData { + total: 0, + regular_total: 0, + mega_total: 0, + empty_total: 0, + bytes_total: None, + selected_slot: 0, + }, + factory: FactoryData { + garages_from_factory: 0, + uploads_total: 0, + }, + account: AccountData { + currencies: std::collections::HashMap::default(), + games: 0, + avatar_id: "???".to_owned(), + is_custom_avatar: false, + premium_until_unix: 0, + premium_until_iso: String::default(), + rank: 0, + }, + sanction: SanctionData { + total: 0, + pending: 0, + warns: 0, + mutes: 0, + muted_until_unix: None, + muted_until_iso: None, + }, + social: SocialData { + clan: None, + friends: 0, + friends_of: 0, + chats: Vec::default() + } + }, + format!("Dashboard loading failed: {}", error), + handlebars_ref, + FORM_NAME, + ) + .respond_to(req) + .map_into_boxed_body() +} + +fn build_perm_data(user: &dyn oj_rc_core::persist::user::WebUser) -> PermissionData { + PermissionData { + r#mod: user.is_mod(), + admin: user.is_admin(), + dev: user.is_dev(), + royal: user.is_royal(), + banned: user.is_banned(), + } +} + +async fn build_garage_data(user: &dyn oj_rc_core::persist::user::WebUser, factory: &dyn oj_rc_factory::VehicleFactoryAdapter) -> Result<(GarageData, FactoryData), Box> { + let stats = user.garage_stats().await?; + let uploads_count = factory.count(libfj::robocraft::ListQuery { + page: 0, + page_size: 1_000, + order: libfj::robocraft::FactoryOrderType::Added, + player_filter: true, + movement_filter: Vec::default(), + movement_category_filter: Vec::default(), + weapon_filter: Vec::default(), + weapon_category_filter: Vec::default(), + minimum_cpu: 0, + maximum_cpu: i32::MAX as _, + text_filter: user.public_id().to_owned(), + text_search_field: libfj::robocraft::FactoryTextSearchField::Player, + buyable: false, + prepend_featured_robot: false, + featured_only: false, + default_page: false, + }).await?; + Ok((GarageData { + total: stats.vehicle_total, + regular_total: stats.regular_vehicle_total, + mega_total: stats.mega_vehicle_total, + empty_total: stats.empty_vehicle_total, + bytes_total: stats.storage_bytes_total, + selected_slot: stats.selected_garage, + }, + FactoryData { + garages_from_factory: stats.factory_vehicle_total, + uploads_total: uploads_count, + })) +} + +async fn build_account_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Result> { + let stats = user.account_stats().await?; + Ok(AccountData { + currencies: stats.currencies.into_iter() + .map(|(key, val)| (currency_to_str(key).to_owned(), val)) + .collect(), + games: stats.games_played, + avatar_id: stats.avatar_id.map(|id| format!("#{}", id)).unwrap_or_else(|| "Custom".to_owned()), + is_custom_avatar: stats.avatar_id.is_none(), + premium_until_unix: stats.premium_until, + premium_until_iso: chrono::DateTime::::from_timestamp_secs(stats.premium_until) + .unwrap_or_default() + .to_rfc3339(), + rank: stats.rank, + }) +} + +fn currency_to_str(currency: oj_rc_core::persist::user::CurrencyType) -> &'static str { + match currency { + oj_rc_core::persist::user::CurrencyType::Free => "robits", + oj_rc_core::persist::user::CurrencyType::Paid => "gc", + oj_rc_core::persist::user::CurrencyType::TechPoints => "tp", + oj_rc_core::persist::user::CurrencyType::Experience => "xp" + } +} + +async fn build_sanction_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Result> { + let stats = user.sanction_stats().await?; + Ok(SanctionData { + total: stats.total, + pending: stats.pending_total, + warns: stats.warn_total, + mutes: stats.mute_total, + muted_until_unix: stats.muted_until, + muted_until_iso: stats.muted_until + .map(|t| chrono::DateTime::::from_timestamp_secs(t) + .unwrap_or_default() + .to_rfc3339() + ), + }) +} + +async fn build_social_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Result> { + let stats = user.social_stats().await?; + Ok(SocialData { + clan: stats.clan.map(|clan| ClanData { + name: clan.name, + description: clan.description, + ty: format!("{:?}", clan.ty), + }), + friends: stats.friends_total, + friends_of: stats.friends_of_total, + chats: stats.chats, + }) +} + #[get("/dashboard")] -pub async fn get(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { - dashboard_impl(handlebars_ref, auth, user_opt, req).await +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 } #[post("/dashboard")] -pub async fn post(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { - dashboard_impl(handlebars_ref, auth, user_opt, req).await +pub async fn post(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 b829bce..e95be25 100644 --- a/rc_society/src/web/index.rs +++ b/rc_society/src/web/index.rs @@ -9,6 +9,7 @@ struct RenderData { is_logged_in: bool, display_name: Option, server: ServerDetails, + links: LinkDetails, } #[derive(Serialize, Deserialize)] @@ -16,6 +17,7 @@ struct ServerDetails { domain: String, cdn: String, auth: String, + factory: String, min_version: i32, server_version: String, start_time_iso: String, @@ -32,6 +34,7 @@ fn server_details(conf: &oj_rc_core::persist::config::ServerConfig) -> ServerDet domain: conf.domain.clone(), cdn: conf.cdn_url.clone(), auth: conf.auth_url.clone(), + factory: conf.factory_url.clone(), min_version: conf.minimum_version, server_version, start_time_unix, @@ -39,9 +42,25 @@ fn server_details(conf: &oj_rc_core::persist::config::ServerConfig) -> ServerDet } } +#[derive(Serialize, Deserialize)] +struct LinkDetails { + feedback: String, + support: String, + wiki: String, +} + +fn links_details(links: &oj_rc_core::persist::config::LinksConfig) -> LinkDetails { + LinkDetails { + feedback: links.feedback_url.clone(), + support: links.support_url.clone(), + wiki: links.wiki_url.clone(), + } +} + #[get("/")] -pub async fn get(handlebars_ref: Data>, auth: Data>, server_config: Data, user_opt: Option, req: HttpRequest) -> Result { +pub async fn get(handlebars_ref: Data>, auth: Data>, server_config: Data, server_links: 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()); if let Some(user) = user_opt { match super::try_auth_user(Some(user), auth.as_ref(), &req).await? { super::LoginReturn::AuthFail(resp) => Ok(resp), @@ -51,6 +70,7 @@ pub async fn get(handlebars_ref: Data>, auth: Data>, auth: Data