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:
@@ -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;
|
||||
|
||||
@@ -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<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]
|
||||
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> {
|
||||
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 <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]
|
||||
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 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<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 get_unclaimed_match_rewards(&self) -> Result<MatchRewards, 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 season_experience: i32,
|
||||
pub experience_award_base: i32,
|
||||
@@ -471,6 +481,46 @@ pub struct MatchRewards {
|
||||
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]
|
||||
pub trait SingleplayerUser: Send + Sync {
|
||||
// regular singleplayer and campaign mode
|
||||
|
||||
Reference in New Issue
Block a user