diff --git a/Cargo.lock b/Cargo.lock index 3a0e241..72e0e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3013,18 +3013,14 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polariton" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dcc388d509743a0419f5a5197a8c79e0f3fd484d5bd0b60e6f361887a8aa301" +version = "0.6.0" dependencies = [ "tokio", ] [[package]] name = "polariton_server" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ea2a26537f7434f154e34d7c522ab10552442241e608e919d7cfc413ac3abc" +version = "0.6.0" dependencies = [ "async-trait", "log", diff --git a/Cargo.toml b/Cargo.toml index c291cf8..a4e35a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,10 +36,10 @@ libfj = { version = "0.10" } log = "0.4" env_logger = "0.11" clap = { version = "4.5", features = [ "derive" ] } -#polariton = { version = "0.5", path = "../polariton", features = [ "tokio-async" ] } -#polariton_server = { version = "0.5", path = "../polariton/server", features = [ "tokio-async" ] } -polariton = { version = "0.4", features = [ "tokio-async" ] } -polariton_server = { version = "0.4", features = [ "tokio-async" ] } +polariton = { version = "0.6", path = "../polariton", features = [ "tokio-async" ] } +polariton_server = { version = "0.6", path = "../polariton/server", features = [ "tokio-async" ] } +#polariton = { version = "0.6", features = [ "tokio-async" ] } +#polariton_server = { version = "0.6", features = [ "tokio-async" ] } serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" async-trait = "0.1" diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 8cda28a..813c80c 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, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser}; +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}; 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 0335dc0..fc45d46 100644 --- a/rc_core/src/persist/user/social.rs +++ b/rc_core/src/persist/user/social.rs @@ -1,7 +1,86 @@ use super::account_json::UserData; +impl UserData { + async fn apply_friend_state_to(&self, public_id: String, state: oj_rc_database::schema::friend::FriendStatus) -> Result { + let target_user = self.db.user_by_public_id(public_id.clone()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} to statify friend 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 statify friend: {}", e), + ) + })?; + if let Some(target_user) = target_user { + self.db.update_friends_state(target_user.id, self.account.id, state).await + .map_err(|e| { + log::error!("Failed to update friend state of user {} 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 update friend state: {}", e), + ) + })?; + Ok(true) + } else { + log::debug!("Cannot statify non-existent user {} friend request", public_id); + Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, + format!("Failed to find user {} to statify friend", public_id), + )) + } + } +} + #[async_trait::async_trait] impl super::SocialUser for UserData { + async fn accept_friend(&self, username: String) -> Result { + self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Accepted).await + } + + async fn decline_friend(&self, username: String) -> Result { + self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Declined).await + } + + async fn cancel_friend(&self, username: String) -> Result { + self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Cancelled).await + } + + async fn remove_friend(&self, username: String) -> Result { + self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Removed).await + } + + async fn list_friends(&self) -> Result, polariton_server::operations::SimpleOpError> { + let friends = self.db.friends_by_user_id(self.account.id, oj_rc_database::schema::friend::FINAL_STATUSES).await + .map_err(|e| { + log::error!("Failed to retrieve friends for user {} : {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve friends: {}", e), + ) + })?; + let friend_ids: Vec = friends.iter().map(|(_, user)| user.id).collect(); + let friend_avatars = self.db.user_auxs_by_user_ids_and_descriptor(friend_ids, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await + .map_err(|e| { + log::error!("Failed to retrieve friend avatars for user {} : {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve friend avatars: {}", e), + ) + })?; + let friend_avatar_map: std::collections::HashMap = friend_avatars.iter() + .filter_map(|avatar| avatar.data.parse().ok().map(|avatar_id| (avatar.user_id, avatar_id))) + .collect(); + Ok(friends.into_iter() + .map(|(friend, user)| super::FriendData { + public_id: user.public_id, + display_name: user.display_name, + clan_name: None, // TODO clan + state: super::FriendInviteStatus::from_db(friend.state), + avatar_id: friend_avatar_map.get(&user.id).copied().unwrap_or(u32::MAX) + }) + .collect() + ) + } + async fn has_unclaimed_match_rewards(&self) -> Result { let count = self.db.count_score_by_user_id_and_claimed(self.account.id, false).await .map_err(|e| { @@ -89,3 +168,91 @@ impl super::SocialUser for UserData { } } } + +#[async_trait::async_trait] +impl super::SocialUserC for UserData { + async fn invite_friend(&self, username: String) -> Result, polariton_server::operations::SimpleOpError> { + let target_user = self.db.user_by_some_social_id(username.clone()).await + .map_err(|e| { + log::error!("Failed to retrieve user {} by user {}: {}", username, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user: {}", e), + ) + })?; + if let Some(target_user) = target_user { + let now = chrono::Utc::now(); + self.db.insert_friends([ + // inviter -> invitee + oj_rc_database::schema::friend::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now.timestamp()), + friend_source: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id), + friend_target: oj_rc_database::sea_orm::ActiveValue::Set(target_user.id), + state: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::friend::FriendStatus::InviteSent), + }, + // invitee -> inviter (reciprocal) + oj_rc_database::schema::friend::ActiveModel { + id: oj_rc_database::sea_orm::ActiveValue::NotSet, + creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now.timestamp()), + friend_source: oj_rc_database::sea_orm::ActiveValue::Set(target_user.id), + friend_target: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id), + state: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::friend::FriendStatus::InvitePending), + }, + ]).await.map_err(|e| { + log::error!("Failed to retrieve user {} to send friend request by user {}: {}", username, self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Failed to retrieve user to send friend request: {}", e), + ) + })?; + let my_user_avatar_aux = 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 for user {} (invite_friend): {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Could not retrieve avatar: {}", e), + ) + })? + .ok_or_else(|| { + log::error!("Failed to find avatar for user {}", self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("No avatar for user {}", self.account.id), + ) + })?; + let target_user_avatar_aux = 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 avatar for user {} (invite_friend): {}", target_user.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::DatabaseError as i16, + format!("Could not retrieve avatar: {}", e), + ) + })? + .ok_or_else(|| { + log::error!("Failed to find avatar for user {} by user {}", target_user.id, self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UnexpectedError as i16, + format!("No avatar for user {}", target_user.id), + ) + })?; + let my_avatar_id: Result = my_user_avatar_aux.data.parse(); + let target_avatar_id: Result = target_user_avatar_aux.data.parse(); + Ok(super::FriendInviteReturn { + target_public_id: target_user.public_id, + target_display_name: target_user.display_name, + my_clan_name: None, // TODO clan + target_clan_name: None, // TODO clan + my_avatar_id: my_avatar_id.unwrap_or(0), + target_player: polariton::operation::Typed::HashMap(vec![ + (polariton::operation::Typed::Str("useCustomAvatar".into()), polariton::operation::Typed::Bool(target_avatar_id.is_err())), + (polariton::operation::Typed::Str("avatarId".into()), polariton::operation::Typed::Int(target_avatar_id.unwrap_or_default())), + ].into()), + }) + } else { + log::debug!("Cannot invite non-existent user {}", username); + Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::SocialErrorCode::UserDoesNotExist as i16, + format!("Failed to find user {}", username), + )) + } + } +} diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 95ef9ce..68537a1 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -73,7 +73,7 @@ pub trait UserAuthenticator { } #[async_trait::async_trait] -pub trait User: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser + FactoryUser { +pub trait User: ChatUser + SocialUser + SocialUserC + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser + FactoryUser { async fn unlocked_parts(&self) -> Vec; async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError>; async fn selected_garage(&self) -> (String, u32); @@ -453,11 +453,21 @@ pub enum CurrencyOp { #[async_trait::async_trait] pub trait SocialUser: Send + Sync { + async fn accept_friend(&self, username: String) -> Result; + async fn decline_friend(&self, username: String) -> Result; + async fn cancel_friend(&self, username: String) -> Result; + async fn remove_friend(&self, username: String) -> Result; + async fn list_friends(&self) -> Result, polariton_server::operations::SimpleOpError>; async fn has_unclaimed_match_rewards(&self) -> Result; async fn get_unclaimed_match_rewards(&self) -> Result; async fn claim_match_rewards(&self) -> Result; } +#[async_trait::async_trait] +pub trait SocialUserC: Send + Sync { + async fn invite_friend(&self, username: String) -> Result, polariton_server::operations::SimpleOpError>; +} + pub struct MatchRewards { pub season_experience: i32, pub experience_award_base: i32, @@ -471,6 +481,46 @@ pub struct MatchRewards { pub premium_robits_earned: i32, } +pub struct FriendInviteReturn { + pub target_public_id: String, + pub target_display_name: String, + pub my_clan_name: Option, + pub target_clan_name: Option, + pub my_avatar_id: u32, + pub target_player: polariton::operation::Typed, +} + +pub struct FriendData { + pub public_id: String, + pub display_name: String, + pub clan_name: Option, + pub state: FriendInviteStatus, // FIXME don't directly pass database type + pub avatar_id: u32, +} + +pub enum FriendInviteStatus { + InviteSent, + InvitePending, + Accepted, + Declined, + Cancelled, + Removed, +} + +impl FriendInviteStatus { + #[inline] + pub(super) fn from_db(state: oj_rc_database::schema::friend::FriendStatus) -> Self { + match state { + oj_rc_database::schema::friend::FriendStatus::InviteSent => Self::InviteSent, + oj_rc_database::schema::friend::FriendStatus::InvitePending => Self::InvitePending, + oj_rc_database::schema::friend::FriendStatus::Accepted => Self::Accepted, + oj_rc_database::schema::friend::FriendStatus::Declined => Self::Declined, + oj_rc_database::schema::friend::FriendStatus::Cancelled => Self::Cancelled, + oj_rc_database::schema::friend::FriendStatus::Removed => Self::Removed, + } + } +} + #[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 new file mode 100644 index 0000000..9cec698 --- /dev/null +++ b/rc_database/src/migration/m20260215_000001_create_friend_table.rs @@ -0,0 +1,53 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260215_000001_create_friend_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Friends table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::friend::Entity) + .col( + ColumnDef::new(crate::schema::friend::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::friend::Column::FriendSource).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-friends-friend_source") + .from(crate::schema::friend::Entity, crate::schema::friend::Column::FriendSource) + .to(crate::schema::user::Entity, crate::schema::user::Column::Id), + ) + .col(ColumnDef::new(crate::schema::friend::Column::FriendTarget).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-friends-friend_target") + .from(crate::schema::friend::Entity, crate::schema::friend::Column::FriendTarget) + .to(crate::schema::user::Entity, crate::schema::user::Column::Id), + ) + .col(ColumnDef::new(crate::schema::friend::Column::State).string().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Friends table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::friend::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index b9ac0cf..5016f3e 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -15,6 +15,7 @@ mod m20250918_000001_add_player_variant; mod m20251228_000001_create_score_table; #[cfg(feature = "factory")] mod m20260126_000001_create_factory_vehicle_table; +mod m20260215_000001_create_friend_table; pub struct Migrator; @@ -37,6 +38,7 @@ impl MigratorTrait for Migrator { Box::new(m20251228_000001_create_score_table::Migration), #[cfg(feature = "factory")] Box::new(m20260126_000001_create_factory_vehicle_table::Migration), + Box::new(m20260215_000001_create_friend_table::Migration), ] } } diff --git a/rc_database/src/schema/friend.rs b/rc_database/src/schema/friend.rs new file mode 100644 index 0000000..be48aa8 --- /dev/null +++ b/rc_database/src/schema/friend.rs @@ -0,0 +1,59 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "friends")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub creation_time: i64, // seconds since unix epoch + pub friend_source: i32, + pub friend_target: i32, + pub state: FriendStatus, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::FriendSource", + to = "super::user::Column::Id" + )] + Source, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::FriendTarget", + to = "super::user::Column::Id" + )] + Target, +} + +/*impl Related for Entity { + fn to() -> RelationDef { + Relation::Source.def() + } +}*/ + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Target.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 FriendStatus { + InviteSent, + InvitePending, + Accepted, + Declined, + Cancelled, + Removed, +} + +pub const FINAL_STATUSES: [FriendStatus; 3] = [ + FriendStatus::Declined, + FriendStatus::Cancelled, + FriendStatus::Removed, +]; diff --git a/rc_database/src/schema/mod.rs b/rc_database/src/schema/mod.rs index 4f332df..643a1b0 100644 --- a/rc_database/src/schema/mod.rs +++ b/rc_database/src/schema/mod.rs @@ -12,6 +12,7 @@ pub mod game_event; pub mod multiplayer_game_score; #[cfg(feature = "factory")] pub mod factory; +pub mod friend; pub fn parse_int_csv(s: &str) -> Vec { s.split(',').filter_map(|i_as_s| { diff --git a/rc_database/src/schema/user.rs b/rc_database/src/schema/user.rs index 0c5e012..73cfc00 100644 --- a/rc_database/src/schema/user.rs +++ b/rc_database/src/schema/user.rs @@ -27,6 +27,8 @@ pub enum Relation { Player, #[sea_orm(has_many = "super::factory::vehicle::Entity")] FactoryUploads, + #[sea_orm(has_many = "super::friend::Entity")] + Friends, // this will probably join the wrong column (i.e. in the wrong direction) } impl Related for Entity { @@ -65,4 +67,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Friends.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index 13f87cc..ccc1f52 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -1,5 +1,5 @@ use sea_orm_migration::MigratorTrait; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, sea_query::ExprTrait}; pub struct Database { orm: std::sync::Arc, @@ -39,6 +39,30 @@ impl Database { .await } + pub async fn user_by_public_id(&self, public_id: String) -> Result, sea_orm::DbErr> { + crate::schema::user::Entity::find() + .filter(crate::schema::user::Column::PublicId.eq(public_id)) + .one(self.orm.as_ref()) + .await + } + + pub async fn user_by_some_social_id(&self, public_id: String) -> Result, sea_orm::DbErr> { + let lower_public_id = public_id.to_lowercase(); + crate::schema::user::Entity::find() + .filter(sea_orm::sea_query::Expr::expr( + sea_orm::sea_query::Func::lower(crate::schema::user::Column::DisplayName.into_expr()) + ).eq(&lower_public_id).or( + sea_orm::sea_query::Func::lower(crate::schema::user::Column::PublicId.into_expr()) + .eq(&lower_public_id) + ).or( + sea_orm::sea_query::Func::lower(crate::schema::user::Column::Email.into_expr()) + .eq(&lower_public_id) + ) + ) + .one(self.orm.as_ref()) + .await + } + pub async fn user_by_steam_id(&self, steam_id: u64) -> Result, sea_orm::DbErr> { crate::schema::user::Entity::find() .filter(crate::schema::user::Column::SteamId.eq(Some(steam_id.to_string()))) @@ -89,6 +113,14 @@ impl Database { .await } + pub async fn user_auxs_by_user_ids_and_descriptor(&self, user_ids: impl std::iter::IntoIterator, descriptor: crate::schema::user_aux::Descriptor) -> Result, sea_orm::DbErr> { + crate::schema::user_aux::Entity::find() + .filter(crate::schema::user_aux::Column::UserId.is_in(user_ids)) + .filter(crate::schema::user_aux::Column::Descriptor.eq(descriptor)) + .all(self.orm.as_ref()) + .await + } + pub async fn insert_user_aux(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; Ok(()) @@ -552,6 +584,43 @@ impl Database { .await } + pub async fn friends_by_user_id(&self, user_id: i32, status_not_in: impl IntoIterator) -> Result, sea_orm::DbErr> { + Ok(crate::schema::friend::Entity::find() + .find_also_related(crate::schema::user::Entity) + .filter(crate::schema::friend::Column::FriendSource.eq(user_id)) + .filter(crate::schema::friend::Column::State.is_not_in(status_not_in)) + .order_by_asc(crate::schema::friend::Column::CreationTime) + .all(self.orm.as_ref()) + .await? + .into_iter() + .filter_map(|(friend, user_opt)| user_opt.map(|user| (friend, user))) + .collect()) + } + + pub async fn insert_friends(&self, entities: impl std::iter::IntoIterator) -> Result<(), sea_orm::DbErr> { + crate::schema::friend::Entity::insert_many(entities).exec(self.orm.as_ref()).await?; + Ok(()) + } + + pub async fn update_friends_state(&self, user_id_1: i32, user_id_2: i32, state: crate::schema::friend::FriendStatus) -> Result<(), sea_orm::DbErr> { + crate::schema::friend::Entity::update_many() + .filter( + sea_orm::sea_query::Condition::any() + .add( + sea_orm::sea_query::Expr::expr(crate::schema::friend::Column::FriendSource.eq(user_id_1)) + .and(crate::schema::friend::Column::FriendTarget.eq(user_id_2)) + ).add( + sea_orm::sea_query::Expr::expr(crate::schema::friend::Column::FriendSource.eq(user_id_2)) + .and(crate::schema::friend::Column::FriendTarget.eq(user_id_1)) + ) + ) + .filter(crate::schema::friend::Column::State.is_not_in(crate::schema::friend::FINAL_STATUSES)) + .col_expr(crate::schema::friend::Column::State, sea_orm::sea_query::Expr::value(state)) + .exec(self.orm.as_ref()) + .await?; + Ok(()) + } + pub async fn metrics(&self) -> super::DatabaseMetrics { self.metrics.lock().unwrap().snapshot() } diff --git a/rc_social_room/src/data/custom.rs b/rc_social_room/src/data/custom.rs index 8496108..a3b7aa2 100644 --- a/rc_social_room/src/data/custom.rs +++ b/rc_social_room/src/data/custom.rs @@ -1,21 +1,47 @@ #[derive(Debug, Clone)] pub enum CustomType { - FriendInfo, // TODO actually serialise + FriendInfo(super::friend::FriendInfo), // TODO actually serialise + Unknown, +} + +impl CustomType { + fn custom_ty(&self) -> u8 { + match self { + Self::FriendInfo(_) => 0, + Self::Unknown => 1, + } + } } pub struct CustomTypeSerdes; impl polariton::serdes::CustomSerdes for CustomTypeSerdes { - fn dump(_c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result { - let payload = vec![ // FIXME don't manually serialize + fn dump(c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result { + w.write_all(&[c.custom_ty()])?; + let mut buf = Vec::new(); + let total_written_len = match c { + CustomType::FriendInfo(friend) => { + friend.dump(&mut std::io::Cursor::new(&mut buf))? + }, + CustomType::Unknown => 0, + }; + w.write_all(&(total_written_len as i16).to_be_bytes())?; + w.write_all(&buf)?; + Ok(3 + total_written_len) + /*let payload = vec![ // FIXME don't manually serialize 0u8, // byte custom type 0u8, 5u8, // short custom object size 3u8, 0u8, 0u8, 0u8, 0u8, // content - ]; - w.write(&payload) + ];*/ } - fn parse(_r: &mut dyn std::io::Read) -> std::io::Result { - Ok(CustomType::FriendInfo) + fn parse(r: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 3]; + r.read_exact(&mut buf)?; + // TODO only read up up to size + match buf[0] { + 0 => super::friend::FriendInfo::parse(r).map(CustomType::FriendInfo), + _ => Ok(CustomType::Unknown), + } } } diff --git a/rc_social_room/src/data/friend.rs b/rc_social_room/src/data/friend.rs index 3038725..1b7cc97 100644 --- a/rc_social_room/src/data/friend.rs +++ b/rc_social_room/src/data/friend.rs @@ -16,4 +16,90 @@ impl AvatarInfo { } } -// TODO pub struct FriendInfo {} +#[derive(Debug, Clone)] +pub struct FriendInfo { + pub status: InviteStatus, + pub is_online: bool, + pub public_id: String, + pub display_name: String, + pub clan_name: String, +} + +impl FriendInfo { + pub(super) fn dump(&self, w: &mut dyn std::io::Write) -> std::io::Result { + w.write_all(&[ + self.status.as_u8(), + self.is_online as u8, + ])?; + let mut total = 2; + total += oj_rc_core::data::write_str_for_binreader(&self.public_id, w)?; + total += oj_rc_core::data::write_str_for_binreader(&self.display_name, w)?; + total += oj_rc_core::data::write_str_for_binreader(&self.clan_name, w)?; + Ok(total) + } + + pub(super) fn parse(r: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 2]; + r.read_exact(&mut buf)?; + let status = InviteStatus::from_u8(buf[0]).ok_or_else(|| std::io::Error::other(format!("Invalid invite status {}", buf[0])))?; + let is_online = buf[1] != 0; + let public_id = oj_rc_core::data::read_str_for_binwriter(r)?; + let display_name = oj_rc_core::data::read_str_for_binwriter(r)?; + let clan_name = oj_rc_core::data::read_str_for_binwriter(r)?; + Ok(Self { + status, + is_online, + public_id, + display_name, + clan_name, + }) + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +pub enum InviteStatus { + InviteSent = 0, + InvitePending = 1, + Accepted = 2, + None = 3 +} + +impl InviteStatus { + #[inline] + pub fn from_u8(num: u8) -> Option { + match num { + 0 => Some(Self::InviteSent), + 1 => Some(Self::InvitePending), + 2 => Some(Self::Accepted), + 3 => Some(Self::None), + _ => None, + } + } + + #[inline] + pub fn as_u8(&self) -> u8 { + *self as u8 + } + + pub fn from_core(core: &oj_rc_core::persist::user::FriendInviteStatus) -> Self { + match core { + oj_rc_core::persist::user::FriendInviteStatus::InviteSent => Self::InviteSent, + oj_rc_core::persist::user::FriendInviteStatus::InvitePending => Self::InvitePending, + oj_rc_core::persist::user::FriendInviteStatus::Accepted => Self::Accepted, + oj_rc_core::persist::user::FriendInviteStatus::Declined + | oj_rc_core::persist::user::FriendInviteStatus::Cancelled + | oj_rc_core::persist::user::FriendInviteStatus::Removed => Self::None, + } + } + + #[inline] + pub fn reciprocal(&self) -> Self { + match self { + Self::InviteSent => Self::InvitePending, + Self::InvitePending => Self::InviteSent, + Self::Accepted => Self::Accepted, + Self::None => Self::None, + } + } +} diff --git a/rc_social_room/src/events/friend_invite_accepted.rs b/rc_social_room/src/events/friend_invite_accepted.rs new file mode 100644 index 0000000..98976ce --- /dev/null +++ b/rc_social_room/src/events/friend_invite_accepted.rs @@ -0,0 +1,28 @@ +pub struct FriendInviteAccepted { + pub friend_public_id: String, + pub friend_display_name: String, +} + +impl FriendInviteAccepted { + pub const CODE: u8 = 1; + + 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.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendInviteAccepted { + 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/friend_invite_cancelled.rs b/rc_social_room/src/events/friend_invite_cancelled.rs new file mode 100644 index 0000000..2284bd2 --- /dev/null +++ b/rc_social_room/src/events/friend_invite_cancelled.rs @@ -0,0 +1,28 @@ +pub struct FriendInviteCancelled { + pub friend_public_id: String, + pub friend_display_name: String, +} + +impl FriendInviteCancelled { + pub const CODE: u8 = 5; + + 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.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendInviteCancelled { + 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/friend_invite_declined.rs b/rc_social_room/src/events/friend_invite_declined.rs new file mode 100644 index 0000000..c8d0f1d --- /dev/null +++ b/rc_social_room/src/events/friend_invite_declined.rs @@ -0,0 +1,28 @@ +pub struct FriendInviteDeclined { + pub friend_public_id: String, + pub friend_display_name: String, +} + +impl FriendInviteDeclined { + pub const CODE: u8 = 2; + + 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.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendInviteDeclined { + 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/friend_invite_received.rs b/rc_social_room/src/events/friend_invite_received.rs new file mode 100644 index 0000000..457a5d7 --- /dev/null +++ b/rc_social_room/src/events/friend_invite_received.rs @@ -0,0 +1,42 @@ +pub struct FriendInviteReceived { + pub friend_public_id: String, + pub friend_display_name: String, + pub clan_name: Option, + pub is_online: bool, // when would this ever be false? + pub avatar_id: u32, // direct from database; u32::MAX means it is a custom avatar +} + +impl FriendInviteReceived { + pub const CODE: u8 = 0; + + 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.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + if let Some(clan_name) = &self.clan_name { + params.insert(31, polariton::operation::Typed::Str(clan_name.into())); + } else { + params.insert(31, polariton::operation::Typed::Null); + } + + params.insert(2, polariton::operation::Typed::Bool(self.is_online)); + params.insert(9, polariton::operation::Typed::HashMap(vec![ + (polariton::operation::Typed::Str("useCustomAvatar".into()), polariton::operation::Typed::Bool(self.avatar_id == u32::MAX)), + (polariton::operation::Typed::Str("avatarId".into()), polariton::operation::Typed::Int(self.avatar_id.try_into().unwrap_or_default())), + ].into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendInviteReceived { + 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/friend_removed.rs b/rc_social_room/src/events/friend_removed.rs new file mode 100644 index 0000000..59a511b --- /dev/null +++ b/rc_social_room/src/events/friend_removed.rs @@ -0,0 +1,28 @@ +pub struct FriendRemoved { + pub friend_public_id: String, + pub friend_display_name: String, +} + +impl FriendRemoved { + pub const CODE: u8 = 3; + + 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.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendRemoved { + 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/friend_status.rs b/rc_social_room/src/events/friend_status.rs new file mode 100644 index 0000000..47a154a --- /dev/null +++ b/rc_social_room/src/events/friend_status.rs @@ -0,0 +1,33 @@ +#[derive(Clone)] +pub struct FriendStatus { + pub friend_public_id: String, + pub friend_display_name: String, + pub is_online: bool, + pub invite_status: crate::data::friend::InviteStatus, +} + +impl FriendStatus { + pub const CODE: u8 = 4; + + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(4); + params.insert(1, polariton::operation::Typed::Str(self.friend_public_id.clone().into())); + params.insert(75, polariton::operation::Typed::Str(self.friend_display_name.clone().into())); + params.insert(2, polariton::operation::Typed::Bool(self.is_online)); + params.insert(3, polariton::operation::Typed::Byte(self.invite_status.as_u8())); + params.into() + } +} + +impl polariton_server::events::IntoEvent for FriendStatus { + 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 new file mode 100644 index 0000000..3696761 --- /dev/null +++ b/rc_social_room/src/events/mod.rs @@ -0,0 +1,6 @@ +pub mod friend_invite_received; +pub mod friend_invite_accepted; +pub mod friend_invite_declined; +pub mod friend_invite_cancelled; +pub mod friend_removed; +pub mod friend_status; diff --git a/rc_social_room/src/main.rs b/rc_social_room/src/main.rs index 0168711..3cc6681 100644 --- a/rc_social_room/src/main.rs +++ b/rc_social_room/src/main.rs @@ -3,6 +3,9 @@ mod cli; mod data; mod operations; +mod events; +mod social_services; +pub use social_services::SocialMesh; use oj_polariton_auth::Handshake; use tokio::net; @@ -15,16 +18,30 @@ pub type UserTy = std::sync::Arc, +} + #[tokio::main] async fn main() -> std::io::Result<()> { env_logger::init(); let args = cli::CliArgs::get(); log::debug!("Got cli args {:?}", args); - let cubes = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); - let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &cubes).await.expect("Bad user data")); + let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); + let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); + let social = std::sync::Arc::new(SocialMesh::new()); - let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(), polariton_server::events::EventsHandler::new())); + let init_ctx = InitConfig { + config, + social: social.clone(), + }; + + let server = std::sync::Arc::new(polariton_server::Server::new( + operations::handler(&init_ctx), + polariton_server::events::EventsHandler::new(), + )); let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); @@ -36,11 +53,11 @@ async fn main() -> std::io::Result<()> { if args.once { log::warn!("Handling first connection and then exiting"); let (socket, address) = listener.accept().await?; - process_socket(socket, address, server.clone(), users.clone()).await; + process_socket(socket, address, server.clone(), users.clone(), social.clone()).await; } else { loop { let (socket, address) = listener.accept().await?; - tokio::spawn(process_socket(socket, address, server.clone(), users.clone())); + tokio::spawn(process_socket(socket, address, server.clone(), users.clone(), social.clone())); } } server.join(); @@ -48,7 +65,7 @@ async fn main() -> std::io::Result<()> { Ok(()) } -async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc>, users: std::sync::Arc) { +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc>, users: std::sync::Arc, social: std::sync::Arc) { log::debug!("Accepting connection from address {}", address); let enc = match do_connect_handshake(&mut socket).await { Some(x) => x, @@ -65,9 +82,19 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd let ctx = polariton::packet::SerdesContext::from_boxed(op_ctx, enc); server.handle_async_with_channel_join(socket_r, socket_w, user_state.clone(), ctx, chann_tx, chann_rx).await; log::debug!("Goodbye connection from address {}", address); - ONLINE_USERS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + ONLINE_USERS.store(social.online_count_read().await - 1, std::sync::atomic::Ordering::SeqCst); if let Ok(user_info) = user_state.user() { update_status(user_info.as_ref().as_ref()).await; + if let Ok(friends) = user_info.list_friends().await { + for friend in friends { + social.send_event_to(&friend.public_id, crate::events::friend_status::FriendStatus { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + is_online: false, + invite_status: crate::data::friend::InviteStatus::from_core(&friend.state).reciprocal(), + }).await; + } + } } } diff --git a/rc_social_room/src/operations/friend_accept.rs b/rc_social_room/src/operations/friend_accept.rs new file mode 100644 index 0000000..cb9c83f --- /dev/null +++ b/rc_social_room/src/operations/friend_accept.rs @@ -0,0 +1,36 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 1; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in & out +const IS_ONLINE_PARAM_KEY: u8 = 2; // bool; out + +pub(super) struct FriendRequestAccepter { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendRequestAccepter { + 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(username)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + user_info.accept_friend(username.string.clone()).await?; + self.social.send_event_to(&username.string, crate::events::friend_invite_accepted::FriendInviteAccepted { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + }).await; + params.insert(IS_ONLINE_PARAM_KEY, Typed::Bool(true)); // when would this ever not be true?? + } + Ok(params) + } +} + +pub(super) fn friend_accept_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendRequestAccepter { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/friend_cancel.rs b/rc_social_room/src/operations/friend_cancel.rs new file mode 100644 index 0000000..9e65392 --- /dev/null +++ b/rc_social_room/src/operations/friend_cancel.rs @@ -0,0 +1,34 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 5; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in & out + +pub(super) struct FriendRequestCanceller { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendRequestCanceller { + 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(username)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + user_info.cancel_friend(username.string.clone()).await?; + self.social.send_event_to(&username.string, crate::events::friend_invite_cancelled::FriendInviteCancelled { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + }).await; + } + Ok(params) + } +} + +pub(super) fn friend_cancel_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendRequestCanceller { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/friend_decline.rs b/rc_social_room/src/operations/friend_decline.rs new file mode 100644 index 0000000..da43057 --- /dev/null +++ b/rc_social_room/src/operations/friend_decline.rs @@ -0,0 +1,34 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 2; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in & out + +pub(super) struct FriendRequestDecliner { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendRequestDecliner { + 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(username)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + user_info.decline_friend(username.string.clone()).await?; + self.social.send_event_to(&username.string, crate::events::friend_invite_declined::FriendInviteDeclined { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + }).await; + } + Ok(params) + } +} + +pub(super) fn friend_decline_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendRequestDecliner { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/friend_invite.rs b/rc_social_room/src/operations/friend_invite.rs new file mode 100644 index 0000000..4c5c254 --- /dev/null +++ b/rc_social_room/src/operations/friend_invite.rs @@ -0,0 +1,46 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 0; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in & out +const DISPLAY_NAME_PARAM_KEY: u8 = 75; // str; out +const CLAN_NAME_PARAM_KEY: u8 = 31; // str; out TODO +const USER_DATA_PARAM_KEY: u8 = 9; // hashtable; out + +pub(super) struct FriendRequestMaker { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendRequestMaker { + 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(username)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + let resp = user_info.invite_friend(username.string).await?; + self.social.send_event_to(&resp.target_public_id, crate::events::friend_invite_received::FriendInviteReceived { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + clan_name: resp.my_clan_name, + is_online: true, + avatar_id: resp.my_avatar_id, + }).await; + params.insert(USERNAME_PARAM_KEY, Typed::Str(resp.target_public_id.into())); + params.insert(DISPLAY_NAME_PARAM_KEY, Typed::Str(resp.target_display_name.into())); + if let Some(clan_name) = resp.target_clan_name { + params.insert(CLAN_NAME_PARAM_KEY, Typed::Str(clan_name.into())); + } + params.insert(USER_DATA_PARAM_KEY, resp.target_player); + } + Ok(params) + } +} + +pub(super) fn friend_invite_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendRequestMaker { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/friend_list.rs b/rc_social_room/src/operations/friend_list.rs index 77f12c8..f2912c9 100644 --- a/rc_social_room/src/operations/friend_list.rs +++ b/rc_social_room/src/operations/friend_list.rs @@ -1,12 +1,14 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed, Arr}; use crate::data::friend::*; +const CODE: u8 = 4; + const FRIENDS_PARAM_KEY: u8 = 5; const AVATAR_PARAM_KEY: u8 = 76; -pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, crate::data::custom::CustomType> { +/*pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, crate::data::custom::CustomType> { SimpleFunc::new(|params, _| { let mut params = params.to_dict(); params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr { @@ -22,4 +24,74 @@ pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(Parame })); Ok(params.into()) }) +}*/ + +pub(super) struct FriendsLister { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendsLister { + 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 friends = user_info.list_friends().await?; + let mut friend_pub_ids = friends.iter().map(|friend| friend.public_id.clone()).collect(); + self.social.filter_online_only(&mut friend_pub_ids).await; + let friends_online_pub_ids = friend_pub_ids; + // Typed::Custom(crate::data::custom::CustomType::FriendInfo) + params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::Custom, // custom + items: friends.iter().map(|friend| + Typed::Custom(crate::data::custom::CustomType::FriendInfo(crate::data::friend::FriendInfo { + status: crate::data::friend::InviteStatus::from_core(&friend.state), + is_online: friends_online_pub_ids.contains(&friend.public_id), + public_id: friend.public_id.clone(), + display_name: friend.display_name.clone(), + clan_name: friend.clan_name.clone().unwrap_or_default(), + })) + ).collect() + })); + params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashmap + items: friends.iter() + .map(|friend| + AvatarInfo { + name: friend.public_id.clone(), + use_custom_avatar: friend.avatar_id == u32::MAX, + avatar_id: friend.avatar_id.try_into().unwrap_or_default(), + }.as_transmissible() + ).collect() + })); + tokio::task::spawn(send_online_event_to_friends( + friends.iter() + .filter(|friend| friends_online_pub_ids.contains(&friend.public_id)) + .map(|friend| ( + friend.public_id.clone(), + crate::events::friend_status::FriendStatus { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + is_online: true, + invite_status: crate::data::friend::InviteStatus::from_core(&friend.state).reciprocal(), + } + )) + .collect(), + self.social.clone(), + )); + Ok(params) + } +} + +async fn send_online_event_to_friends(events: Vec<(String, crate::events::friend_status::FriendStatus)>, social: std::sync::Arc) { + for (public_id, event) in events { + social.send_event_to(&public_id, event).await; + } +} + +pub(super) fn friends_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendsLister { + social: init_ctx.social.clone(), + }) } diff --git a/rc_social_room/src/operations/friend_remove.rs b/rc_social_room/src/operations/friend_remove.rs new file mode 100644 index 0000000..9df8d36 --- /dev/null +++ b/rc_social_room/src/operations/friend_remove.rs @@ -0,0 +1,34 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 3; + +const USERNAME_PARAM_KEY: u8 = 1; // str; in & out + +pub(super) struct FriendRequestRemover { + social: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FriendRequestRemover { + 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(username)) = params.remove(&USERNAME_PARAM_KEY) { + let user_info = user.user()?; + user_info.remove_friend(username.string.clone()).await?; + self.social.send_event_to(&username.string, crate::events::friend_removed::FriendRemoved { + friend_public_id: user_info.public_id().to_owned(), + friend_display_name: user_info.display_name().to_owned(), + }).await; + } + Ok(params) + } +} + +pub(super) fn friend_remove_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl { + SimpleOpImpl::new(FriendRequestRemover { + social: init_ctx.social.clone(), + }) +} diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index 2df2601..8ad97ce 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -10,15 +10,20 @@ mod platoon_data; mod calculate_mmr; mod previous_battle_rewards_get; mod previous_battle_rewards_claim; +mod friend_invite; +mod friend_accept; +mod friend_decline; +mod friend_cancel; +mod friend_remove; use polariton_server::operations::OperationsHandler; -pub fn handler() -> OperationsHandler { +pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler { OperationsHandler::::new() .modify(oj_rc_core::polariton::RcOpModifier) - .add(more_auth::MoreLobbyAuth) + .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()) // TODO friend object parsing Token: 0x0200169C RID: 5788 + .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()) @@ -32,7 +37,11 @@ pub fn handler() -> OperationsHandler::default()) // AvatarUpdatedRequest, sent on services_room avatar_set success (just needs to be ack-ed; no params) .add(calculate_mmr::mmr_provider()) .add(polariton_server::operations::Ack::<25, _>::default()) // save social settings, sent on escape menu settings save (should probably be saved someday...) - .add(polariton_server::operations::Ack::<0, _>::default()) // send friend request, can be sent from match leaderboard + .add(friend_invite::friend_invite_provider(init_ctx)) // send friend request, can be sent from match leaderboard .add(previous_battle_rewards_get::get_battle_rewards_provider()) .add(previous_battle_rewards_claim::claim_battle_rewards_provider()) + .add(friend_accept::friend_accept_provider(init_ctx)) + .add(friend_decline::friend_decline_provider(init_ctx)) + .add(friend_cancel::friend_cancel_provider(init_ctx)) + .add(friend_remove::friend_remove_provider(init_ctx)) } diff --git a/rc_social_room/src/operations/more_auth.rs b/rc_social_room/src/operations/more_auth.rs index 54a08bd..5d79942 100644 --- a/rc_social_room/src/operations/more_auth.rs +++ b/rc_social_room/src/operations/more_auth.rs @@ -1,7 +1,15 @@ use polariton::operation::Typed; use polariton_server::operations::{Operation, OperationCode}; -pub struct MoreLobbyAuth; +pub fn more_lobby_auth(init_ctx: &crate::InitConfig) -> MoreLobbyAuth { + MoreLobbyAuth { + social: init_ctx.social.clone(), + } +} + +pub struct MoreLobbyAuth { + social: std::sync::Arc, +} impl MoreLobbyAuth { const AUTH_PAYLOAD_KEY: u8 = 245; @@ -15,6 +23,10 @@ impl Operation for MoreLobbyAuth { let params_dict = params.to_dict(); if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) { if user.update_with_auth(&auth_payload.string).await { + self.social.add_user( + user.user().unwrap().public_id().to_owned(), + user.event_sender().to_owned().downgrade(), + ).await; crate::update_status(user.user().unwrap().as_ref().as_ref()).await; let mut resp_params = std::collections::HashMap::with_capacity(1); resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); diff --git a/rc_social_room/src/social_services.rs b/rc_social_room/src/social_services.rs new file mode 100644 index 0000000..be3fbbf --- /dev/null +++ b/rc_social_room/src/social_services.rs @@ -0,0 +1,59 @@ +/// Primarily keeps track of who is online so events can be sent to them. +pub struct SocialMesh { + users: tokio::sync::RwLock>, +} + +struct UserHandle { + emitter: polariton_server::events::WeakEventEmitter, + is_alive: std::sync::atomic::AtomicBool, +} + +impl SocialMesh { + pub fn new() -> Self { + Self { + users: tokio::sync::RwLock::new(std::collections::HashMap::new()), + } + } + + pub async fn send_event_to(&self, public_id: &str, event: impl polariton_server::events::IntoEvent) -> bool { + let user_lock = self.users.read().await; + if let Some(user_handle) = user_lock.get(public_id) { + let is_success = user_handle.emitter.emit(event); + user_handle.is_alive.swap(is_success, std::sync::atomic::Ordering::SeqCst); + is_success + } else { + false + } + } + + pub async fn add_user( + &self, + public_id: String, + emitter: polariton_server::events::WeakEventEmitter, + ) { + let mut user_lock = self.users.write().await; + Self::cleanup_dead_users(&mut user_lock).await; + user_lock.insert(public_id, UserHandle { + emitter, + is_alive: std::sync::atomic::AtomicBool::new(true), + }); + } + + /// Filter out offline users + pub async fn filter_online_only(&self, public_ids: &mut std::collections::HashSet) { + let mut user_lock = self.users.write().await; + Self::cleanup_dead_users(&mut user_lock).await; + public_ids.retain(|public_id| user_lock.contains_key(public_id)); + } + + async fn cleanup_dead_users(users: &mut std::collections::HashMap) { + users.retain(|_public_id, handle| handle.is_alive.load(std::sync::atomic::Ordering::SeqCst)); + } + + pub async fn online_count_read(&self) -> u64 { + let user_lock = self.users.read().await; + user_lock.iter() + .filter(|(_, handle)| handle.is_alive.load(std::sync::atomic::Ordering::SeqCst)) + .count() as u64 + } +}