mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Friends functionality (#90)
### Description Implements and closes #86 ### Game Robocraft ### Please confirm - [x] I am the legal owner or represent the owner of all work submitted - [x] I consent to my submission being added to this FOSS project - [ ] This PR used LLMs to generate some or all of the code changes Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/90 Co-authored-by: NG (Graham) <ngniusness@gmail.com> Co-committed-by: NG (Graham) <ngniusness@gmail.com>
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -3013,18 +3013,14 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polariton"
|
name = "polariton"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6dcc388d509743a0419f5a5197a8c79e0f3fd484d5bd0b60e6f361887a8aa301"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polariton_server"
|
name = "polariton_server"
|
||||||
version = "0.4.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51ea2a26537f7434f154e34d7c522ab10552442241e608e919d7cfc413ac3abc"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ libfj = { version = "0.10" }
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
clap = { version = "4.5", features = [ "derive" ] }
|
clap = { version = "4.5", features = [ "derive" ] }
|
||||||
#polariton = { version = "0.5", path = "../polariton", features = [ "tokio-async" ] }
|
polariton = { version = "0.6", path = "../polariton", features = [ "tokio-async" ] }
|
||||||
#polariton_server = { version = "0.5", path = "../polariton/server", features = [ "tokio-async" ] }
|
polariton_server = { version = "0.6", path = "../polariton/server", features = [ "tokio-async" ] }
|
||||||
polariton = { version = "0.4", features = [ "tokio-async" ] }
|
#polariton = { version = "0.6", features = [ "tokio-async" ] }
|
||||||
polariton_server = { version = "0.4", features = [ "tokio-async" ] }
|
#polariton_server = { version = "0.6", features = [ "tokio-async" ] }
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mod inventory;
|
|||||||
pub use inventory::{UnlockedParts, UnlockOverride};
|
pub use inventory::{UnlockedParts, UnlockOverride};
|
||||||
|
|
||||||
mod traits;
|
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 mod intercom;
|
||||||
pub use intercom::generate_token as generate_intercom_token;
|
pub use intercom::generate_token as generate_intercom_token;
|
||||||
|
|||||||
@@ -1,7 +1,86 @@
|
|||||||
use super::account_json::UserData;
|
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<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
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]
|
#[async_trait::async_trait]
|
||||||
impl super::SocialUser for UserData {
|
impl super::SocialUser for UserData {
|
||||||
|
async fn accept_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Accepted).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decline_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Declined).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Cancelled).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
self.apply_friend_state_to(username, oj_rc_database::schema::friend::FriendStatus::Removed).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_friends(&self) -> Result<Vec<super::FriendData>, 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<i32> = 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<i32, u32> = 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<bool, polariton_server::operations::SimpleOpError> {
|
async fn has_unclaimed_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
let count = self.db.count_score_by_user_id_and_claimed(self.account.id, false).await
|
let count = self.db.count_score_by_user_id_and_claimed(self.account.id, false).await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -89,3 +168,91 @@ impl super::SocialUser for UserData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl <C> super::SocialUserC<C> for UserData {
|
||||||
|
async fn invite_friend(&self, username: String) -> Result<super::FriendInviteReturn<C>, 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<u32, _> = my_user_avatar_aux.data.parse();
|
||||||
|
let target_avatar_id: Result<i32, _> = 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),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub trait UserAuthenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser + FactoryUser {
|
pub trait User<C>: ChatUser + SocialUser + SocialUserC<C> + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser + FactoryUser {
|
||||||
async fn unlocked_parts(&self) -> Vec<u32>;
|
async fn unlocked_parts(&self) -> Vec<u32>;
|
||||||
async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError>;
|
async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError>;
|
||||||
async fn selected_garage(&self) -> (String, u32);
|
async fn selected_garage(&self) -> (String, u32);
|
||||||
@@ -453,11 +453,21 @@ pub enum CurrencyOp {
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait SocialUser: Send + Sync {
|
pub trait SocialUser: Send + Sync {
|
||||||
|
async fn accept_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
|
async fn decline_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
|
async fn cancel_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
|
async fn remove_friend(&self, username: String) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
|
async fn list_friends(&self) -> Result<Vec<FriendData>, polariton_server::operations::SimpleOpError>;
|
||||||
async fn has_unclaimed_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
async fn has_unclaimed_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
async fn get_unclaimed_match_rewards(&self) -> Result<MatchRewards, polariton_server::operations::SimpleOpError>;
|
async fn get_unclaimed_match_rewards(&self) -> Result<MatchRewards, polariton_server::operations::SimpleOpError>;
|
||||||
async fn claim_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
async fn claim_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait SocialUserC<C>: Send + Sync {
|
||||||
|
async fn invite_friend(&self, username: String) -> Result<FriendInviteReturn<C>, polariton_server::operations::SimpleOpError>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MatchRewards {
|
pub struct MatchRewards {
|
||||||
pub season_experience: i32,
|
pub season_experience: i32,
|
||||||
pub experience_award_base: i32,
|
pub experience_award_base: i32,
|
||||||
@@ -471,6 +481,46 @@ pub struct MatchRewards {
|
|||||||
pub premium_robits_earned: i32,
|
pub premium_robits_earned: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FriendInviteReturn<C> {
|
||||||
|
pub target_public_id: String,
|
||||||
|
pub target_display_name: String,
|
||||||
|
pub my_clan_name: Option<String>,
|
||||||
|
pub target_clan_name: Option<String>,
|
||||||
|
pub my_avatar_id: u32,
|
||||||
|
pub target_player: polariton::operation::Typed<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FriendData {
|
||||||
|
pub public_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub clan_name: Option<String>,
|
||||||
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait SingleplayerUser: Send + Sync {
|
pub trait SingleplayerUser: Send + Sync {
|
||||||
// regular singleplayer and campaign mode
|
// regular singleplayer and campaign mode
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ mod m20250918_000001_add_player_variant;
|
|||||||
mod m20251228_000001_create_score_table;
|
mod m20251228_000001_create_score_table;
|
||||||
#[cfg(feature = "factory")]
|
#[cfg(feature = "factory")]
|
||||||
mod m20260126_000001_create_factory_vehicle_table;
|
mod m20260126_000001_create_factory_vehicle_table;
|
||||||
|
mod m20260215_000001_create_friend_table;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20251228_000001_create_score_table::Migration),
|
Box::new(m20251228_000001_create_score_table::Migration),
|
||||||
#[cfg(feature = "factory")]
|
#[cfg(feature = "factory")]
|
||||||
Box::new(m20260126_000001_create_factory_vehicle_table::Migration),
|
Box::new(m20260126_000001_create_factory_vehicle_table::Migration),
|
||||||
|
Box::new(m20260215_000001_create_friend_table::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
rc_database/src/schema/friend.rs
Normal file
59
rc_database/src/schema/friend.rs
Normal file
@@ -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<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Source.def()
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> 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,
|
||||||
|
];
|
||||||
@@ -12,6 +12,7 @@ pub mod game_event;
|
|||||||
pub mod multiplayer_game_score;
|
pub mod multiplayer_game_score;
|
||||||
#[cfg(feature = "factory")]
|
#[cfg(feature = "factory")]
|
||||||
pub mod factory;
|
pub mod factory;
|
||||||
|
pub mod friend;
|
||||||
|
|
||||||
pub fn parse_int_csv(s: &str) -> Vec<u32> {
|
pub fn parse_int_csv(s: &str) -> Vec<u32> {
|
||||||
s.split(',').filter_map(|i_as_s| {
|
s.split(',').filter_map(|i_as_s| {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub enum Relation {
|
|||||||
Player,
|
Player,
|
||||||
#[sea_orm(has_many = "super::factory::vehicle::Entity")]
|
#[sea_orm(has_many = "super::factory::vehicle::Entity")]
|
||||||
FactoryUploads,
|
FactoryUploads,
|
||||||
|
#[sea_orm(has_many = "super::friend::Entity")]
|
||||||
|
Friends, // this will probably join the wrong column (i.e. in the wrong direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::permissions::Entity> for Entity {
|
impl Related<super::permissions::Entity> for Entity {
|
||||||
@@ -65,4 +67,10 @@ impl Related<super::factory::vehicle::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::friend::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Friends.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use sea_orm_migration::MigratorTrait;
|
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 {
|
pub struct Database {
|
||||||
orm: std::sync::Arc<sea_orm::DatabaseConnection>,
|
orm: std::sync::Arc<sea_orm::DatabaseConnection>,
|
||||||
@@ -39,6 +39,30 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn user_by_public_id(&self, public_id: String) -> Result<Option<crate::schema::user::Model>, 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<Option<crate::schema::user::Model>, 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<Option<crate::schema::user::Model>, sea_orm::DbErr> {
|
pub async fn user_by_steam_id(&self, steam_id: u64) -> Result<Option<crate::schema::user::Model>, sea_orm::DbErr> {
|
||||||
crate::schema::user::Entity::find()
|
crate::schema::user::Entity::find()
|
||||||
.filter(crate::schema::user::Column::SteamId.eq(Some(steam_id.to_string())))
|
.filter(crate::schema::user::Column::SteamId.eq(Some(steam_id.to_string())))
|
||||||
@@ -89,6 +113,14 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn user_auxs_by_user_ids_and_descriptor(&self, user_ids: impl std::iter::IntoIterator<Item=i32>, descriptor: crate::schema::user_aux::Descriptor) -> Result<Vec<crate::schema::user_aux::Model>, 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<crate::schema::user_aux::ActiveModel>) -> Result<(), sea_orm::DbErr> {
|
pub async fn insert_user_aux(&self, entities: Vec<crate::schema::user_aux::ActiveModel>) -> Result<(), sea_orm::DbErr> {
|
||||||
crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?;
|
crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -552,6 +584,43 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn friends_by_user_id(&self, user_id: i32, status_not_in: impl IntoIterator<Item=crate::schema::friend::FriendStatus>) -> Result<Vec<(crate::schema::friend::Model, crate::schema::user::Model)>, 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<Item=crate::schema::friend::ActiveModel>) -> 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 {
|
pub async fn metrics(&self) -> super::DatabaseMetrics {
|
||||||
self.metrics.lock().unwrap().snapshot()
|
self.metrics.lock().unwrap().snapshot()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CustomType {
|
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;
|
pub struct CustomTypeSerdes;
|
||||||
|
|
||||||
impl polariton::serdes::CustomSerdes<CustomType> for CustomTypeSerdes {
|
impl polariton::serdes::CustomSerdes<CustomType> for CustomTypeSerdes {
|
||||||
fn dump(_c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result<usize> {
|
fn dump(c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result<usize> {
|
||||||
let payload = vec![ // FIXME don't manually serialize
|
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, // byte custom type
|
||||||
0u8, 5u8, // short custom object size
|
0u8, 5u8, // short custom object size
|
||||||
3u8, 0u8, 0u8, 0u8, 0u8, // content
|
3u8, 0u8, 0u8, 0u8, 0u8, // content
|
||||||
];
|
];*/
|
||||||
w.write(&payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(_r: &mut dyn std::io::Read) -> std::io::Result<CustomType> {
|
fn parse(r: &mut dyn std::io::Read) -> std::io::Result<CustomType> {
|
||||||
Ok(CustomType::FriendInfo)
|
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<usize> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
28
rc_social_room/src/events/friend_invite_accepted.rs
Normal file
28
rc_social_room/src/events/friend_invite_accepted.rs
Normal file
@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendInviteAccepted {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
rc_social_room/src/events/friend_invite_cancelled.rs
Normal file
28
rc_social_room/src/events/friend_invite_cancelled.rs
Normal file
@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendInviteCancelled {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
rc_social_room/src/events/friend_invite_declined.rs
Normal file
28
rc_social_room/src/events/friend_invite_declined.rs
Normal file
@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendInviteDeclined {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
rc_social_room/src/events/friend_invite_received.rs
Normal file
42
rc_social_room/src/events/friend_invite_received.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
pub struct FriendInviteReceived {
|
||||||
|
pub friend_public_id: String,
|
||||||
|
pub friend_display_name: String,
|
||||||
|
pub clan_name: Option<String>,
|
||||||
|
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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendInviteReceived {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
rc_social_room/src/events/friend_removed.rs
Normal file
28
rc_social_room/src/events/friend_removed.rs
Normal file
@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendRemoved {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
rc_social_room/src/events/friend_status.rs
Normal file
33
rc_social_room/src/events/friend_status.rs
Normal file
@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
|
||||||
|
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 <C: Send + 'static> polariton_server::events::IntoEvent<C> for FriendStatus {
|
||||||
|
const CHANNEL: u8 = 0;
|
||||||
|
const ENCRYPT: bool = true;
|
||||||
|
const RELIABLE: bool = true;
|
||||||
|
|
||||||
|
fn into_event(self) -> polariton::operation::Event<C> {
|
||||||
|
polariton::operation::Event {
|
||||||
|
code: Self::CODE,
|
||||||
|
params: self.as_event_params(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
rc_social_room/src/events/mod.rs
Normal file
6
rc_social_room/src/events/mod.rs
Normal file
@@ -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;
|
||||||
@@ -3,6 +3,9 @@ mod cli;
|
|||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
mod operations;
|
mod operations;
|
||||||
|
mod events;
|
||||||
|
mod social_services;
|
||||||
|
pub use social_services::SocialMesh;
|
||||||
|
|
||||||
use oj_polariton_auth::Handshake;
|
use oj_polariton_auth::Handshake;
|
||||||
use tokio::net;
|
use tokio::net;
|
||||||
@@ -15,16 +18,30 @@ pub type UserTy = std::sync::Arc<oj_rc_core::UserState<crate::data::custom::Cust
|
|||||||
pub static START_TIMESTAMP_S: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
|
pub static START_TIMESTAMP_S: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
|
||||||
pub static ONLINE_USERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
pub static ONLINE_USERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||||
|
|
||||||
|
pub struct InitConfig {
|
||||||
|
pub config: oj_rc_core::persist::config::ConfigImpl,
|
||||||
|
pub social: std::sync::Arc<SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let args = cli::CliArgs::get();
|
let args = cli::CliArgs::get();
|
||||||
log::debug!("Got cli args {:?}", args);
|
log::debug!("Got cli args {:?}", args);
|
||||||
|
|
||||||
let cubes = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config 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, &cubes).await.expect("Bad user 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");
|
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 {
|
if args.once {
|
||||||
log::warn!("Handling first connection and then exiting");
|
log::warn!("Handling first connection and then exiting");
|
||||||
let (socket, address) = listener.accept().await?;
|
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 {
|
} else {
|
||||||
loop {
|
loop {
|
||||||
let (socket, address) = listener.accept().await?;
|
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();
|
server.join();
|
||||||
@@ -48,7 +65,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc<polariton_server::Server<crate::UserTy, crate::data::custom::CustomType>>, users: std::sync::Arc<oj_rc_core::UserImpl>) {
|
async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc<polariton_server::Server<crate::UserTy, crate::data::custom::CustomType>>, users: std::sync::Arc<oj_rc_core::UserImpl>, social: std::sync::Arc<SocialMesh>) {
|
||||||
log::debug!("Accepting connection from address {}", address);
|
log::debug!("Accepting connection from address {}", address);
|
||||||
let enc = match do_connect_handshake(&mut socket).await {
|
let enc = match do_connect_handshake(&mut socket).await {
|
||||||
Some(x) => x,
|
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);
|
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;
|
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);
|
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() {
|
if let Ok(user_info) = user_state.user() {
|
||||||
update_status(user_info.as_ref().as_ref()).await;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
rc_social_room/src/operations/friend_accept.rs
Normal file
36
rc_social_room/src/operations/friend_accept.rs
Normal file
@@ -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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendRequestAccepter {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::data::custom::CustomType, crate::UserTy, FriendRequestAccepter> {
|
||||||
|
SimpleOpImpl::new(FriendRequestAccepter {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
34
rc_social_room/src/operations/friend_cancel.rs
Normal file
34
rc_social_room/src/operations/friend_cancel.rs
Normal file
@@ -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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendRequestCanceller {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::data::custom::CustomType, crate::UserTy, FriendRequestCanceller> {
|
||||||
|
SimpleOpImpl::new(FriendRequestCanceller {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
34
rc_social_room/src/operations/friend_decline.rs
Normal file
34
rc_social_room/src/operations/friend_decline.rs
Normal file
@@ -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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendRequestDecliner {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::data::custom::CustomType, crate::UserTy, FriendRequestDecliner> {
|
||||||
|
SimpleOpImpl::new(FriendRequestDecliner {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
46
rc_social_room/src/operations/friend_invite.rs
Normal file
46
rc_social_room/src/operations/friend_invite.rs
Normal file
@@ -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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendRequestMaker {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::data::custom::CustomType, crate::UserTy, FriendRequestMaker> {
|
||||||
|
SimpleOpImpl::new(FriendRequestMaker {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
use polariton_server::operations::SimpleFunc;
|
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
|
||||||
use polariton::operation::{ParameterTable, Typed, Arr};
|
use polariton::operation::{ParameterTable, Typed, Arr};
|
||||||
|
|
||||||
use crate::data::friend::*;
|
use crate::data::friend::*;
|
||||||
|
|
||||||
|
const CODE: u8 = 4;
|
||||||
|
|
||||||
const FRIENDS_PARAM_KEY: u8 = 5;
|
const FRIENDS_PARAM_KEY: u8 = 5;
|
||||||
const AVATAR_PARAM_KEY: u8 = 76;
|
const AVATAR_PARAM_KEY: u8 = 76;
|
||||||
|
|
||||||
pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable<crate::data::custom::CustomType>, &crate::UserTy) -> Result<ParameterTable<crate::data::custom::CustomType>, i16>) + Sync + Sync, crate::data::custom::CustomType> {
|
/*pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable<crate::data::custom::CustomType>, &crate::UserTy) -> Result<ParameterTable<crate::data::custom::CustomType>, i16>) + Sync + Sync, crate::data::custom::CustomType> {
|
||||||
SimpleFunc::new(|params, _| {
|
SimpleFunc::new(|params, _| {
|
||||||
let mut params = params.to_dict();
|
let mut params = params.to_dict();
|
||||||
params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr {
|
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())
|
Ok(params.into())
|
||||||
})
|
})
|
||||||
|
}*/
|
||||||
|
|
||||||
|
pub(super) struct FriendsLister {
|
||||||
|
social: std::sync::Arc<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendsLister {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::SocialMesh>) {
|
||||||
|
for (public_id, event) in events {
|
||||||
|
social.send_event_to(&public_id, event).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn friends_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl<crate::data::custom::CustomType, crate::UserTy, FriendsLister> {
|
||||||
|
SimpleOpImpl::new(FriendsLister {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
34
rc_social_room/src/operations/friend_remove.rs
Normal file
34
rc_social_room/src/operations/friend_remove.rs
Normal file
@@ -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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SimpleOperation<crate::data::custom::CustomType> for FriendRequestRemover {
|
||||||
|
type User = crate::UserTy;
|
||||||
|
const CODE: u8 = CODE;
|
||||||
|
|
||||||
|
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, 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<crate::data::custom::CustomType, crate::UserTy, FriendRequestRemover> {
|
||||||
|
SimpleOpImpl::new(FriendRequestRemover {
|
||||||
|
social: init_ctx.social.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,15 +10,20 @@ mod platoon_data;
|
|||||||
mod calculate_mmr;
|
mod calculate_mmr;
|
||||||
mod previous_battle_rewards_get;
|
mod previous_battle_rewards_get;
|
||||||
mod previous_battle_rewards_claim;
|
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;
|
use polariton_server::operations::OperationsHandler;
|
||||||
|
|
||||||
pub fn handler() -> OperationsHandler<crate::UserTy, crate::data::custom::CustomType> {
|
pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy, crate::data::custom::CustomType> {
|
||||||
OperationsHandler::<crate::UserTy, crate::data::custom::CustomType>::new()
|
OperationsHandler::<crate::UserTy, crate::data::custom::CustomType>::new()
|
||||||
.modify(oj_rc_core::polariton::RcOpModifier)
|
.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(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(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(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(clan_invite::clan_invites_provider())
|
||||||
@@ -32,7 +37,11 @@ pub fn handler() -> OperationsHandler<crate::UserTy, crate::data::custom::Custom
|
|||||||
.add(polariton_server::operations::Ack::<6, _>::default()) // AvatarUpdatedRequest, sent on services_room avatar_set success (just needs to be ack-ed; no params)
|
.add(polariton_server::operations::Ack::<6, _>::default()) // AvatarUpdatedRequest, sent on services_room avatar_set success (just needs to be ack-ed; no params)
|
||||||
.add(calculate_mmr::mmr_provider())
|
.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::<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_get::get_battle_rewards_provider())
|
||||||
.add(previous_battle_rewards_claim::claim_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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
use polariton::operation::Typed;
|
use polariton::operation::Typed;
|
||||||
use polariton_server::operations::{Operation, OperationCode};
|
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<crate::SocialMesh>,
|
||||||
|
}
|
||||||
|
|
||||||
impl MoreLobbyAuth {
|
impl MoreLobbyAuth {
|
||||||
const AUTH_PAYLOAD_KEY: u8 = 245;
|
const AUTH_PAYLOAD_KEY: u8 = 245;
|
||||||
@@ -15,6 +23,10 @@ impl <C: Send + 'static> Operation<C> for MoreLobbyAuth {
|
|||||||
let params_dict = params.to_dict();
|
let params_dict = params.to_dict();
|
||||||
if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) {
|
if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) {
|
||||||
if user.update_with_auth(&auth_payload.string).await {
|
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;
|
crate::update_status(user.user().unwrap().as_ref().as_ref()).await;
|
||||||
let mut resp_params = std::collections::HashMap::with_capacity(1);
|
let mut resp_params = std::collections::HashMap::with_capacity(1);
|
||||||
resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0));
|
resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0));
|
||||||
|
|||||||
59
rc_social_room/src/social_services.rs
Normal file
59
rc_social_room/src/social_services.rs
Normal file
@@ -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<std::collections::HashMap<String, UserHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserHandle {
|
||||||
|
emitter: polariton_server::events::WeakEventEmitter<crate::data::custom::CustomType>,
|
||||||
|
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<crate::data::custom::CustomType>) -> 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<crate::data::custom::CustomType>,
|
||||||
|
) {
|
||||||
|
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<String>) {
|
||||||
|
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<String, UserHandle>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user