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:
NG (Graham) authored and NGnius committed 2026-02-16 04:28:14 +00:00
1 parent 872f224af6
commit 9502522ac4
30 files changed
+1108 -35

No files matched your search

@@ -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(),
})
}
@@ -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(),
})
}
@@ -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(),
})
}
@@ -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(),
})
}
+74 -2
View File
@@ -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::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, _| {
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<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(),
})
}
@@ -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(),
})
}
+13 -4
View File
@@ -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<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()
.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<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(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))
}
+13 -1
View File
@@ -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<crate::SocialMesh>,
}
impl MoreLobbyAuth {
const AUTH_PAYLOAD_KEY: u8 = 245;
@@ -15,6 +23,10 @@ impl <C: Send + 'static> Operation<C> 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));