From f0cca11d3c1f79f1f5e6d419b365f2f50845ffab Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 22 Feb 2026 22:39:34 -0500 Subject: [PATCH] Implement most of clans functionality; close #89 --- cdn/src/main.rs | 1 + cdn/src/robocraft/clan_avatar.rs | 11 +- rc_core/src/persist/user/intercom.rs | 16 + rc_core/src/persist/user/mod.rs | 2 +- rc_core/src/persist/user/social.rs | 856 ++++++++++++++++++ rc_core/src/persist/user/traits.rs | 101 +++ .../m20260215_000001_create_friend_table.rs | 2 +- .../m20260221_000001_create_clan_table.rs | 41 + ...0260221_000002_create_clan_member_table.rs | 54 ++ rc_database/src/migration/mod.rs | 4 + rc_database/src/schema/clan.rs | 35 + rc_database/src/schema/clan_member.rs | 59 ++ rc_database/src/schema/mod.rs | 2 + rc_database/src/wrapper.rs | 148 +++ rc_social_room/src/data/clan.rs | 73 +- rc_social_room/src/data/clan_invite.rs | 11 + .../src/events/clan_data_changed.rs | 33 + .../src/events/clan_invite_cancelled.rs | 26 + .../src/events/clan_invite_received.rs | 36 + .../src/events/clan_member_data_changed.rs | 44 + .../src/events/clan_member_joined.rs | 36 + rc_social_room/src/events/clan_member_left.rs | 31 + .../src/events/clan_member_removed.rs | 22 + rc_social_room/src/events/clan_renamed.rs | 30 + rc_social_room/src/events/mod.rs | 8 + rc_social_room/src/main.rs | 1 + rc_social_room/src/operations/clan_create.rs | 76 ++ .../src/operations/clan_experience_poll.rs | 33 + rc_social_room/src/operations/clan_info.rs | 65 +- rc_social_room/src/operations/clan_invite.rs | 47 +- .../src/operations/clan_invite_accept.rs | 80 ++ .../src/operations/clan_invite_cancel.rs | 50 + .../src/operations/clan_invite_decline.rs | 45 + .../src/operations/clan_invite_decline_all.rs | 28 + .../src/operations/clan_invite_to.rs | 64 ++ rc_social_room/src/operations/clan_join.rs | 79 ++ rc_social_room/src/operations/clan_leave.rs | 42 + .../src/operations/clan_member_remove.rs | 48 + .../src/operations/clan_member_rerank.rs | 61 ++ rc_social_room/src/operations/clan_modify.rs | 86 ++ rc_social_room/src/operations/clan_my_info.rs | 34 + rc_social_room/src/operations/mod.rs | 33 +- rc_social_room/src/operations/search_clan.rs | 77 +- 43 files changed, 2605 insertions(+), 26 deletions(-) create mode 100644 rc_database/src/migration/m20260221_000001_create_clan_table.rs create mode 100644 rc_database/src/migration/m20260221_000002_create_clan_member_table.rs create mode 100644 rc_database/src/schema/clan.rs create mode 100644 rc_database/src/schema/clan_member.rs create mode 100644 rc_social_room/src/events/clan_data_changed.rs create mode 100644 rc_social_room/src/events/clan_invite_cancelled.rs create mode 100644 rc_social_room/src/events/clan_invite_received.rs create mode 100644 rc_social_room/src/events/clan_member_data_changed.rs create mode 100644 rc_social_room/src/events/clan_member_joined.rs create mode 100644 rc_social_room/src/events/clan_member_left.rs create mode 100644 rc_social_room/src/events/clan_member_removed.rs create mode 100644 rc_social_room/src/events/clan_renamed.rs create mode 100644 rc_social_room/src/operations/clan_create.rs create mode 100644 rc_social_room/src/operations/clan_experience_poll.rs create mode 100644 rc_social_room/src/operations/clan_invite_accept.rs create mode 100644 rc_social_room/src/operations/clan_invite_cancel.rs create mode 100644 rc_social_room/src/operations/clan_invite_decline.rs create mode 100644 rc_social_room/src/operations/clan_invite_decline_all.rs create mode 100644 rc_social_room/src/operations/clan_invite_to.rs create mode 100644 rc_social_room/src/operations/clan_join.rs create mode 100644 rc_social_room/src/operations/clan_leave.rs create mode 100644 rc_social_room/src/operations/clan_member_remove.rs create mode 100644 rc_social_room/src/operations/clan_member_rerank.rs create mode 100644 rc_social_room/src/operations/clan_modify.rs create mode 100644 rc_social_room/src/operations/clan_my_info.rs diff --git a/cdn/src/main.rs b/cdn/src/main.rs index 4c8340e..58a4a1f 100644 --- a/cdn/src/main.rs +++ b/cdn/src/main.rs @@ -29,6 +29,7 @@ async fn main() -> std::io::Result<()> { .service(robocraft::user_avatar::get) .service(robocraft::user_avatar::post) .service(robocraft::clan_avatar::get) + .service(robocraft::clan_avatar::post) .service(robocraft::brawl_data::get) .service(robocraft::campaign_data::get) .service(robocraft::factory::arc::get) diff --git a/cdn/src/robocraft/clan_avatar.rs b/cdn/src/robocraft/clan_avatar.rs index 1213d53..8b444ab 100644 --- a/cdn/src/robocraft/clan_avatar.rs +++ b/cdn/src/robocraft/clan_avatar.rs @@ -1,4 +1,4 @@ -use actix_web::{get, web::{Data, Path}, Responder}; +use actix_web::{get, post, web::{Data, Path, Bytes}, Responder}; #[get("/clanavatar/Live/{name}")] pub async fn get(cli: Data, name: Path) -> impl Responder { @@ -11,3 +11,12 @@ pub async fn get(cli: Data, name: Path) -> impl Res actix_files::NamedFile::open_async(std::path::PathBuf::from(&cli.assets_robocraft).join(super::DEFAULT_IMAGE)).await } } + +#[post("/clanavatar/Live/{name}")] +pub async fn post(cli: Data, auth: Data, name: Path, body: Bytes, req: actix_web::HttpRequest) -> Result { + auth.validate(&req, &name)?; + let path = std::path::PathBuf::from(&cli.data_robocraft).join("clanavatar").join(format!("{}.jpg", *name)); + log::debug!("Saving clanavatar for {} to {}: {}B", name, path.display(), body.len()); + std::fs::write(path, &body).map_err(super::IntercomOpError::Io)?; + Ok(actix_web::HttpResponse::NoContent().finish()) +} diff --git a/rc_core/src/persist/user/intercom.rs b/rc_core/src/persist/user/intercom.rs index fd07950..cd2f6f6 100644 --- a/rc_core/src/persist/user/intercom.rs +++ b/rc_core/src/persist/user/intercom.rs @@ -33,6 +33,22 @@ impl super::account_json::UserData { .await?; Ok(()) } + + pub(super) async fn save_clan_avatar(&self, image: Vec, clan_name: &str) -> Result<(), polariton_server::operations::SimpleOpError> { + // seems to always be jpg + let token = generate_token(clan_name.as_bytes(), &self.secret); + let auth_header_val = format!("Internal {}", token); + let url = format!("{}/clanavatar/Live/{}", self.cdn, clan_name); + if let Err(e) = self.http_client.post(url) + .header("Authorization", auth_header_val) + .body(image) + .send() + .await { + log::error!("Failed to update clan avatar for {} ({}): {}", self.account.public_id, self.account.id, e); + return Err((crate::data::error_codes::SocialErrorCode::UnexpectedError as i16).into()); + } + Ok(()) + } } #[async_trait::async_trait] diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index bda478f..e7539d0 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}; +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}; pub mod intercom; pub use intercom::generate_token as generate_intercom_token; diff --git a/rc_core/src/persist/user/social.rs b/rc_core/src/persist/user/social.rs index 46f3e92..e9d1e54 100644 --- a/rc_core/src/persist/user/social.rs +++ b/rc_core/src/persist/user/social.rs @@ -28,6 +28,39 @@ impl UserData { )) } } + + async fn clan_members_of_clan(&self, clan_id: i32) -> Result, polariton_server::operations::SimpleOpError> { + let members = self.db.clan_members_by_clan_id(clan_id).await + .map_err(|e| { + log::error!("Failed to retrieve members of clan {} by user {}: {}", clan_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve members of clan: {}", e), + ) + })?; + let avatar_infos = self.db.user_auxs_by_user_ids_and_descriptor(members.iter().map(|(m, _)| m.user_id), oj_rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve clan {} member avatars by user {}: {}", clan_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan member avatars: {}", e), + ) + })?; + let avatar_infos: std::collections::HashMap> = avatar_infos.into_iter() + .map(|aux| (aux.user_id, aux.data.parse().ok())) + .collect(); + Ok(members.into_iter() + .map(|(member, user)| super::ClanMember { + public_id: user.public_id, + display_name: user.display_name, + is_confirmed: matches!(member.status, oj_rc_database::schema::clan_member::ClanMemberStatus::Confirmed), + avatar_id: avatar_infos.get(&user.id).copied().flatten(), + rank: super::ClanMemberRank::db_to_core(&member.rank), + season_xp: 0, // TODO clan seasons + }) + .collect() + ) + } } #[async_trait::async_trait] @@ -198,6 +231,829 @@ impl super::SocialUser for UserData { Ok(false) } } + + async fn my_clan_info(&self, include_members: bool) -> Result)>, polariton_server::operations::SimpleOpError> { + let clan_opt = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan: {}", e), + ) + })?; + if let Some((clan, _member)) = clan_opt { + if include_members { + let members = self.clan_members_of_clan(clan.id).await?; + Ok(Some(( + super::ClanData { + name: clan.name, + description: clan.description, + ty: super::ClanType::db_to_core(&clan.variant), + size: members.iter().filter(|x| x.is_confirmed).count() as _, + }, + members, + ))) + } else { + Ok(Some(( + super::ClanData { + name: clan.name, + description: clan.description, + ty: super::ClanType::db_to_core(&clan.variant), + size: 0, + }, + Vec::default(), + ))) + } + } else { + Ok(None) + } + } + + async fn clan_info(&self, clan_name: &str) -> Result)>, polariton_server::operations::SimpleOpError> { + let clan_opt = self.db.clan_by_name(clan_name.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve clan {} for user {}: {}", clan_name, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan {}: {}", clan_name, e), + ) + })?; + if let Some(clan) = clan_opt { + let members = self.clan_members_of_clan(clan.id).await?; + Ok(Some(( + super::ClanData { + name: clan.name, + description: clan.description, + ty: super::ClanType::db_to_core(&clan.variant), + size: members.iter().filter(|x| x.is_confirmed).count() as _, + }, + members, + ))) + } else { + Ok(None) + } + } + + async fn search_clan(&self, search: super::ClanSearchQuery)-> Result, polariton_server::operations::SimpleOpError> { + // TODO support days since active (if it's actually used by client) + let clan_results = self.db.clans_by_search( + search.search_string, + search.start_range as u64, + search.end_range as u64, + search.types.into_iter().map(super::ClanType::core_to_db) + ).await + .map_err(|e| { + log::error!("Failed to retrieve clans for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clans: {}", e), + ) + })?; + // FIXME retrieve all member counts in one database call + let mut member_counts = std::collections::HashMap::with_capacity(clan_results.len()); + for clan in clan_results.iter() { + let member_count = self.db.count_clan_members_by_clan_id(clan.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan {} member count for user {}: {}", clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan {} member count: {}", clan.id, e), + ) + })?; + member_counts.insert(clan.id, member_count); + } + Ok(clan_results.into_iter() + .map(|clan| super::ClanData { + name: clan.name, + description: clan.description, + ty: super::ClanType::db_to_core(&clan.variant), + size: member_counts.get(&clan.id).map(|x| *x as i32).unwrap_or_default(), + }) + .collect() + ) + } + + async fn create_clan(&self, clan: super::ClanData, avatar: Vec)-> Result, polariton_server::operations::SimpleOpError> { + // TODO make this a transaction + let existing_clan = self.db.clan_by_name(clan.name.clone()).await + .map_err(|e| { + log::error!("Failed to do exist check on clan {} for user {}: {}", clan.name, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to do exist check on clan {}: {}", clan.name, e), + ) + })?; + if existing_clan.is_some() { + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanAlreadyExists as i16, + format!("Clan with name \"{}\" is already in database", clan.name), + )); + } + let now = chrono::Utc::now().timestamp(); + let new_clan = oj_rc_database::schema::clan::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + name: oj_rc_database::sea_orm::ActiveValue::Set(clan.name.clone()), + description: oj_rc_database::sea_orm::ActiveValue::Set(clan.description), + variant: oj_rc_database::sea_orm::ActiveValue::Set(clan.ty.core_to_db()), + }; + let new_clan = self.db.insert_clan(new_clan).await + .map_err(|e| { + log::error!("Failed to save new clan for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to save new clan: {}", e), + ) + })?; + let first_member = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id), + clan_id: oj_rc_database::sea_orm::ActiveValue::Set(new_clan.id), + rank: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberRank::Leader), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Confirmed), + }; + self.db.insert_clan_member(first_member).await + .map_err(|e| { + log::error!("Failed to save first clan {} member of user {}: {}", new_clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to save first clan {} member: {}", new_clan.id, e), + ) + })?; + let avatar_info: Option = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve avatar info of user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve avatar info: {}", e), + ) + })? + .map(|x| x.data.parse().ok()).flatten(); + + if !avatar.is_empty() { + self.save_clan_avatar(avatar, &clan.name).await?; + } + + Ok(vec![ + super::ClanMember { + public_id: self.account.public_id.clone(), + display_name: self.account.display_name.clone(), + is_confirmed: true, + avatar_id: avatar_info, + rank: super::ClanMemberRank::Leader, + season_xp: 0, + } + ]) + } + + async fn join_clan(&self, clan_name: &str) -> Result<(super::ClanData, Vec), polariton_server::operations::SimpleOpError> { + let current_clan = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan: {}", e), + ) + })?; + if current_clan.is_some() { + log::debug!("User {} cannot join a clan; they are already in one", self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::AlreadyInClan as i16, + format!("User {} is already in clan", self.account.id), + )); + } + let existing_clan = self.db.clan_by_name(clan_name.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve existing clan {} for user {}: {}", clan_name, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve existing clan {}: {}", clan_name, e), + ) + })?; + if let Some(clan) = existing_clan { + // pre-join checks + let invited_clan_opt = self.db.clan_invited_to_for_user_id_and_clan_id(self.account.id, clan.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan invites of user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to save new clan invites: {}", e), + ) + })?; + match clan.variant { + oj_rc_database::schema::clan::ClanType::Public => { + // nothing + }, + oj_rc_database::schema::clan::ClanType::Private => { + let is_invited = invited_clan_opt.is_some(); + if !is_invited { + log::debug!("User {} tried to join clan {} without being invited", self.account.id, clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanClosed as i16, + format!("Clan \"{}\" is private and user {} is not invited", clan.name, self.account.id), + )); + } + }, + oj_rc_database::schema::clan::ClanType::Abandoned => { + log::debug!("User {} tried to join abandoned clan {}", self.account.id, clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanClosed as i16, + format!("Clan \"{}\" is abandoned", clan.name), + )); + }, + oj_rc_database::schema::clan::ClanType::Banned => { + log::debug!("User {} tried to join banned clan {}", self.account.id, clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanClosed as i16, + format!("Clan \"{}\" is banned", clan.name), + )); + }, + } + if let Some((_invited_clan, invited_member)) = invited_clan_opt { + self.db.update_clan_member(oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(invited_member.id), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Confirmed), + ..Default::default() + }).await + .map_err(|e| { + log::error!("Failed to update invited clan {} member of user {}: {}", clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to update invited clan {} member: {}", clan.id, e), + ) + })?; + } else { + let now = chrono::Utc::now().timestamp(); + let new_member = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id), + clan_id: oj_rc_database::sea_orm::ActiveValue::Set(clan.id), + rank: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberRank::Member), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Confirmed), + }; + self.db.insert_clan_member(new_member).await + .map_err(|e| { + log::error!("Failed to save new clan {} member of user {}: {}", clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to save new clan {} member: {}", clan.id, e), + ) + })?; + } + self.db.update_clan_member_decline_all_invites(self.account.id).await + .map_err(|e| { + log::error!("Failed to decline clan invites for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to decline clan invites: {}", e), + ) + })?; + let members = self.clan_members_of_clan(clan.id).await?; + Ok(( + super::ClanData { + name: clan.name, + description: clan.description, + ty: super::ClanType::db_to_core(&clan.variant), + size: members.iter().filter(|x| x.is_confirmed).count() as _, + }, + members, + )) + } else { + log::warn!("Failed to find clan with name \"{}\" in database", clan_name); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanNotFound as i16, + format!("Clan with name \"{}\" is not in database", clan_name), + )); + } + } + + + async fn leave_clan(&self) -> Result { + let joined_clan_opt = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan of user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan of user: {}", e), + ) + })?; + if let Some((joined_clan, member)) = joined_clan_opt { + let is_leader = matches!(member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Leader); + if is_leader { + log::warn!("User {} tried to leave clan {} that they lead", self.account.id, joined_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::NotClanLeader as i16, + format!("Failed to leave clan: you are the leader"), + )); + } + let new_model = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(member.id), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Deactivated), + ..Default::default() + }; + self.db.update_clan_member(new_model).await + .map_err(|e| { + log::error!("Failed to update clan {} member for user {}: {}", joined_clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve update clan member for user: {}", e), + ) + })?; + Ok(true) + } else { + Ok(false) + } + } + + async fn remove_user_from_clan(&self, public_id: &str) -> Result { + let joined_clan_opt = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan of user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan of user: {}", e), + ) + })?; + if let Some((joined_clan, member)) = joined_clan_opt { + let is_leader = matches!(member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Leader); + let is_officer = matches!(member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Officer); + if !(is_leader || is_officer) { + log::warn!("User {} tried to remove user {} from clan {} without permission", self.account.id, public_id, joined_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanRankTooLow as i16, + format!("Failed to remove user from clan: no permission"), + )); + } + let members = self.db.clan_members_by_clan_id(joined_clan.id).await + .map_err(|e| { + log::error!("Failed to retrieve joined clan {} members for user {}: {}", joined_clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve joined clan {} members for user: {}", joined_clan.id, e), + ) + })?; + let target_member_opt = members.iter() + .find(|(_mem, user)| user.public_id == public_id); + if let Some((target_member, target_user)) = target_member_opt { + let can_kick = match target_member.rank { + oj_rc_database::schema::clan_member::ClanMemberRank::Member => true, // already guaranteed to be leader OR officer + oj_rc_database::schema::clan_member::ClanMemberRank::Officer => is_leader, + oj_rc_database::schema::clan_member::ClanMemberRank::Leader => false, + }; + if !can_kick { + log::warn!("User {} tried to remove user {} from clan {} with insufficient permission", self.account.id, target_user.id, joined_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanRankTooLow as i16, + format!("Failed to remove user from clan: insufficient permission"), + )); + } + let new_model = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(target_member.id), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Deactivated), + ..Default::default() + }; + self.db.update_clan_member(new_model).await + .map_err(|e| { + log::error!("Failed to update clan {} member for user {}: {}", joined_clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve update clan member for user: {}", e), + ) + })?; + Ok(true) + } else { + log::warn!("User {} tried to remove non-existent user {} from clan {}", self.account.id, public_id, joined_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotFoundInClan as i16, + format!("Failed to remove user from clan: user not in clan"), + )); + } + } else { + log::debug!("User {} (not in a clan) tried to remove user {} from a clan", self.account.id, public_id); + Ok(false) + } + } + + async fn update_clan(&self, name: Option, description: Option, ty: Option, avatar: Option>) -> Result, polariton_server::operations::SimpleOpError> { + let joined_clan_opt = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve clan of user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve clan of user: {}", e), + ) + })?; + if let Some((joined_clan, member)) = joined_clan_opt { + let is_leader = matches!(member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Leader); + if !is_leader { + log::debug!("User {} is not leader but tried to modify clan {}", self.account.id, joined_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::NotClanLeader as i16, + format!("Failed to modify clan: user is not the leader"), + )); + } + let new_clan = oj_rc_database::schema::clan::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(joined_clan.id), + creation_time: oj_rc_database::sea_orm::ActiveValue::NotSet, + name: if let Some(name) = name { oj_rc_database::sea_orm::ActiveValue::Set(name) } else { oj_rc_database::sea_orm::ActiveValue::NotSet }, + description: if let Some(description) = description { oj_rc_database::sea_orm::ActiveValue::Set(description) } else { oj_rc_database::sea_orm::ActiveValue::NotSet }, + variant: if let Some(ty) = ty { oj_rc_database::sea_orm::ActiveValue::Set(ty.core_to_db()) } else { oj_rc_database::sea_orm::ActiveValue::NotSet }, + }; + self.db.update_clan(new_clan).await + .map_err(|e| { + log::error!("Failed to update clan {} for user {}: {}", joined_clan.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to update clan for user: {}", e), + ) + })?; + if let Some(new_avatar) = avatar { + self.save_clan_avatar(new_avatar, &joined_clan.name).await?; + } + self.clan_members_of_clan(joined_clan.id).await + } else { + log::debug!("User {} is not in a clan but tried to modify clan", self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + format!("Failed to modify clan for user not in clan"), + )); + } + } + + async fn invite_to_clan(&self, public_id: &str) -> Result { + // TODO make this a transaction + let my_clan_opt = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} to invite to clan for user {}: {}", public_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user to invite to clan for user: {}", e), + ) + })?; + if let Some((my_clan, my_member)) = my_clan_opt { + if matches!(my_member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Member) { + log::debug!("User {} cannot invite to clan {} (rank is member)", self.account.id, my_clan.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanRankTooLow as i16, + "User cannot invite to clan (rank is member)".to_owned(), + )); + } + let target_user = self.db.user_by_public_id(public_id.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} to invite to clan for user {}: {}", public_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user to invite to clan for user: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} to invite to clan {} by user {}", public_id, my_clan.id, self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, + "Failed to find user to invite to clan by user".to_owned(), + ) + })?; + let invite_opt = self.db.clan_invited_to_for_user_id_and_clan_id(target_user.id, my_clan.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan invite by user {}: {}", target_user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invite by user: {}", e), + ) + })?; + if invite_opt.is_some() { + log::debug!("User {} is already invited to clan {} by user {}", public_id, my_clan.id, self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::AlreadyInvited as i16, + "User is already invited to clan".to_owned(), + )); + } + let target_user_clan_opt = self.db.clan_by_user_id(target_user.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan by user {}: {}", target_user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan by user: {}", e), + ) + })?; + if let Some((target_user_clan, _target_user_member)) = target_user_clan_opt { + log::debug!("User {} is already in clan {}; cannot do invite by user {}", target_user.id, target_user_clan.id, self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::AlreadyInClan as i16, + "User is already in clan".to_owned(), + )); + } + let now = chrono::Utc::now().timestamp(); + let new_invite = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), + user_id: oj_rc_database::sea_orm::ActiveValue::Set(target_user.id), + clan_id: oj_rc_database::sea_orm::ActiveValue::Set(my_clan.id), + rank: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberRank::Member), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Invited), + }; + self.db.insert_clan_member(new_invite).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan by user {}: {}", target_user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan by user: {}", e), + ) + })?; + let avatar_id = self.db.user_aux_by_user_id_and_descriptor(target_user.id, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve user {} avatar info by user {}: {}", target_user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user avatar info by user: {}", e), + ) + })? + .and_then(|x| x.data.parse().ok()); + Ok(super::ClanMember { + public_id: target_user.public_id, + display_name: target_user.display_name, + is_confirmed: false, + avatar_id: avatar_id, + rank: super::ClanMemberRank::Member, + season_xp: 0, + }) + } else { + log::debug!("User {} tried to invite user {} to their non-existent clan", self.account.id, public_id); + Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + format!("Failed to invite user to clan for user: invitee is not in clan"), + )) + } + } + + async fn my_clan_invites(&self) -> Result, polariton_server::operations::SimpleOpError> { + let invites = self.db.clans_invited_to_for_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan invites: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invites: {}", e), + ) + })?; + let clan_ids = invites.iter().map(|(clan, _invite)| clan.id); + let clan_leaders = self.db.clan_leaders_by_clan_ids(clan_ids).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan invite leaders: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invite leaders: {}", e), + ) + })?; + let clan_leaders: std::collections::HashMap:: = clan_leaders.into_iter() + .map(|(member, user)| (member.clan_id, user)) + .collect(); + let clan_leader_ids = clan_leaders.values().map(|user| user.id); + let clan_leaders_avatars = self.db.user_auxs_by_user_ids_and_descriptor( + clan_leader_ids, + oj_rc_database::schema::user_aux::Descriptor::AvatarId + ).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan invite leaders' avatars: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invite leaders' avatar: {}", e), + ) + })?; + let clan_leaders_avatars: std::collections::HashMap::> = clan_leaders_avatars.into_iter() + .map(|avatar| (avatar.user_id, avatar.data.parse().ok())) + .collect(); + let mut clan_invites = Vec::with_capacity(invites.len()); + for (invite_clan, _invite_member) in invites { + if let Some(invitee) = clan_leaders.get(&invite_clan.id) { + clan_invites.push(super::ClanInviteData { + public_id: invitee.public_id.clone(), + display_name: invitee.display_name.clone(), + avatar_id: clan_leaders_avatars.get(&invitee.id).copied().flatten(), + clan_name: invite_clan.name, + clan_description: invite_clan.description, + size: 0, // TODO + }); + } + } + Ok(clan_invites) + } + + async fn decline_clan_invite(&self, clan_name: &str) -> Result, polariton_server::operations::SimpleOpError> { + let clan_invite_opt = self.db.clan_invited_to_for_user_id_and_clan_name(self.account.id, clan_name.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan {} invite: {}", self.account.id, clan_name, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invite: {}", e), + ) + })?; + if let Some((clan, invite)) = clan_invite_opt { + let to_update = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(invite.id), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Deactivated), + ..Default::default() + }; + self.db.update_clan_member(to_update).await + .map_err(|e| { + log::error!("Failed to update user {} clan {} invite {} to declined: {}", self.account.id, clan.id, invite.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to update user clan invite to declined: {}", e), + ) + })?; + self.clan_members_of_clan(clan.id).await + } else { + log::debug!("User {} cannot decline non-existent invite to clan {}", self.account.id, clan_name); + Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::NoInvite as i16, + format!("User cannot decline non-existent clan invite"), + )) + } + } + + async fn decline_all_clan_invites(&self) -> Result { + self.db.update_clan_member_decline_all_invites(self.account.id).await + .map_err(|e| { + log::error!("Failed to update all clan invites to decline for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to update all clan invites to decline for user: {}", e), + ) + })?; + Ok(true) + } + + async fn cancel_invite_to_clan(&self, public_id: &str) -> Result<(super::ClanData, Vec), polariton_server::operations::SimpleOpError> { + let user_opt = self.db.user_by_public_id(public_id.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} to cancel invite by user {}: {}", public_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user to cancel invite by user: {}", e), + ) + })?; + if let Some(user) = user_opt { + let (my_clan, _my_member) = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan to cancel invite: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan to cancel invite by user: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} clan to cancel invite", self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + "Cannot find user clan to cancel invite by user".to_owned(), + ) + })?; + let (_clan, invite) = self.db.clan_invited_to_for_user_id_and_clan_id(user.id, my_clan.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan invite to cancel invite for user {}: {}", user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan invite to cancel invite by user: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} clan invite to cancel invite for user {}", user.id, self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::NoInvite as i16, + "Cannot find user clan to cancel invite by user".to_owned(), + ) + })?; + let new_invite = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(invite.id), + status: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberStatus::Deactivated), + ..Default::default() + }; + self.db.update_clan_member(new_invite).await + .map_err(|e| { + log::error!("Failed to update user {} clan invite to cancel invite for user {}: {}", user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to udpate user clan invite to cancel invite by user: {}", e), + ) + })?; + let members = self.clan_members_of_clan(my_clan.id).await?; + Ok(( + super::ClanData { + name: my_clan.name, + description: my_clan.description, + ty: super::ClanType::db_to_core(&my_clan.variant), + size: members.len() as _, + }, + members, + )) + } else { + log::debug!("Cannot find user {} to cancel invite by user {}", public_id, self.account.id); + Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, + "Cannot find user to cancel invite by user".to_owned(), + )) + } + } + + async fn update_clan_member(&self, public_id: &str, rank: super::ClanMemberRank) -> Result, polariton_server::operations::SimpleOpError> { + let user = self.db.user_by_public_id(public_id.to_owned()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} to update clan member for user {}: {}", public_id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user to update clan member for user: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} to update clan member for user {}", public_id, self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, + "Cannot find user to update clan member for user".to_owned(), + ) + })?; + let (my_clan, my_member) = self.db.clan_by_user_id(self.account.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan to update clan member: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan to clan member: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} clan to update clan member", self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + "Cannot find user clan to udpate clan member".to_owned(), + ) + })?; + let is_leader = matches!(my_member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Leader); + let is_officer = matches!(my_member.rank, oj_rc_database::schema::clan_member::ClanMemberRank::Officer); + let can_update = match rank { + super::ClanMemberRank::Member => is_leader, + super::ClanMemberRank::Officer => is_officer || is_leader, + super::ClanMemberRank::Leader => is_leader, + }; + if !can_update { + log::debug!("User {} does not have enough permissions to update clan member rank of user {}", self.account.id, user.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::ClanRankTooLow as i16, + "User does not have enough permissions to update clan member rank of user".to_owned(), + )); + } + let (target_clan, target_member) = self.db.clan_by_user_id(user.id).await + .map_err(|e| { + log::error!("Failed to retrieve user {} clan to update clan member for user {}: {}", user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user clan to clan member: {}", e), + ) + })? + .ok_or_else(|| { + log::debug!("Cannot find user {} clan to update clan member for user {}", user.id, self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + "Cannot find user clan to udpate clan member".to_owned(), + ) + })?; + if target_clan.id != my_clan.id { + log::debug!("User {} is not in same clan as user {} to update clan member rank", self.account.id, user.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserNotInClan as i16, + "User is not in same clan as user to update clan member rank".to_owned(), + )); + } + let to_update = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(target_member.id), + rank: oj_rc_database::sea_orm::ActiveValue::Set(rank.core_to_db()), + ..Default::default() + }; + self.db.update_clan_member(to_update).await + .map_err(|e| { + log::error!("Failed to update user {} clan member rank by user {}: {}", user.id, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to udpate user clan member rank by user: {}", e), + ) + })?; + if matches!(rank, super::ClanMemberRank::Leader) { + let to_update = oj_rc_database::schema::clan_member::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::Set(my_member.id), + rank: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::clan_member::ClanMemberRank::Officer), + ..Default::default() + }; + self.db.update_clan_member(to_update).await + .map_err(|e| { + log::error!("Failed to update own clan member rank by user {} to demote to officer: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to udpate own clan member rank by user to demote to officer: {}", e), + ) + })?; + } + self.clan_members_of_clan(my_clan.id).await + } } #[async_trait::async_trait] diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 0a6f666..9da58fa 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -462,6 +462,20 @@ pub trait SocialUser: Send + Sync { async fn has_unclaimed_match_rewards(&self) -> Result; async fn get_unclaimed_match_rewards(&self) -> Result; async fn claim_match_rewards(&self) -> Result; + async fn my_clan_info(&self, include_members: bool) -> Result)>, polariton_server::operations::SimpleOpError>; + async fn clan_info(&self, clan_name: &str) -> Result)>, polariton_server::operations::SimpleOpError>; + async fn search_clan(&self, search: ClanSearchQuery)-> Result, polariton_server::operations::SimpleOpError>; + async fn create_clan(&self, clan: ClanData, avatar: Vec)-> Result, polariton_server::operations::SimpleOpError>; + async fn join_clan(&self, clan_name: &str) -> Result<(ClanData, Vec), polariton_server::operations::SimpleOpError>; + async fn leave_clan(&self) -> Result; + async fn remove_user_from_clan(&self, public_id: &str) -> Result; + async fn update_clan(&self, name: Option, description: Option, ty: Option, avatar: Option>) -> Result, polariton_server::operations::SimpleOpError>; + async fn invite_to_clan(&self, public_id: &str) -> Result; + async fn my_clan_invites(&self) -> Result, polariton_server::operations::SimpleOpError>; + async fn decline_clan_invite(&self, clan_name: &str) -> Result, polariton_server::operations::SimpleOpError>; + async fn decline_all_clan_invites(&self) -> Result; + async fn cancel_invite_to_clan(&self, public_id: &str) -> Result<(ClanData, Vec), polariton_server::operations::SimpleOpError>; + async fn update_clan_member(&self, public_id: &str, rank: ClanMemberRank) -> Result, polariton_server::operations::SimpleOpError>; } #[async_trait::async_trait] @@ -506,6 +520,7 @@ pub struct SocialInfo { pub avatar_id: Option, } +#[derive(Clone, Copy)] pub enum FriendInviteStatus { InviteSent, InvitePending, @@ -529,6 +544,92 @@ impl FriendInviteStatus { } } +pub struct ClanData { + pub name: String, + pub description: String, + pub ty: ClanType, + pub size: i32, +} + +pub struct ClanInviteData { + pub public_id: String, + pub display_name: String, + pub avatar_id: Option, + pub clan_name: String, + pub clan_description: String, + pub size: i32, +} + +#[derive(Clone, Copy, Debug)] +pub enum ClanType { + Open, + Closed, +} + +impl ClanType { + #[inline] + pub(super) fn db_to_core(status: &oj_rc_database::schema::clan::ClanType) -> Self { + match status { + oj_rc_database::schema::clan::ClanType::Public => Self::Open, + oj_rc_database::schema::clan::ClanType::Private => Self::Closed, + oj_rc_database::schema::clan::ClanType::Banned => Self::Closed, + oj_rc_database::schema::clan::ClanType::Abandoned => Self::Closed, + } + } + + #[inline] + pub(super) fn core_to_db(self) -> oj_rc_database::schema::clan::ClanType { + match self { + Self::Open => oj_rc_database::schema::clan::ClanType::Public, + Self::Closed => oj_rc_database::schema::clan::ClanType::Private, + } + } +} + +pub struct ClanMember { + pub public_id: String, + pub display_name: String, + pub is_confirmed: bool, + pub avatar_id: Option, + pub rank: ClanMemberRank, + pub season_xp: i32, +} + +#[derive(Clone, Copy)] +pub enum ClanMemberRank { + Member, + Officer, + Leader, +} + +impl ClanMemberRank { + #[inline] + pub(super) fn db_to_core(status: &oj_rc_database::schema::clan_member::ClanMemberRank) -> Self { + match status { + oj_rc_database::schema::clan_member::ClanMemberRank::Member => Self::Member, + oj_rc_database::schema::clan_member::ClanMemberRank::Officer => Self::Officer, + oj_rc_database::schema::clan_member::ClanMemberRank::Leader => Self::Leader, + } + } + #[inline] + pub(super) fn core_to_db(self) -> oj_rc_database::schema::clan_member::ClanMemberRank { + match self { + Self::Member => oj_rc_database::schema::clan_member::ClanMemberRank::Member, + Self::Officer => oj_rc_database::schema::clan_member::ClanMemberRank::Officer, + Self::Leader => oj_rc_database::schema::clan_member::ClanMemberRank::Leader, + } + } +} + +#[derive(Debug)] +pub struct ClanSearchQuery { + pub search_string: String, + pub days_since_active: i32, + pub start_range: i32, + pub end_range: i32, + pub types: Vec, +} + #[async_trait::async_trait] pub trait SingleplayerUser: Send + Sync { // regular singleplayer and campaign mode diff --git a/rc_database/src/migration/m20260215_000001_create_friend_table.rs b/rc_database/src/migration/m20260215_000001_create_friend_table.rs index 9cec698..672be26 100644 --- a/rc_database/src/migration/m20260215_000001_create_friend_table.rs +++ b/rc_database/src/migration/m20260215_000001_create_friend_table.rs @@ -23,7 +23,7 @@ impl MigrationTrait for Migration { .auto_increment() .primary_key(), ) - .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::friend::Column::CreationTime).big_integer().not_null()) .col(ColumnDef::new(crate::schema::friend::Column::FriendSource).integer().not_null()) .foreign_key( ForeignKey::create() diff --git a/rc_database/src/migration/m20260221_000001_create_clan_table.rs b/rc_database/src/migration/m20260221_000001_create_clan_table.rs new file mode 100644 index 0000000..1ffef49 --- /dev/null +++ b/rc_database/src/migration/m20260221_000001_create_clan_table.rs @@ -0,0 +1,41 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260221_000001_create_clan_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Clans table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::clan::Entity) + .col( + ColumnDef::new(crate::schema::clan::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::clan::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::clan::Column::Name).string().not_null()) + .col(ColumnDef::new(crate::schema::clan::Column::Description).string().not_null()) + .col(ColumnDef::new(crate::schema::clan::Column::Variant).string().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Clans table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::clan::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/m20260221_000002_create_clan_member_table.rs b/rc_database/src/migration/m20260221_000002_create_clan_member_table.rs new file mode 100644 index 0000000..82395b0 --- /dev/null +++ b/rc_database/src/migration/m20260221_000002_create_clan_member_table.rs @@ -0,0 +1,54 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260221_000002_create_clan_member_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Clan Members table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::clan_member::Entity) + .col( + ColumnDef::new(crate::schema::clan_member::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::clan_member::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::clan_member::Column::UserId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-clan_members-user_id") + .from(crate::schema::clan_member::Entity, crate::schema::clan_member::Column::UserId) + .to(crate::schema::user::Entity, crate::schema::user::Column::Id), + ) + .col(ColumnDef::new(crate::schema::clan_member::Column::ClanId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-clan_members-clan_id") + .from(crate::schema::clan_member::Entity, crate::schema::clan_member::Column::ClanId) + .to(crate::schema::clan::Entity, crate::schema::clan::Column::Id), + ) + .col(ColumnDef::new(crate::schema::clan_member::Column::Rank).string().not_null()) + .col(ColumnDef::new(crate::schema::clan_member::Column::Status).string().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Clan Members table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::clan_member::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index 5016f3e..b5feb71 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -16,6 +16,8 @@ mod m20251228_000001_create_score_table; #[cfg(feature = "factory")] mod m20260126_000001_create_factory_vehicle_table; mod m20260215_000001_create_friend_table; +mod m20260221_000001_create_clan_table; +mod m20260221_000002_create_clan_member_table; pub struct Migrator; @@ -39,6 +41,8 @@ impl MigratorTrait for Migrator { #[cfg(feature = "factory")] Box::new(m20260126_000001_create_factory_vehicle_table::Migration), Box::new(m20260215_000001_create_friend_table::Migration), + Box::new(m20260221_000001_create_clan_table::Migration), + Box::new(m20260221_000002_create_clan_member_table::Migration), ] } } diff --git a/rc_database/src/schema/clan.rs b/rc_database/src/schema/clan.rs new file mode 100644 index 0000000..f4da80a --- /dev/null +++ b/rc_database/src/schema/clan.rs @@ -0,0 +1,35 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "clans")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub creation_time: i64, // seconds since unix epoch + pub name: String, + pub description: String, + pub variant: ClanType, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::clan_member::Entity")] + ClanMember, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ClanMember.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum ClanType { + Public, + Private, + Banned, + Abandoned, +} diff --git a/rc_database/src/schema/clan_member.rs b/rc_database/src/schema/clan_member.rs new file mode 100644 index 0000000..8b6b875 --- /dev/null +++ b/rc_database/src/schema/clan_member.rs @@ -0,0 +1,59 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "clan_members")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub creation_time: i64, // seconds since unix epoch + pub user_id: i32, + pub clan_id: i32, + pub rank: ClanMemberRank, + pub status: ClanMemberStatus, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, + #[sea_orm( + belongs_to = "super::clan::Entity", + from = "Column::ClanId", + to = "super::clan::Column::Id" + )] + Clan, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Clan.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum ClanMemberRank { + Member, + Officer, + Leader, +} + +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase")] +pub enum ClanMemberStatus { + Invited, + Confirmed, + Deactivated, +} diff --git a/rc_database/src/schema/mod.rs b/rc_database/src/schema/mod.rs index 643a1b0..30eaffc 100644 --- a/rc_database/src/schema/mod.rs +++ b/rc_database/src/schema/mod.rs @@ -13,6 +13,8 @@ pub mod multiplayer_game_score; #[cfg(feature = "factory")] pub mod factory; pub mod friend; +pub mod clan; +pub mod clan_member; pub fn parse_int_csv(s: &str) -> Vec { s.split(',').filter_map(|i_as_s| { diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index d7030ed..a80dce5 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -628,6 +628,154 @@ impl Database { Ok(()) } + pub async fn clan_by_name(&self, name: String) -> Result, sea_orm::DbErr> { + crate::schema::clan::Entity::find() + .filter( + sea_orm::sea_query::Func::lower(crate::schema::clan::Column::Name.into_expr()) + .eq(name.to_lowercase()) + ) + .one(self.orm.as_ref()).await + } + + pub async fn clan_by_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan::Entity::find() + .find_also_related(crate::schema::clan_member::Entity) + .filter(crate::schema::clan_member::Column::UserId.eq(user_id)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Confirmed)) + .one(self.orm.as_ref()) + .await? + .and_then(|(clan, member)| member.map(|member| (clan, member)))) + } + + pub async fn clans_invited_to_for_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan::Entity::find() + .find_also_related(crate::schema::clan_member::Entity) + .filter(crate::schema::clan_member::Column::UserId.eq(user_id)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Invited)) + .all(self.orm.as_ref()) + .await? + .into_iter() + .filter_map(|(clan, member)| member.map(|member| (clan, member))) + .collect()) + } + + pub async fn clan_invited_to_for_user_id_and_clan_id(&self, user_id: i32, clan_id: i32) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan::Entity::find() + .find_also_related(crate::schema::clan_member::Entity) + .filter(crate::schema::clan_member::Column::UserId.eq(user_id)) + .filter(crate::schema::clan_member::Column::ClanId.eq(clan_id)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Invited)) + .one(self.orm.as_ref()) + .await? + .and_then(|(clan, member)| member.map(|member| (clan, member)))) + } + + pub async fn clan_invited_to_for_user_id_and_clan_name(&self, user_id: i32, clan_name: String) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan::Entity::find() + .find_also_related(crate::schema::clan_member::Entity) + .filter(crate::schema::clan_member::Column::UserId.eq(user_id)) + .filter( + sea_orm::sea_query::Func::lower(crate::schema::clan::Column::Name.into_expr()) + .eq(clan_name.to_lowercase()) + ) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Invited)) + .one(self.orm.as_ref()) + .await? + .and_then(|(clan, member)| member.map(|member| (clan, member)))) + } + + pub async fn clans_by_search(&self, s: String, start: u64, _end: u64, types: impl std::iter::Iterator) -> Result, sea_orm::DbErr> { + let lower_s_like = format!("%{}%", s.trim_matches('%').to_lowercase()); + let types: Vec<_> = types.collect(); + crate::schema::clan::Entity::find() + .filter( + sea_orm::sea_query::Expr::expr( + sea_orm::sea_query::Func::lower(crate::schema::clan::Column::Name.into_expr()) + ).like(&lower_s_like) + ) + .filter(if types.is_empty() { + crate::schema::clan::Column::Variant.is_in([ + crate::schema::clan::ClanType::Public, + crate::schema::clan::ClanType::Private, + ]) + } else { + crate::schema::clan::Column::Variant.is_in(types) + }) + // FIXME don't return clans that are not strictly in the start..end range + .paginate(self.orm.as_ref(), 50) + .fetch_page(start / 50) + .await + } + + pub async fn insert_clan(&self, entity: crate::schema::clan::ActiveModel) -> Result { + #[cfg(debug_assertions)] + assert!(matches!(entity.id, sea_orm::ActiveValue::NotSet)); + entity.insert(self.orm.as_ref()).await + } + + pub async fn update_clan(&self, entity: crate::schema::clan::ActiveModel) -> Result { + entity.update(self.orm.as_ref()).await + } + + pub async fn clan_members_by_clan_id(&self, clan_id: i32) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan_member::Entity::find() + .find_also_related(crate::schema::user::Entity) + .filter(crate::schema::clan_member::Column::ClanId.eq(clan_id)) + .filter(crate::schema::clan_member::Column::Status.is_in([ + crate::schema::clan_member::ClanMemberStatus::Invited, + crate::schema::clan_member::ClanMemberStatus::Confirmed, + ])) + .all(self.orm.as_ref()) + .await? + .into_iter() + .filter_map(|(member, user)| user.map(|user| (member, user))) + .collect() + ) + } + + pub async fn clan_leaders_by_clan_ids(&self, clan_ids: impl std::iter::Iterator) -> Result, sea_orm::DbErr> { + Ok(crate::schema::clan_member::Entity::find() + .find_also_related(crate::schema::user::Entity) + .filter(crate::schema::clan_member::Column::ClanId.is_in(clan_ids)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Confirmed)) + .filter(crate::schema::clan_member::Column::Rank.eq(crate::schema::clan_member::ClanMemberRank::Leader)) + .all(self.orm.as_ref()) + .await? + .into_iter() + .filter_map(|(member, user)| user.map(|user| (member, user))) + .collect() + ) + } + + pub async fn insert_clan_member(&self, entity: crate::schema::clan_member::ActiveModel) -> Result { + #[cfg(debug_assertions)] + assert!(matches!(entity.id, sea_orm::ActiveValue::NotSet)); + entity.insert(self.orm.as_ref()).await + } + + pub async fn update_clan_member(&self, entity: crate::schema::clan_member::ActiveModel) -> Result { + entity.update(self.orm.as_ref()).await + } + + pub async fn update_clan_member_decline_all_invites(&self, user_id: i32) -> Result<(), sea_orm::DbErr> { + crate::schema::clan_member::Entity::update_many() + .filter(crate::schema::clan_member::Column::UserId.eq(user_id)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Invited)) + .col_expr(crate::schema::clan_member::Column::Status, sea_orm::sea_query::Expr::value(crate::schema::clan_member::ClanMemberStatus::Deactivated)) + .exec(self.orm.as_ref()) + .await?; + Ok(()) + } + + pub async fn count_clan_members_by_clan_id(&self, clan_id: i32) -> Result { + crate::schema::clan_member::Entity::find() + .find_also_related(crate::schema::user::Entity) + .filter(crate::schema::clan_member::Column::ClanId.eq(clan_id)) + .filter(crate::schema::clan_member::Column::Status.eq(crate::schema::clan_member::ClanMemberStatus::Confirmed)) + .count(self.orm.as_ref()) + .await + } + pub async fn metrics(&self) -> super::DatabaseMetrics { self.metrics.lock().unwrap().snapshot() } diff --git a/rc_social_room/src/data/clan.rs b/rc_social_room/src/data/clan.rs index 0d204ff..52ea3f4 100644 --- a/rc_social_room/src/data/clan.rs +++ b/rc_social_room/src/data/clan.rs @@ -30,25 +30,84 @@ impl ClanMember { #[repr(u8)] #[derive(Copy, Clone)] pub enum ClanMemberState { - Idk0, - Idk1, - Idk2, - // TODO figure out how many of these there are (and what they mean) + Invited = 0, // Invited + Confirmed = 1, // Confirmed } #[repr(u8)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub enum ClanMemberRank { Member = 0, Officer = 1, Leader = 2, } +impl ClanMemberRank { + #[inline] + pub fn from_core(rank: oj_rc_core::persist::user::ClanMemberRank) -> Self { + match rank { + oj_rc_core::persist::user::ClanMemberRank::Member => Self::Member, + oj_rc_core::persist::user::ClanMemberRank::Officer => Self::Officer, + oj_rc_core::persist::user::ClanMemberRank::Leader => Self::Leader, + } + } + + #[inline] + pub fn to_core(self) -> oj_rc_core::persist::user::ClanMemberRank { + match self { + Self::Member => oj_rc_core::persist::user::ClanMemberRank::Member, + Self::Officer => oj_rc_core::persist::user::ClanMemberRank::Officer, + Self::Leader => oj_rc_core::persist::user::ClanMemberRank::Leader, + } + } + + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::Member), + 1 => Some(Self::Officer), + 2 => Some(Self::Leader), + _ => None, + } + } +} + #[repr(u8)] #[derive(Copy, Clone)] pub enum ClanType { - Open = 1, - Closed = 2, + Open = 0, + Closed = 1, +} + +impl ClanType { + #[inline] + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::Open), + 1 => Some(Self::Closed), + _ => None + } + } + + #[inline] + pub fn to_u8(self) -> u8 { + self as u8 + } + + #[inline] + pub fn to_core(self) -> oj_rc_core::persist::user::ClanType { + match self { + Self::Open => oj_rc_core::persist::user::ClanType::Open, + Self::Closed => oj_rc_core::persist::user::ClanType::Closed, + } + } + + #[inline] + pub fn from_core(ty: oj_rc_core::persist::user::ClanType) -> Self { + match ty { + oj_rc_core::persist::user::ClanType::Open => Self::Open, + oj_rc_core::persist::user::ClanType::Closed => Self::Closed, + } + } } pub struct ClanInfo { diff --git a/rc_social_room/src/data/clan_invite.rs b/rc_social_room/src/data/clan_invite.rs index f1ac443..4cd94ff 100644 --- a/rc_social_room/src/data/clan_invite.rs +++ b/rc_social_room/src/data/clan_invite.rs @@ -21,4 +21,15 @@ impl ClanInviteInfo { (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)), ].into()) } + + pub fn from_core(core: oj_rc_core::persist::user::ClanInviteData) -> Self { + Self { + username: core.public_id, + display_name: core.display_name, + clan_name: core.clan_name, + clan_size: core.size, + use_custom_avatar: core.avatar_id.is_none(), + avatar_id: core.avatar_id.unwrap_or_default(), + } + } } diff --git a/rc_social_room/src/events/clan_data_changed.rs b/rc_social_room/src/events/clan_data_changed.rs new file mode 100644 index 0000000..e5d6272 --- /dev/null +++ b/rc_social_room/src/events/clan_data_changed.rs @@ -0,0 +1,33 @@ +#[derive(Clone)] +pub struct ClanDataUpdated { + pub description: Option, + pub ty: Option, +} + +impl ClanDataUpdated { + pub const CODE: u8 = 27; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(2); + if let Some(description) = &self.description { + params.insert(32, polariton::operation::Typed::Str(description.into())); + } + if let Some(ty) = self.ty { + params.insert(34, polariton::operation::Typed::Int(ty.to_u8() as _)); + } + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanDataUpdated { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_invite_cancelled.rs b/rc_social_room/src/events/clan_invite_cancelled.rs new file mode 100644 index 0000000..dc5eaf3 --- /dev/null +++ b/rc_social_room/src/events/clan_invite_cancelled.rs @@ -0,0 +1,26 @@ +pub struct ClanInviteCancelled { + pub clan_name: String, +} + +impl ClanInviteCancelled { + pub const CODE: u8 = 25; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(1); + params.insert(31, polariton::operation::Typed::Str(self.clan_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanInviteCancelled { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_invite_received.rs b/rc_social_room/src/events/clan_invite_received.rs new file mode 100644 index 0000000..f909c6a --- /dev/null +++ b/rc_social_room/src/events/clan_invite_received.rs @@ -0,0 +1,36 @@ +pub struct ClanInviteReceived { + pub inviter_public_id: String, + pub inviter_display_name: String, + pub clan_size: i32, + pub clan_name: String, + pub avatar_id: Option, +} + +impl ClanInviteReceived { + pub const CODE: u8 = 21; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(5); + params.insert(1, polariton::operation::Typed::Str(self.inviter_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.inviter_display_name.clone().into())); + params.insert(31, polariton::operation::Typed::Str(self.clan_name.clone().into())); + params.insert(35, polariton::operation::Typed::Int(self.clan_size)); + + params.insert(13, polariton::operation::Typed::Bool(self.avatar_id.is_none())); + params.insert(14, polariton::operation::Typed::Int(self.avatar_id.unwrap_or_default())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanInviteReceived { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_member_data_changed.rs b/rc_social_room/src/events/clan_member_data_changed.rs new file mode 100644 index 0000000..e35f6c8 --- /dev/null +++ b/rc_social_room/src/events/clan_member_data_changed.rs @@ -0,0 +1,44 @@ +#[derive(Clone)] +pub struct ClanMemberDataUpdated { + pub member_public_id: String, + pub member_display_name: String, + pub avatar_id: Option, + pub state: Option, + pub rank: Option, + pub is_online: Option, +} + +impl ClanMemberDataUpdated { + pub const CODE: u8 = 26; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(8); + params.insert(1, polariton::operation::Typed::Str(self.member_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.member_display_name.clone().into())); + params.insert(13, polariton::operation::Typed::Bool(self.avatar_id.is_none())); + params.insert(14, polariton::operation::Typed::Int(self.avatar_id.unwrap_or_default())); + if let Some(state) = self.state { + params.insert(37, polariton::operation::Typed::Int(state as _)); + } + if let Some(rank) = self.rank { + params.insert(38, polariton::operation::Typed::Int(rank as _)); + } + if let Some(is_online) = self.is_online { + params.insert(2, polariton::operation::Typed::Bool(is_online)); + } + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanMemberDataUpdated { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_member_joined.rs b/rc_social_room/src/events/clan_member_joined.rs new file mode 100644 index 0000000..6a6d654 --- /dev/null +++ b/rc_social_room/src/events/clan_member_joined.rs @@ -0,0 +1,36 @@ +#[derive(Clone)] +pub struct ClanMemberJoined { + pub joiner_public_id: String, + pub joiner_display_name: String, + pub avatar_id: Option, + pub state: crate::data::clan::ClanMemberState, + pub season_xp: i32, +} + +impl ClanMemberJoined { + pub const CODE: u8 = 22; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(6); + params.insert(1, polariton::operation::Typed::Str(self.joiner_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.joiner_display_name.clone().into())); + params.insert(13, polariton::operation::Typed::Bool(self.avatar_id.is_none())); + params.insert(14, polariton::operation::Typed::Int(self.avatar_id.unwrap_or_default())); + params.insert(37, polariton::operation::Typed::Int(self.state as _)); + params.insert(48, polariton::operation::Typed::Int(self.season_xp)); + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanMemberJoined { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_member_left.rs b/rc_social_room/src/events/clan_member_left.rs new file mode 100644 index 0000000..da552f2 --- /dev/null +++ b/rc_social_room/src/events/clan_member_left.rs @@ -0,0 +1,31 @@ +#[derive(Clone)] +pub struct ClanMemberLeft { + pub leaver_public_id: String, + pub new_leader_public_id: Option, +} + +impl ClanMemberLeft { + pub const CODE: u8 = 23; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(2); + params.insert(1, polariton::operation::Typed::Str(self.leaver_public_id.clone().into())); + if let Some(new_leader) = &self.new_leader_public_id { + params.insert(45, polariton::operation::Typed::Str(new_leader.into())); + } + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanMemberLeft { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_member_removed.rs b/rc_social_room/src/events/clan_member_removed.rs new file mode 100644 index 0000000..401bf87 --- /dev/null +++ b/rc_social_room/src/events/clan_member_removed.rs @@ -0,0 +1,22 @@ +pub struct ClanMemberRemoved; + +impl ClanMemberRemoved { + pub const CODE: u8 = 24; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + polariton::operation::ParameterTable::with_capacity(0) + } +} + +impl polariton_server::events::IntoEvent for ClanMemberRemoved { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/clan_renamed.rs b/rc_social_room/src/events/clan_renamed.rs new file mode 100644 index 0000000..f8af707 --- /dev/null +++ b/rc_social_room/src/events/clan_renamed.rs @@ -0,0 +1,30 @@ +pub struct ClanRenamed { + pub old_name: String, + pub new_name: String, + pub admin_public_id: String, +} + +impl ClanRenamed { + pub const CODE: u8 = 28; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(3); + params.insert(31, polariton::operation::Typed::Str(self.old_name.clone().into())); + params.insert(44, polariton::operation::Typed::Str(self.new_name.clone().into())); + params.insert(1, polariton::operation::Typed::Str(self.admin_public_id.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for ClanRenamed { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_event_params(), + } + } +} diff --git a/rc_social_room/src/events/mod.rs b/rc_social_room/src/events/mod.rs index 2a08c19..f6a1e1a 100644 --- a/rc_social_room/src/events/mod.rs +++ b/rc_social_room/src/events/mod.rs @@ -12,3 +12,11 @@ pub mod platoon_removed; pub mod platoon_leader_changed; pub mod platoon_member_kick; pub mod platoon_member_avatar_update; +pub mod clan_invite_received; +pub mod clan_member_joined; +pub mod clan_member_left; +pub mod clan_member_removed; +pub mod clan_invite_cancelled; +pub mod clan_member_data_changed; +pub mod clan_data_changed; +//pub mod clan_renamed; diff --git a/rc_social_room/src/main.rs b/rc_social_room/src/main.rs index 1a61516..617a9dc 100644 --- a/rc_social_room/src/main.rs +++ b/rc_social_room/src/main.rs @@ -50,6 +50,7 @@ async fn main() -> std::io::Result<()> { let start_time = chrono::Utc::now(); START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); + log::info!("social_room ready"); if args.once { log::warn!("Handling first connection and then exiting"); let (socket, address) = listener.accept().await?; diff --git a/rc_social_room/src/operations/clan_create.rs b/rc_social_room/src/operations/clan_create.rs new file mode 100644 index 0000000..2e4c61c --- /dev/null +++ b/rc_social_room/src/operations/clan_create.rs @@ -0,0 +1,76 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::clan::*; + +const CODE: u8 = 31; + +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; in +const CLAN_DESC_PARAM_KEY: u8 = 32; // str; in +const CLAN_TYPE_PARAM_KEY: u8 = 34; // int (ClanType); in +const CLAN_AVATAR_PARAM_KEY: u8 = 33; // bytes; in +const MEMBERS_PARAM_KEY: u8 = 36; // arr of hashmap (ClanMember); out + +pub(super) struct ClanCreator { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanCreator { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(clan_name)) = params.remove(&CLAN_NAME_PARAM_KEY) { + if let Some(Typed::Str(clan_desc)) = params.remove(&CLAN_DESC_PARAM_KEY) { + if let Some(Typed::Int(clan_ty)) = params.remove(&CLAN_TYPE_PARAM_KEY) { + let ty = ClanType::from_u8(clan_ty as u8) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("Invalid clan type {}", clan_ty), + ))?; + let clavatar = if let Some(Typed::Bytes(clavatar)) = params.remove(&CLAN_AVATAR_PARAM_KEY) { + clavatar.vec + } else { + Vec::default() + }; + let new_clan = oj_rc_core::persist::user::ClanData { + name: clan_name.string, + description: clan_desc.string, + ty: ty.to_core(), + size: 0, + }; + let user_info = user.user()?; + let new_members = user_info.create_clan(new_clan, clavatar).await?; + let mut online_clan_members: std::collections::HashSet = new_members.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + params.insert(MEMBERS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, + custom_ty: None, + items: new_members.into_iter() + .map(|member| ClanMember { + is_online: online_clan_members.contains(&member.public_id), + username: member.public_id, + display_name: member.display_name, + member_state: if member.is_confirmed { ClanMemberState::Confirmed } else { ClanMemberState::Invited }, + use_custom_avatar: member.avatar_id.is_none(), + avatar_id: member.avatar_id.unwrap_or_default(), + rank: ClanMemberRank::from_core(member.rank), + season_xp: member.season_xp, + }.as_transmissible()) + .collect() + })); + } + } + } + Ok(params) + } +} + +pub(super) fn creat_clan_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanCreator { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/clan_experience_poll.rs b/rc_social_room/src/operations/clan_experience_poll.rs new file mode 100644 index 0000000..a6ff329 --- /dev/null +++ b/rc_social_room/src/operations/clan_experience_poll.rs @@ -0,0 +1,33 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed, Dict}; + +const CODE: u8 = 57; + +const EXPERIENCE_TABLE_PARAM_KEY: u8 = 48; // dict string -> int; out +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; in + +pub(super) struct ClanExperiencePoller; + +#[async_trait::async_trait] +impl SimpleOperation for ClanExperiencePoller { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, _user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(clan)) = params.remove(&CLAN_NAME_PARAM_KEY) { + log::debug!("Not providing clan experience for clan {} (not implemented)", clan.string); + params.insert(EXPERIENCE_TABLE_PARAM_KEY, Typed::Dict(Dict { + key_ty: polariton::serdes::TypePrefix::Str, + val_ty: polariton::serdes::TypePrefix::Int, + items: vec![ + (Typed::Str("NGniusness".into()), Typed::Int(i32::MAX)), + ], + })); + } + Ok(params) + } +} + +pub(super) fn clan_experience_provider() -> SimpleOpImpl { + SimpleOpImpl::new(ClanExperiencePoller) +} diff --git a/rc_social_room/src/operations/clan_info.rs b/rc_social_room/src/operations/clan_info.rs index 8c54054..b6ee3f6 100644 --- a/rc_social_room/src/operations/clan_info.rs +++ b/rc_social_room/src/operations/clan_info.rs @@ -1,15 +1,17 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed, Arr}; use crate::data::clan::*; +const CODE: u8 = 33; + const CLAN_NAME_PARAM_KEY: u8 = 31; // in and out const MEMBERS_PARAM_KEY: u8 = 36; // out only const ROBITS_CONVERSION_PARAM_KEY: u8 = 51; // out only const CLAN_DESCRIPTION_PARAM_KEY: u8 = 32; // out only -const CLAN_TYPE_PARAM_KEY: u8 = 32; // out only +const CLAN_TYPE_PARAM_KEY: u8 = 34; // out only -pub(super) fn clan_info_provider() -> SimpleFunc<33, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { +/*pub(super) fn clan_info_provider() -> SimpleFunc<33, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { SimpleFunc::new(|params, _| { let mut params = params.to_dict(); if let Some(Typed::Str(clan_name)) = params.get(&CLAN_NAME_PARAM_KEY) { @@ -59,4 +61,61 @@ pub(super) fn clan_info_provider() -> SimpleFunc<33, crate::User Ok(params.into()) }) +}*/ + +pub(super) struct ClanInfoGetter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInfoGetter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let clan_info = if let Some(Typed::Str(clan_name)) = params.remove(&CLAN_NAME_PARAM_KEY) { + // retrieve info for other clan (return nothing if clan does not exist) + log::debug!("Getting {} clan info", clan_name.string); + user_info.clan_info(&clan_name.string).await? + } else { + // retrieve info for user's own clan (return nothing if user not a part of a clan) + log::debug!("Getting own clan info"); + user_info.my_clan_info(true).await? + }; + if let Some((clan_info, members_info)) = clan_info { + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + log::debug!("Found clan info for {} ({} members, {} online)", clan_info.name, members_info.len(), online_clan_members.len()); + params.insert(MEMBERS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, + items: members_info.into_iter() + .map(|member| ClanMember { + is_online: online_clan_members.contains(&member.public_id), + username: member.public_id, + display_name: member.display_name, + member_state: if member.is_confirmed { ClanMemberState::Confirmed } else { ClanMemberState::Invited }, + use_custom_avatar: member.avatar_id.is_none(), + avatar_id: member.avatar_id.unwrap_or_default(), + rank: ClanMemberRank::from_core(member.rank), + season_xp: member.season_xp, + }.as_transmissible()) + .collect(), + })); + params.insert(ROBITS_CONVERSION_PARAM_KEY, Typed::Float(0.5)); // TODO ??? + params.insert(CLAN_DESCRIPTION_PARAM_KEY, Typed::Str(clan_info.description.into())); + params.insert(CLAN_NAME_PARAM_KEY, Typed::Str(clan_info.name.into())); + params.insert(CLAN_TYPE_PARAM_KEY, Typed::Int(ClanType::from_core(clan_info.ty).to_u8() as _)); + } + Ok(params) + } +} + +pub(super) fn clan_info_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInfoGetter { + social: init_ctx.social.clone(), + }) } diff --git a/rc_social_room/src/operations/clan_invite.rs b/rc_social_room/src/operations/clan_invite.rs index 441cd4d..df9fa59 100644 --- a/rc_social_room/src/operations/clan_invite.rs +++ b/rc_social_room/src/operations/clan_invite.rs @@ -1,11 +1,15 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed, Arr}; +use crate::data::clan_invite::*; + //use crate::data::clan_invite::*; -const PARAM_KEY: u8 = 42; +const CODE: u8 = 39; -pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { +const INVITES_PARAM_KEY: u8 = 42; + +/*pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { SimpleFunc::new(|params, _| { let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::::Arr(Arr { @@ -25,4 +29,41 @@ pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::U })); Ok(params.into()) }) +}*/ + +pub(super) struct ClanInviteLister { + //social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviteLister { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let invites = user_info.my_clan_invites().await?; + let typed_invites = invites.into_iter() + .map(|invite| ClanInviteInfo { + username: invite.public_id, + display_name: invite.display_name, + clan_name: invite.clan_name, + use_custom_avatar: invite.avatar_id.is_none(), + avatar_id: invite.avatar_id.unwrap_or_default(), + clan_size: invite.size, + }.as_transmissible()) + .collect(); + params.insert(INVITES_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, + items: typed_invites, + })); + Ok(params) + } +} + +pub(super) fn clan_invites_provider(_init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviteLister { + //social: init_ctx.social.clone(), + }) } diff --git a/rc_social_room/src/operations/clan_invite_accept.rs b/rc_social_room/src/operations/clan_invite_accept.rs new file mode 100644 index 0000000..50940c9 --- /dev/null +++ b/rc_social_room/src/operations/clan_invite_accept.rs @@ -0,0 +1,80 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::clan::*; + +const CODE: u8 = 36; + +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; in and out +const CLAN_DESCRIPTION_PARAM_KEY: u8 = 32; // str; out +const CLAN_TYPE_PARAM_KEY: u8 = 34; // int; out +const ROBITS_CONVERSION_PARAM_KEY: u8 = 51; // out only +const MEMBERS_PARAM_KEY: u8 = 36; // hashmap (ClanMember); out + +pub(super) struct ClanInviteAccepter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviteAccepter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(clan_name)) = params.remove(&CLAN_NAME_PARAM_KEY) { + let user_info = user.user()?; + log::debug!("User {} wants to accept invite to clan {}", user_info.public_id(), clan_name.string); + let (clan_info, members_info) = user_info.join_clan(&clan_name.string).await?; + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + log::debug!("Found clan info for {} ({} members, {} online)", clan_info.name, members_info.len(), online_clan_members.len()); + + let my_pub_id = user_info.public_id(); + let self_member = members_info.iter() + .find(|mem| mem.public_id == my_pub_id) + .unwrap(); + let event = crate::events::clan_member_data_changed::ClanMemberDataUpdated { + member_public_id: my_pub_id.to_owned(), + member_display_name: user_info.display_name().to_owned(), + avatar_id: self_member.avatar_id, + state: Some(crate::data::clan::ClanMemberState::Confirmed), + rank: None, + is_online: Some(true), + }; + for online_member in online_clan_members.iter() { + if online_member == my_pub_id { continue; } + self.social.send_event_to(online_member, event.clone()).await; + } + + params.insert(MEMBERS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, + items: members_info.into_iter() + .map(|member| ClanMember { + is_online: online_clan_members.contains(&member.public_id), + username: member.public_id, + display_name: member.display_name, + member_state: if member.is_confirmed { ClanMemberState::Confirmed } else { ClanMemberState::Invited }, + use_custom_avatar: member.avatar_id.is_none(), + avatar_id: member.avatar_id.unwrap_or_default(), + rank: ClanMemberRank::from_core(member.rank), + season_xp: member.season_xp, + }.as_transmissible()) + .collect(), + })); + params.insert(ROBITS_CONVERSION_PARAM_KEY, Typed::Float(0.5)); // TODO + params.insert(CLAN_DESCRIPTION_PARAM_KEY, Typed::Str(clan_info.description.into())); + params.insert(CLAN_NAME_PARAM_KEY, Typed::Str(clan_info.name.into())); + params.insert(CLAN_TYPE_PARAM_KEY, Typed::Int(ClanType::from_core(clan_info.ty).to_u8() as _)); + } + Ok(params) + } +} + +pub(super) fn clan_accept_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviteAccepter { + social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_invite_cancel.rs b/rc_social_room/src/operations/clan_invite_cancel.rs new file mode 100644 index 0000000..22ab05c --- /dev/null +++ b/rc_social_room/src/operations/clan_invite_cancel.rs @@ -0,0 +1,50 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 45; + +const PUBLIC_ID_PARAM_KEY: u8 = 1; // str; in + +pub(super) struct ClanInviteCanceller { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviteCanceller { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(invitee)) = params.remove(&PUBLIC_ID_PARAM_KEY) { + let user_info = user.user()?; + log::debug!("User {} wants to cancel invite to user {}", user_info.public_id(), invitee.string); + let (clan_info, members_info) = user_info.cancel_invite_to_clan(&invitee.string).await?; + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + + let event = crate::events::clan_invite_cancelled::ClanInviteCancelled { + clan_name: clan_info.name, + }; + self.social.send_event_to(&invitee.string, event).await; + + let my_pub_id = user_info.public_id(); + let event = crate::events::clan_member_left::ClanMemberLeft { + leaver_public_id: my_pub_id.to_owned(), + new_leader_public_id: None, + }; + for online_member in online_clan_members.iter() { + if online_member == my_pub_id { continue; } + self.social.send_event_to(online_member, event.clone()).await; + } + } + Ok(params) + } +} + +pub(super) fn clan_cancel_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviteCanceller { + social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_invite_decline.rs b/rc_social_room/src/operations/clan_invite_decline.rs new file mode 100644 index 0000000..a40eb6f --- /dev/null +++ b/rc_social_room/src/operations/clan_invite_decline.rs @@ -0,0 +1,45 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 37; + +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; in and out + +pub(super) struct ClanInviteDecliner { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviteDecliner { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(clan_name)) = params.remove(&CLAN_NAME_PARAM_KEY) { + let user_info = user.user()?; + log::debug!("User {} wants to decline invite to clan {}", user_info.public_id(), clan_name.string); + let members_info = user_info.decline_clan_invite(&clan_name.string).await?; + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + + let my_pub_id = user_info.public_id(); + let event = crate::events::clan_member_left::ClanMemberLeft { + leaver_public_id: my_pub_id.to_owned(), + new_leader_public_id: None, + }; + for online_member in online_clan_members.iter() { + if online_member == my_pub_id { continue; } + self.social.send_event_to(online_member, event.clone()).await; + } + } + Ok(params) + } +} + +pub(super) fn clan_decline_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviteDecliner { + social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_invite_decline_all.rs b/rc_social_room/src/operations/clan_invite_decline_all.rs new file mode 100644 index 0000000..7e52aa4 --- /dev/null +++ b/rc_social_room/src/operations/clan_invite_decline_all.rs @@ -0,0 +1,28 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::ParameterTable; + +const CODE: u8 = 44; + +pub(super) struct ClanInviteDeclineAller { + //social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviteDeclineAller { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + log::debug!("User {} wants to decline all clan invites", user_info.public_id()); + user_info.decline_all_clan_invites().await?; + // TODO send clan member leave events to all clans + Ok(params) + } +} + +pub(super) fn clan_decline_all_provider(_init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviteDeclineAller { + //social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_invite_to.rs b/rc_social_room/src/operations/clan_invite_to.rs new file mode 100644 index 0000000..03e1cdf --- /dev/null +++ b/rc_social_room/src/operations/clan_invite_to.rs @@ -0,0 +1,64 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 35; + +const PUBLIC_ID_PARAM_KEY: u8 = 1; // str; in +const DISPLAY_NAME_PARAM_KEY: u8 = 75; // str; out +const CUSTOM_AVATAR_PARAM_KEY: u8 = 13; // bool; out +const AVATAR_ID_PARAM_KEY: u8 = 14; // int; out +const SEASON_XP_PARAM_KEY: u8 = 48; // int; out + +pub(super) struct ClanInviter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanInviter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(invitee)) = params.remove(&PUBLIC_ID_PARAM_KEY) { + let user_info = user.user()?; + log::debug!("User {} wants to invite {} to their clan", user_info.account_id(), invitee.string); + let invitee = user_info.invite_to_clan(&invitee.string).await?; + params.insert(PUBLIC_ID_PARAM_KEY, Typed::Str(invitee.public_id.clone().into())); + params.insert(DISPLAY_NAME_PARAM_KEY, Typed::Str(invitee.display_name.clone().into())); + params.insert(CUSTOM_AVATAR_PARAM_KEY, Typed::Bool(invitee.avatar_id.is_none())); + params.insert(AVATAR_ID_PARAM_KEY, Typed::Int(invitee.avatar_id.unwrap_or_default())); + params.insert(SEASON_XP_PARAM_KEY, Typed::Int(invitee.season_xp)); + if let Some((my_clan, my_clan_members)) = user_info.my_clan_info(true).await? { + let my_pub_id = user_info.public_id(); + let my_soc_info = user_info.list_social_info(&[my_pub_id.to_owned()]).await?; + let event = crate::events::clan_invite_received::ClanInviteReceived { + inviter_public_id: my_pub_id.to_owned(), + inviter_display_name: user_info.display_name().to_owned(), + clan_size: my_clan.size, + clan_name: my_clan.name, + avatar_id: my_soc_info.first().and_then(|x| x.avatar_id), + }; + self.social.send_event_to(&invitee.public_id, event).await; + + let event = crate::events::clan_member_joined::ClanMemberJoined { + joiner_public_id: invitee.public_id, + joiner_display_name: invitee.display_name, + avatar_id: invitee.avatar_id, + state: crate::data::clan::ClanMemberState::Invited, + season_xp: invitee.season_xp, + }; + for member in my_clan_members { + if member.public_id == my_pub_id { continue; } + self.social.send_event_to(&member.public_id, event.clone()).await; + } + } + } + Ok(params) + } +} + +pub(super) fn invite_to_clan_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanInviter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/clan_join.rs b/rc_social_room/src/operations/clan_join.rs new file mode 100644 index 0000000..ecbab59 --- /dev/null +++ b/rc_social_room/src/operations/clan_join.rs @@ -0,0 +1,79 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed, Arr}; + +use crate::data::clan::*; + +const CODE: u8 = 34; + +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; in and out +const CLAN_DESCRIPTION_PARAM_KEY: u8 = 32; // str; out +const CLAN_TYPE_PARAM_KEY: u8 = 34; // int; out +const ROBITS_CONVERSION_PARAM_KEY: u8 = 51; // out only +const MEMBERS_PARAM_KEY: u8 = 36; // hashmap (ClanMember); out + +pub(super) struct ClanJoiner { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanJoiner { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(clan_name)) = params.remove(&CLAN_NAME_PARAM_KEY) { + let user_info = user.user()?; + log::debug!("User {} wants to join clan {}", user_info.public_id(), clan_name.string); + let (clan_info, members_info) = user_info.join_clan(&clan_name.string).await?; + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + log::debug!("Found clan info for {} ({} members, {} online)", clan_info.name, members_info.len(), online_clan_members.len()); + + let my_pub_id = user_info.public_id(); + let self_member = members_info.iter() + .find(|mem| mem.public_id == my_pub_id) + .unwrap(); + let event = crate::events::clan_member_joined::ClanMemberJoined { + joiner_public_id: my_pub_id.to_owned(), + joiner_display_name: user_info.display_name().to_owned(), + avatar_id: self_member.avatar_id, + state: crate::data::clan::ClanMemberState::Confirmed, + season_xp: 0, + }; + for online_member in online_clan_members.iter() { + if online_member == my_pub_id { continue; } + self.social.send_event_to(online_member, event.clone()).await; + } + + params.insert(MEMBERS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, + items: members_info.into_iter() + .map(|member| ClanMember { + is_online: online_clan_members.contains(&member.public_id), + username: member.public_id, + display_name: member.display_name, + member_state: if member.is_confirmed { ClanMemberState::Confirmed } else { ClanMemberState::Invited }, + use_custom_avatar: member.avatar_id.is_none(), + avatar_id: member.avatar_id.unwrap_or_default(), + rank: ClanMemberRank::from_core(member.rank), + season_xp: member.season_xp, + }.as_transmissible()) + .collect(), + })); + params.insert(ROBITS_CONVERSION_PARAM_KEY, Typed::Float(0.5)); // TODO + params.insert(CLAN_DESCRIPTION_PARAM_KEY, Typed::Str(clan_info.description.into())); + params.insert(CLAN_NAME_PARAM_KEY, Typed::Str(clan_info.name.into())); + params.insert(CLAN_TYPE_PARAM_KEY, Typed::Int(ClanType::from_core(clan_info.ty).to_u8() as _)); + } + Ok(params) + } +} + +pub(super) fn clan_join_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanJoiner { + social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_leave.rs b/rc_social_room/src/operations/clan_leave.rs new file mode 100644 index 0000000..d52b8b1 --- /dev/null +++ b/rc_social_room/src/operations/clan_leave.rs @@ -0,0 +1,42 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::ParameterTable; + +const CODE: u8 = 40; + +pub(super) struct ClanLeaver { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanLeaver { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let mut members = if let Some((_clan, members)) = user_info.my_clan_info(true).await? { + members.into_iter().map(|mem| mem.public_id).collect() + } else { + std::collections::HashSet::default() + }; + log::debug!("User {} wants to leave their clan", user_info.public_id()); + user_info.leave_clan().await?; + self.social.filter_online_only(&mut members).await; + let my_pub_id = user_info.public_id(); + let event = crate::events::clan_member_left::ClanMemberLeft { + leaver_public_id: my_pub_id.to_owned(), + new_leader_public_id: None, + }; + for member in members { + if member == my_pub_id { continue; } + self.social.send_event_to(&member, event.clone()).await; + } + Ok(params) + } +} + +pub(super) fn clan_leave_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanLeaver { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/clan_member_remove.rs b/rc_social_room/src/operations/clan_member_remove.rs new file mode 100644 index 0000000..e56fa58 --- /dev/null +++ b/rc_social_room/src/operations/clan_member_remove.rs @@ -0,0 +1,48 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 41; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in + +pub(super) struct ClanRemover { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanRemover { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(to_remove)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + let mut members = if let Some((_clan, members)) = user_info.my_clan_info(true).await? { + members.into_iter().map(|mem| mem.public_id).collect() + } else { + std::collections::HashSet::default() + }; + log::debug!("User {} wants to remove {} from their clan", user_info.public_id(), to_remove.string); + user_info.remove_user_from_clan(&to_remove.string).await?; + self.social.send_event_to(&to_remove.string, crate::events::clan_member_removed::ClanMemberRemoved).await; + self.social.filter_online_only(&mut members).await; + let my_pub_id = user_info.public_id(); + let event = crate::events::clan_member_left::ClanMemberLeft { + leaver_public_id: to_remove.string.clone(), + new_leader_public_id: None, + }; + for member in members { + if member == to_remove.string { continue; } + if member == my_pub_id { continue; } + self.social.send_event_to(&member, event.clone()).await; + } + } + Ok(params) + } +} + +pub(super) fn clan_remove_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanRemover { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/clan_member_rerank.rs b/rc_social_room/src/operations/clan_member_rerank.rs new file mode 100644 index 0000000..3f3c808 --- /dev/null +++ b/rc_social_room/src/operations/clan_member_rerank.rs @@ -0,0 +1,61 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 38; + +const PUBLIC_ID_PARAM_KEY: u8 = 1; // str; in and out +const MEMBER_RANK_PARAM_KEY: u8 = 38; // int enum (ClanMemberRank); in + +pub(super) struct ClanMemberRankChanger { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanMemberRankChanger { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Str(member)) = params.remove(&PUBLIC_ID_PARAM_KEY) { + if let Some(Typed::Int(new_rank)) = params.remove(&MEMBER_RANK_PARAM_KEY) { + let new_rank = crate::data::clan::ClanMemberRank::from_u8(new_rank as _) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("Invalid clan member rank {}", new_rank), + ))?; + let user_info = user.user()?; + let my_pub_id = user_info.public_id(); + log::debug!("User {} wants to update user {} to clan member rank {:?}", my_pub_id, member.string, new_rank); + let members_info = user_info.update_clan_member(&member.string, new_rank.to_core()).await?; + let mut online_clan_members: std::collections::HashSet = members_info.iter() + .map(|mem| mem.public_id.clone()) + .collect(); + self.social.filter_online_only(&mut online_clan_members).await; + let target_member_opt = members_info.iter() + .find(|mem| mem.public_id == member.string); + if let Some(target_member) = target_member_opt { + let event = crate::events::clan_member_data_changed::ClanMemberDataUpdated { + member_public_id: target_member.public_id.clone(), + member_display_name: target_member.display_name.clone(), + avatar_id: target_member.avatar_id, + state: None, + rank: Some(new_rank), + is_online: Some(true), + }; + for online_member in online_clan_members { + if online_member == my_pub_id { continue; } + //if online_member == member.string { continue; } + self.social.send_event_to(&online_member, event.clone()).await; + } + } + } + } + Ok(params) + } +} + +pub(super) fn clan_rank_change_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanMemberRankChanger { + social: init_ctx.social.clone() + }) +} diff --git a/rc_social_room/src/operations/clan_modify.rs b/rc_social_room/src/operations/clan_modify.rs new file mode 100644 index 0000000..d1bfe22 --- /dev/null +++ b/rc_social_room/src/operations/clan_modify.rs @@ -0,0 +1,86 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +use crate::data::clan::*; + +const CODE: u8 = 42; + +const CLAN_DESC_PARAM_KEY: u8 = 32; // str; in +const CLAN_TYPE_PARAM_KEY: u8 = 34; // int (ClanType); in +const CLAN_AVATAR_PARAM_KEY: u8 = 33; // bytes; in + +pub(super) struct ClanDataChanger { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for ClanDataChanger { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let new_ty = if let Some(Typed::Int(clan_ty)) = params.remove(&CLAN_TYPE_PARAM_KEY) { + let ty = ClanType::from_u8(clan_ty as u8) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("Invalid clan type {}", clan_ty), + ))?; + Some(ty) + } else { + None + }; + let new_clavatar = if let Some(Typed::Bytes(clavatar)) = params.remove(&CLAN_AVATAR_PARAM_KEY) { + if clavatar.vec.is_empty() { + None + } else { + Some(clavatar.vec) + } + } else { + None + }; + let new_desc = if let Some(Typed::Str(clan_desc)) = params.remove(&CLAN_DESC_PARAM_KEY) { + // (technically this is a client bug) + // when the game client updates a different field, the description will get sent as a zero-length string + // which is impossible to differentiate from someone saving the description as zero-length + // null/non-existent *should* be different than an empty string + // So, let's try the closest guess the server can achieve: + // => assume a zero-length string means null *unless* all the other fields are null + if clan_desc.string.is_empty() && (new_ty.is_some() || new_clavatar.is_some()) { + None + } else { + Some(clan_desc.string) + } + } else { + None + }; + if new_desc.is_none() && new_ty.is_none() && new_clavatar.is_none() { + log::debug!("Got clan data change no-op (doing nothing)"); + return Ok(params); + } + let user_info = user.user()?; + let members = user_info.update_clan( + None, + new_desc.clone(), + new_ty.map(|x| x.to_core()), + new_clavatar, + ).await?; + let mut online_members = members.into_iter().map(|mem| mem.public_id).collect(); + self.social.filter_online_only(&mut online_members).await; + let my_pub_id = user_info.public_id(); + let event = crate::events::clan_data_changed::ClanDataUpdated { + description: new_desc, + ty: new_ty, + }; + for member in online_members { + if member == my_pub_id { continue; } + self.social.send_event_to(&member, event.clone()).await; + } + Ok(params) + } +} + +pub(super) fn update_clan_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(ClanDataChanger { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/clan_my_info.rs b/rc_social_room/src/operations/clan_my_info.rs new file mode 100644 index 0000000..7046b7b --- /dev/null +++ b/rc_social_room/src/operations/clan_my_info.rs @@ -0,0 +1,34 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +use crate::data::clan::*; + +const CODE: u8 = 43; + +const CLAN_NAME_PARAM_KEY: u8 = 31; // in and out +const CLAN_DESCRIPTION_PARAM_KEY: u8 = 32; // out only +const CLAN_TYPE_PARAM_KEY: u8 = 34; // out only + +pub(super) struct MyClanInfoGetter {} + +#[async_trait::async_trait] +impl SimpleOperation for MyClanInfoGetter { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let user_info = user.user()?; + let clan_info = user_info.my_clan_info(false).await?; + if let Some((clan_info, _members_info)) = clan_info { + log::debug!("Found my clan info for {}", clan_info.name); + params.insert(CLAN_DESCRIPTION_PARAM_KEY, Typed::Str(clan_info.description.into())); + params.insert(CLAN_NAME_PARAM_KEY, Typed::Str(clan_info.name.into())); + params.insert(CLAN_TYPE_PARAM_KEY, Typed::Int(ClanType::from_core(clan_info.ty).to_u8() as _)); + } + Ok(params) + } +} + +pub(super) fn clan_info_provider() -> SimpleOpImpl { + SimpleOpImpl::new(MyClanInfoGetter {}) +} diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index caf23b8..f5f18d7 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -23,6 +23,19 @@ mod platoon_leave; mod platoon_kick; mod platoon_invites; mod platoon_status; +mod clan_create; +mod clan_my_info; +mod clan_join; +mod clan_leave; +mod clan_member_remove; +mod clan_modify; +mod clan_invite_to; +mod clan_invite_accept; +mod clan_invite_decline; +mod clan_invite_decline_all; +mod clan_invite_cancel; +mod clan_member_rerank; +mod clan_experience_poll; use polariton_server::operations::OperationsHandler; @@ -30,14 +43,12 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler::new() .modify(oj_rc_core::polariton::RcOpModifier) .add(more_auth::more_lobby_auth(init_ctx)) - //.add(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan) .add(friend_list::friends_provider(init_ctx)) // TODO friend object parsing Token: 0x0200169C RID: 5788 .add(settings::settings_provider()) // TODO save settings persistently - .add(polariton_server::operations::Ack::<43, _>::default()) // get my clan info (this is equivalent to not being in a clan) - .add(clan_invite::clan_invites_provider()) - //.add(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite) + .add(clan_my_info::clan_info_provider()) + .add(clan_invite::clan_invites_provider(init_ctx)) .add(platoon_invites::platoon_pending_provider(init_ctx)) - .add(clan_info::clan_info_provider()) + .add(clan_info::clan_info_provider(init_ctx)) .add(search_clan::search_clans_provider()) .add(polariton_server::operations::Ack::<52, _>::default()) // validate pending season rewards (this just always needs to be ack-ed) .add(season_rewards::season_rewards_provider()) @@ -60,4 +71,16 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler() -> SimpleFunc<32, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { +/*pub(super) fn search_clans_provider() -> SimpleFunc<32, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { SimpleFunc::new(|params, _| { let mut params = params.to_dict(); if let Some(Typed::Str(s)) = params.get(&STRING_PARAM_KEY) { @@ -35,4 +37,69 @@ pub(super) fn search_clans_provider() -> SimpleFunc<32, crate::U })); Ok(params.into()) }) +}*/ + +pub(super) struct ClansSearcher {} + +#[async_trait::async_trait] +impl SimpleOperation for ClansSearcher { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let search_string = if let Some(Typed::Str(search)) = params.remove(&STRING_PARAM_KEY) { + search.string + } else { + String::default() + }; + if let Some(Typed::Int(days_since_active)) = params.remove(&DAYS_SINCE_ACTIVE_PARAM_KEY) { + if let Some(Typed::Int(start_range)) = params.remove(&START_RANGE_PARAM_KEY) { + if let Some(Typed::Int(end_range)) = params.remove(&END_RANGE_PARAM_KEY) { + if let Some(Typed::Arr(clan_types)) = params.remove(&TYPES_PARAM_KEY) { + let mut types = Vec::with_capacity(clan_types.items.len()); + for item in clan_types.items { + if let Typed::Int(ty) = item { + types.push( + crate::data::clan::ClanType::from_u8(ty as u8) + .ok_or_else(|| SimpleOpError::with_message( + oj_rc_core::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("Invalid clan type {}", ty), + ))? + .to_core() + ); + } + } + let search_query = oj_rc_core::persist::user::ClanSearchQuery { + search_string, + days_since_active, + start_range, + end_range, + types, + }; + let user_info = user.user()?; + log::debug!("Searching for clans with query {:?}", search_query); + let results = user_info.search_clan(search_query).await?; + log::debug!("Got {} clans from search", results.len()); + params.insert(RESULTS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + custom_ty: None, + items: results.into_iter() + .map(|clan| ClanInfo { + clan_name: clan.name, + clan_description: clan.description, + clan_type: ClanType::from_core(clan.ty), + clan_size: clan.size, + }.as_transmissible()) + .collect(), + })); + } + } + } + } + Ok(params) + } +} + +pub(super) fn search_clans_provider() -> SimpleOpImpl { + SimpleOpImpl::new(ClansSearcher {}) }