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

View File

@@ -1,21 +1,47 @@
#[derive(Debug, Clone)]
pub enum CustomType {
FriendInfo, // TODO actually serialise
FriendInfo(super::friend::FriendInfo), // TODO actually serialise
Unknown,
}
impl CustomType {
fn custom_ty(&self) -> u8 {
match self {
Self::FriendInfo(_) => 0,
Self::Unknown => 1,
}
}
}
pub struct CustomTypeSerdes;
impl polariton::serdes::CustomSerdes<CustomType> for CustomTypeSerdes {
fn dump(_c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result<usize> {
let payload = vec![ // FIXME don't manually serialize
fn dump(c: &CustomType, w: &mut dyn std::io::Write) -> std::io::Result<usize> {
w.write_all(&[c.custom_ty()])?;
let mut buf = Vec::new();
let total_written_len = match c {
CustomType::FriendInfo(friend) => {
friend.dump(&mut std::io::Cursor::new(&mut buf))?
},
CustomType::Unknown => 0,
};
w.write_all(&(total_written_len as i16).to_be_bytes())?;
w.write_all(&buf)?;
Ok(3 + total_written_len)
/*let payload = vec![ // FIXME don't manually serialize
0u8, // byte custom type
0u8, 5u8, // short custom object size
3u8, 0u8, 0u8, 0u8, 0u8, // content
];
w.write(&payload)
];*/
}
fn parse(_r: &mut dyn std::io::Read) -> std::io::Result<CustomType> {
Ok(CustomType::FriendInfo)
fn parse(r: &mut dyn std::io::Read) -> std::io::Result<CustomType> {
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),
}
}
}

View File

@@ -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,
}
}
}

View 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(),
}
}
}

View 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(),
}
}
}

View 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(),
}
}
}

View 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(),
}
}
}

View 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(),
}
}
}

View 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(),
}
}
}

View 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;

View File

@@ -3,6 +3,9 @@ mod cli;
mod data;
mod operations;
mod events;
mod social_services;
pub use social_services::SocialMesh;
use oj_polariton_auth::Handshake;
use tokio::net;
@@ -15,16 +18,30 @@ pub type UserTy = std::sync::Arc<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 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]
async fn main() -> std::io::Result<()> {
env_logger::init();
let args = cli::CliArgs::get();
log::debug!("Got cli args {:?}", args);
let cubes = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data");
let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &cubes).await.expect("Bad user data"));
let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data");
let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data"));
let social = std::sync::Arc::new(SocialMesh::new());
let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(), polariton_server::events::EventsHandler::new()));
let init_ctx = InitConfig {
config,
social: social.clone(),
};
let server = std::sync::Arc::new(polariton_server::Server::new(
operations::handler(&init_ctx),
polariton_server::events::EventsHandler::new(),
));
let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
@@ -36,11 +53,11 @@ async fn main() -> std::io::Result<()> {
if args.once {
log::warn!("Handling first connection and then exiting");
let (socket, address) = listener.accept().await?;
process_socket(socket, address, server.clone(), users.clone()).await;
process_socket(socket, address, server.clone(), users.clone(), social.clone()).await;
} else {
loop {
let (socket, address) = listener.accept().await?;
tokio::spawn(process_socket(socket, address, server.clone(), users.clone()));
tokio::spawn(process_socket(socket, address, server.clone(), users.clone(), social.clone()));
}
}
server.join();
@@ -48,7 +65,7 @@ async fn main() -> std::io::Result<()> {
Ok(())
}
async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc<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);
let enc = match do_connect_handshake(&mut socket).await {
Some(x) => x,
@@ -65,9 +82,19 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd
let ctx = polariton::packet::SerdesContext::from_boxed(op_ctx, enc);
server.handle_async_with_channel_join(socket_r, socket_w, user_state.clone(), ctx, chann_tx, chann_rx).await;
log::debug!("Goodbye connection from address {}", address);
ONLINE_USERS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
ONLINE_USERS.store(social.online_count_read().await - 1, std::sync::atomic::Ordering::SeqCst);
if let Ok(user_info) = user_state.user() {
update_status(user_info.as_ref().as_ref()).await;
if let Ok(friends) = user_info.list_friends().await {
for friend in friends {
social.send_event_to(&friend.public_id, crate::events::friend_status::FriendStatus {
friend_public_id: user_info.public_id().to_owned(),
friend_display_name: user_info.display_name().to_owned(),
is_online: false,
invite_status: crate::data::friend::InviteStatus::from_core(&friend.state).reciprocal(),
}).await;
}
}
}
}

View 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(),
})
}

View 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(),
})
}

View 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(),
})
}

View 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(),
})
}

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(),
})
}

View 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(),
})
}

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))
}

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));

View 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
}
}