From 792a6362e36ace134bcd8a8b0a13009359ecfee8 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 14 Apr 2025 21:17:41 -0400 Subject: [PATCH] Implement basic chat functionality --- Cargo.lock | 3 + assets/robocraft/chat.json | 18 ++ assets/robocraft/config.json | 9 +- rc_chat_room/Cargo.toml | 3 + rc_chat_room/src/data/channel.rs | 19 +- rc_chat_room/src/events/chat_message.rs | 47 +++++ rc_chat_room/src/events/mod.rs | 1 + rc_chat_room/src/main.rs | 9 +- rc_chat_room/src/op_handler.rs | 52 +++++ .../src/operations/all_joined_channels.rs | 21 +- rc_chat_room/src/operations/join_channel.rs | 41 ++++ rc_chat_room/src/operations/mod.rs | 20 +- rc_chat_room/src/operations/more_auth.rs | 70 +++++-- .../src/operations/public_channels.rs | 20 ++ rc_chat_room/src/operations/send_message.rs | 51 +++++ rc_chat_room/src/operations/user_online.rs | 36 ++++ .../src/persist/chat_user/chat_json.rs | 111 +++++++++++ rc_chat_room/src/persist/chat_user/mod.rs | 8 + rc_chat_room/src/persist/chat_user/traits.rs | 8 + rc_chat_room/src/persist/config/chat.rs | 43 +++++ rc_chat_room/src/persist/config/mod.rs | 4 + rc_chat_room/src/persist/mod.rs | 2 + rc_chat_room/src/state/chat/chat.rs | 180 ++++++++++++++++++ rc_chat_room/src/state/chat/config.rs | 144 ++++++++++++++ rc_chat_room/src/state/chat/mod.rs | 13 ++ rc_chat_room/src/state/chat/room.rs | 92 +++++++++ rc_chat_room/src/state/chat/user.rs | 43 +++++ rc_chat_room/src/state/mod.rs | 2 + rc_core/src/data/error_codes.rs | 24 +++ rc_core/src/persist/chat.rs | 15 ++ rc_core/src/persist/config/cubes_json.rs | 10 +- rc_core/src/persist/config/traits.rs | 1 + rc_core/src/persist/mod.rs | 3 + rc_core/src/persist/user/account_json.rs | 9 +- rc_core/src/persist/user/traits.rs | 3 +- rc_core/src/state.rs | 11 +- utils/cube_gen.py | 7 + 37 files changed, 1119 insertions(+), 34 deletions(-) create mode 100644 assets/robocraft/chat.json create mode 100644 rc_chat_room/src/events/chat_message.rs create mode 100644 rc_chat_room/src/events/mod.rs create mode 100644 rc_chat_room/src/op_handler.rs create mode 100644 rc_chat_room/src/operations/join_channel.rs create mode 100644 rc_chat_room/src/operations/public_channels.rs create mode 100644 rc_chat_room/src/operations/send_message.rs create mode 100644 rc_chat_room/src/operations/user_online.rs create mode 100644 rc_chat_room/src/persist/chat_user/chat_json.rs create mode 100644 rc_chat_room/src/persist/chat_user/mod.rs create mode 100644 rc_chat_room/src/persist/chat_user/traits.rs create mode 100644 rc_chat_room/src/persist/config/chat.rs create mode 100644 rc_chat_room/src/persist/config/mod.rs create mode 100644 rc_chat_room/src/persist/mod.rs create mode 100644 rc_chat_room/src/state/chat/chat.rs create mode 100644 rc_chat_room/src/state/chat/config.rs create mode 100644 rc_chat_room/src/state/chat/mod.rs create mode 100644 rc_chat_room/src/state/chat/room.rs create mode 100644 rc_chat_room/src/state/chat/user.rs create mode 100644 rc_chat_room/src/state/mod.rs create mode 100644 rc_core/src/persist/chat.rs diff --git a/Cargo.lock b/Cargo.lock index 12622ae..292bbda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1834,6 +1834,9 @@ dependencies = [ "polariton_auth", "polariton_server", "rc_core", + "regex", + "serde", + "serde_json", "tokio", ] diff --git a/assets/robocraft/chat.json b/assets/robocraft/chat.json new file mode 100644 index 0000000..5bf791d --- /dev/null +++ b/assets/robocraft/chat.json @@ -0,0 +1,18 @@ +{ + "commands": [ + { + "regex": "\\?online", + "op": { + "type": "BuiltIn", + "built_in": "OnlineUsers" + } + }, + { + "regex": "\\?users", + "op": { + "type": "BuiltIn", + "built_in": "TotalUsers" + } + } + ] +} diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index eb31d70..a61c2f8 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14416,6 +14416,13 @@ ] } }, + "chat": { + "public_channels": [ + "main", + "sys", + "jam_club" + ] + }, "settings": { "banners": [ { @@ -14476,4 +14483,4 @@ } ] } -} \ No newline at end of file +} diff --git a/rc_chat_room/Cargo.toml b/rc_chat_room/Cargo.toml index baa8114..41056e1 100644 --- a/rc_chat_room/Cargo.toml +++ b/rc_chat_room/Cargo.toml @@ -16,3 +16,6 @@ polariton.workspace = true polariton_auth = { version = "*", path = "../polariton_auth" } polariton_server.workspace = true rc_core = { version = "*", path = "../rc_core" } +serde.workspace = true +serde_json.workspace = true +regex = "1" diff --git a/rc_chat_room/src/data/channel.rs b/rc_chat_room/src/data/channel.rs index ba7a3d1..5fc556a 100644 --- a/rc_chat_room/src/data/channel.rs +++ b/rc_chat_room/src/data/channel.rs @@ -44,7 +44,7 @@ impl ChatChannelMember { #[allow(dead_code)] #[repr(u8)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] pub enum ChatChannelType { None = 0, Public = 1, @@ -57,6 +57,23 @@ pub enum ChatChannelType { CustomGame = 8, } +impl ChatChannelType { + pub fn from_u8(num: u8) -> Result { + match num { + 0 => Ok(Self::None), + 1 => Ok(Self::Public), + 2 => Ok(Self::Battle), + 3 => Ok(Self::BattleTeam), + 4 => Ok(Self::Platoon), + 5 => Ok(Self::Custom), + 6 => Ok(Self::Clan), + 7 => Ok(Self::Private), + 8 => Ok(Self::CustomGame), + _ => Err(rc_core::data::error_codes::ChatErrorCodes::UnexpectedError as i16) + } + } +} + #[allow(dead_code)] #[repr(u8)] #[derive(Copy, Clone)] diff --git a/rc_chat_room/src/events/chat_message.rs b/rc_chat_room/src/events/chat_message.rs new file mode 100644 index 0000000..44c9d86 --- /dev/null +++ b/rc_chat_room/src/events/chat_message.rs @@ -0,0 +1,47 @@ +pub struct PublicMessage { + pub sender_name: String, + pub sender_display_name: String, + pub text: String, + pub is_dev: bool, + pub is_mod: bool, + pub is_admin: bool, + pub channel_name: String, + pub channel_ty: crate::data::channel::ChatChannelType, +} + +impl PublicMessage { + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(8); + params.insert(5, polariton::operation::Typed::Str(self.sender_name.clone().into())); + params.insert(30, polariton::operation::Typed::Str(self.sender_display_name.clone().into())); + params.insert(2, polariton::operation::Typed::Str(self.text.clone().into())); + params.insert(6, polariton::operation::Typed::Bool(self.is_dev)); + params.insert(12, polariton::operation::Typed::Bool(self.is_mod)); + params.insert(13, polariton::operation::Typed::Bool(self.is_admin)); + params.insert(3, polariton::operation::Typed::Str(self.channel_name.clone().into())); + params.insert(1, polariton::operation::Typed::Int(self.channel_ty as _)); + params.into() + } +} + +pub struct PrivateMessage { + pub sender_name: String, + pub sender_display_name: String, + pub text: String, + pub is_dev: bool, + pub is_mod: bool, + pub is_admin: bool, +} + +impl PrivateMessage { + pub fn as_event_params(&self) -> polariton::operation::ParameterTable { + let mut params = std::collections::HashMap::with_capacity(6); + params.insert(5, polariton::operation::Typed::Str(self.sender_name.clone().into())); + params.insert(30, polariton::operation::Typed::Str(self.sender_display_name.clone().into())); + params.insert(2, polariton::operation::Typed::Str(self.text.clone().into())); + params.insert(6, polariton::operation::Typed::Bool(self.is_dev)); + params.insert(12, polariton::operation::Typed::Bool(self.is_mod)); + params.insert(13, polariton::operation::Typed::Bool(self.is_admin)); + params.into() + } +} diff --git a/rc_chat_room/src/events/mod.rs b/rc_chat_room/src/events/mod.rs new file mode 100644 index 0000000..8fc12b7 --- /dev/null +++ b/rc_chat_room/src/events/mod.rs @@ -0,0 +1 @@ +pub mod chat_message; diff --git a/rc_chat_room/src/main.rs b/rc_chat_room/src/main.rs index bcd347d..46a86b8 100644 --- a/rc_chat_room/src/main.rs +++ b/rc_chat_room/src/main.rs @@ -1,8 +1,13 @@ #![forbid(unsafe_code)] mod cli; +mod state; +mod persist; +mod op_handler; +pub use op_handler::SimpleChatFunc; mod data; mod operations; +mod events; use polariton_auth::Handshake; use tokio::net; @@ -21,7 +26,9 @@ async fn main() -> std::io::Result<()> { let cubes = rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(rc_core::persist::user::UserImpl::load(&args.data, &cubes).expect("Bad user data")); - let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(), polariton_server::events::EventsHandler::new())); + let chat_system = state::chat::ChatImpl::new(&args.assets, &args.data).expect("Bad chat config data"); + + let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(chat_system, &args.data, &cubes), polariton_server::events::EventsHandler::new())); let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); diff --git a/rc_chat_room/src/op_handler.rs b/rc_chat_room/src/op_handler.rs new file mode 100644 index 0000000..ce926bb --- /dev/null +++ b/rc_chat_room/src/op_handler.rs @@ -0,0 +1,52 @@ +use polariton::operation::{ParameterTable, OperationResponse}; +use polariton_server::operations::{Operation, OperationCode}; + +pub struct SimpleChatFunc, &U, &crate::state::ChatImpl) -> Result, i16>) + Send + Sync, C: Send + Sync + 'static = ()> { + _user_ty: std::marker::PhantomData, + _custom_ty: std::marker::PhantomData, + chat: crate::state::ChatImpl, + func: F, +} + +impl , &U, &crate::state::ChatImpl) -> Result, i16>) + Send + Sync> SimpleChatFunc { + pub fn new(f: F, chat: crate::state::ChatImpl) -> Self { + Self { + _user_ty: std::marker::PhantomData::default(), + _custom_ty: std::marker::PhantomData::default(), + chat, + func: f, + } + } +} + +impl , &U, &crate::state::ChatImpl) -> Result, i16>) + Send + Sync> Operation for SimpleChatFunc { + type User = U; + + fn handle(&self, p: polariton::operation::ParameterTable, u: &Self::User) -> OperationResponse { + match (self.func)(p, u, &self.chat) { + Ok(p_out) => { + OperationResponse { + code: CODE, + return_code: 0, + message: polariton::operation::Typed::Null, + params: p_out, + } + }, + Err(e_code) => { + OperationResponse { + code: CODE, + return_code: e_code, + message: polariton::operation::Typed::Null, + params: std::collections::HashMap::new().into(), + } + } + } + + } +} + +impl , &U, &crate::state::ChatImpl) -> Result, i16>) + Send + Sync> OperationCode for SimpleChatFunc { + fn op_code() -> u8 { + CODE + } +} diff --git a/rc_chat_room/src/operations/all_joined_channels.rs b/rc_chat_room/src/operations/all_joined_channels.rs index 14d91b7..bb26d8d 100644 --- a/rc_chat_room/src/operations/all_joined_channels.rs +++ b/rc_chat_room/src/operations/all_joined_channels.rs @@ -1,14 +1,17 @@ -use polariton_server::operations::SimpleFunc; -use polariton::operation::{ParameterTable, Typed, Arr}; - -use crate::data::channel::*; +use crate::SimpleChatFunc; +use crate::persist::chat_user::ChatUser; +use polariton::operation::ParameterTable; const PARAM_KEY: u8 = 18; -pub(super) fn all_channels_provider() -> SimpleFunc<11, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { +pub(super) fn all_channels_provider(chat_system: crate::state::ChatImpl) -> SimpleChatFunc<11, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::ChatImpl) -> Result) + Sync + Sync> { + SimpleChatFunc::new(|params, user: &crate::UserTy, _chat_system: &crate::state::ChatImpl| { let mut params = params.to_dict(); - params.insert(PARAM_KEY, Typed::Arr(Arr { + let user_trait = user.user()?; + let chat_user = super::get_chat_user(user_trait.as_ref().as_ref()); + //let chat_user: &ChatUserImpl = user_trait.ext(std::any::TypeId::of::()).unwrap().downcast_ref().unwrap(); + params.insert(PARAM_KEY, chat_user.subscribed_channels()); + /*params.insert(PARAM_KEY, Typed::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashtable items: vec![ ChatChannelInfo { @@ -52,7 +55,7 @@ pub(super) fn all_channels_provider() -> SimpleFunc<11, crate::UserTy, impl (Fn( channel_ty: ChatChannelType::Custom, }.as_transmissible(), ], - })); + }));*/ Ok(params.into()) - }) + }, chat_system) } diff --git a/rc_chat_room/src/operations/join_channel.rs b/rc_chat_room/src/operations/join_channel.rs new file mode 100644 index 0000000..9f15d8b --- /dev/null +++ b/rc_chat_room/src/operations/join_channel.rs @@ -0,0 +1,41 @@ +use crate::{persist::chat_user::ChatUser, SimpleChatFunc}; +use polariton::operation::{ParameterTable, Typed}; + +const CHANNEL_NAME_PARAM_KEY: u8 = 3; // str; in +const CHANNEL_TYPE_PARAM_KEY: u8 = 1; // int; in +const CHANNEL_PASSWORD_PARAM_KEY: u8 = 16; // str; in +const CHANNEL_INFO_PARAM_KEY: u8 = 17; // hashtable; out + +pub(super) fn join_channel_provider(chat_system: crate::state::ChatImpl) -> SimpleChatFunc<1, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::ChatImpl) -> Result) + Sync + Sync> { + SimpleChatFunc::new(|params, user: &crate::UserTy, chat_system| { + let mut params = params.to_dict(); + if let Some(Typed::Str(chann_name)) = params.remove(&CHANNEL_NAME_PARAM_KEY) { + if let Some(Typed::Int(chann_ty)) = params.remove(&CHANNEL_TYPE_PARAM_KEY) { + if let Some(Typed::Str(channel_pwd)) = params.remove(&CHANNEL_PASSWORD_PARAM_KEY) { + log::warn!("Received channel password {} which is unsupported", channel_pwd.string); + } + let user_info = user.user()?; + let chat_user = super::get_chat_user(user_info.as_ref().as_ref()); + chat_system.system_mut().join_channel(user_info.token().uuid.clone(), chann_name.string.clone()); + let response = chat_user.add_subscribed_channel(chann_name.string, crate::data::channel::ChatChannelType::from_u8(chann_ty as _)?); + params.insert(CHANNEL_INFO_PARAM_KEY, response); + } + } + Ok(params.into()) + }, chat_system) +} + +pub(super) fn leave_channel_provider(chat_system: crate::state::ChatImpl) -> SimpleChatFunc<5, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::ChatImpl) -> Result) + Sync + Sync> { + SimpleChatFunc::new(|params, user: &crate::UserTy, chat_system| { + let mut params = params.to_dict(); + if let Some(Typed::Str(chann_name)) = params.remove(&CHANNEL_NAME_PARAM_KEY) { + if let Some(Typed::Int(chann_ty)) = params.remove(&CHANNEL_TYPE_PARAM_KEY) { + let user_info = user.user()?; + let chat_user = super::get_chat_user(user_info.as_ref().as_ref()); + chat_system.system_mut().leave_channel(user_info.token().uuid.clone(), chann_name.string.clone()); + chat_user.remove_subscribed_channel(chann_name.string, crate::data::channel::ChatChannelType::from_u8(chann_ty as _)?); + } + } + Ok(params.into()) + }, chat_system) +} diff --git a/rc_chat_room/src/operations/mod.rs b/rc_chat_room/src/operations/mod.rs index 4439f0f..a4bb3ad 100644 --- a/rc_chat_room/src/operations/mod.rs +++ b/rc_chat_room/src/operations/mod.rs @@ -2,15 +2,29 @@ mod more_auth; mod chat_ignores; mod pending_sanctions; mod all_joined_channels; +mod send_message; +mod public_channels; +mod join_channel; +mod user_online; use polariton_server::operations::OperationsHandler; -pub fn handler() -> OperationsHandler { +pub fn handler(chat_system: crate::state::chat::ChatImpl, data_root: impl AsRef, conf: &rc_core::persist::config::ConfigImpl) -> OperationsHandler { OperationsHandler::new() - .add(more_auth::MoreLobbyAuth) + .add(more_auth::MoreLobbyAuth::new(chat_system.clone(), data_root)) .add(chat_ignores::ignores_provider()) .add(pending_sanctions::pending_sanctions_checker()) - .add(all_joined_channels::all_channels_provider()) + .add(all_joined_channels::all_channels_provider(chat_system.clone())) .add(polariton_server::operations::Ack::<12, _>::default()) + .add(send_message::send_public_message_handler(chat_system.clone())) + .add(public_channels::public_channels_provider(conf)) + .add(join_channel::join_channel_provider(chat_system.clone())) + .add(user_online::is_online_provider(chat_system.clone())) + .add(send_message::send_private_message_handler(chat_system.clone())) + .add(join_channel::leave_channel_provider(chat_system.clone())) //.add(polariton_server::operations::Ack::<00000, _>::default()) } + +pub(self) fn get_chat_user<'a, C>(user: &'a dyn rc_core::persist::user::User) -> &'a crate::persist::chat_user::ChatUserImpl { + user.ext(std::any::TypeId::of::()).unwrap().downcast_ref().unwrap() +} diff --git a/rc_chat_room/src/operations/more_auth.rs b/rc_chat_room/src/operations/more_auth.rs index 5dba737..bbdd916 100644 --- a/rc_chat_room/src/operations/more_auth.rs +++ b/rc_chat_room/src/operations/more_auth.rs @@ -1,10 +1,52 @@ use polariton::operation::Typed; use polariton_server::operations::{Operation, OperationCode}; -pub struct MoreLobbyAuth; +use crate::persist::chat_user::{ChatUser, ChatUserImpl}; + +pub struct MoreLobbyAuth { + chat_system: crate::state::chat::ChatImpl, + root: std::path::PathBuf, +} impl MoreLobbyAuth { const AUTH_PAYLOAD_KEY: u8 = 245; + + pub fn new(chat_system: crate::state::chat::ChatImpl, root: impl AsRef) -> Self { + Self { + chat_system, + root: root.as_ref().to_path_buf(), + } + } + + fn build_ext_map(&self, token: &rc_core::persist::user::UserToken) -> Option>> { + let user_dir = self.root.join(rc_core::persist::user::USERS_DIR).join(&token.uuid); + let data = if let Ok(data) = ChatUserImpl::load(&user_dir) { + data + } else { + let data = ChatUserImpl::default_load(&user_dir); + data + }; + let mut map = std::collections::HashMap::with_capacity(1); + map.insert(std::any::TypeId::of::(), Box::new(data) as _); + Some(map) + } + + fn do_auth(&self, params: std::collections::HashMap>, user: &crate::UserTy) -> Result, i16> { + if let Some(Typed::Str(auth_payload)) = params.get(&Self::AUTH_PAYLOAD_KEY) { + if user.update_with_auth_ext(&auth_payload.string, |t| self.build_ext_map(t)) { + let user_impl = user.user()?; + let name = user_impl.token().uuid.clone(); + let chat_user = super::get_chat_user(user_impl.as_ref().as_ref()); + let channels = chat_user.subscribed_channels_strings(); + let event_tx = user.event_sender(); + self.chat_system.system_mut().connect_user(name, channels, event_tx); + let mut resp_params = std::collections::HashMap::new(); + resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); + return Ok(resp_params.into()); + } + } + Err(120) + } } impl Operation for MoreLobbyAuth { @@ -12,24 +54,24 @@ impl Operation for MoreLobbyAuth { fn handle(&self, params: polariton::operation::ParameterTable, user: &Self::User) -> polariton::operation::OperationResponse { 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) { - let mut resp_params = std::collections::HashMap::new(); - resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); - return polariton::operation::OperationResponse { - code: 230, + match self.do_auth(params_dict, user) { + Ok(params) => { + polariton::operation::OperationResponse { + code: Self::op_code(), return_code: 0, message: polariton::operation::Typed::Null, - params: resp_params.into(), + params, + } + }, + Err(code) => { + polariton::operation::OperationResponse { + code: Self::op_code(), + return_code: code, + message: polariton::operation::Typed::Null, + params: std::collections::HashMap::new().into(), } } } - polariton::operation::OperationResponse { - code: 230, - return_code: 120, - message: polariton::operation::Typed::Null, - params: std::collections::HashMap::new().into(), - } } } diff --git a/rc_chat_room/src/operations/public_channels.rs b/rc_chat_room/src/operations/public_channels.rs new file mode 100644 index 0000000..26f0854 --- /dev/null +++ b/rc_chat_room/src/operations/public_channels.rs @@ -0,0 +1,20 @@ +use polariton_server::operations::Immediate; +//use polariton::operation::{ParameterTable, Typed}; +use rc_core::ConfigProvider; + +const PARAM_KEY: u8 = 20; + +pub(super) fn public_channels_provider(conf: &rc_core::persist::config::ConfigImpl) -> Immediate<13, crate::UserTy> { + let pub_channs = conf.public_channels(); + Immediate::new(move || { + let mut params = std::collections::HashMap::with_capacity(1); + params.insert(PARAM_KEY, pub_channs.to_owned()); + /*params.insert(PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::Str, + items: vec![ + Typed::Str("Pluto".into()), + ], + }));*/ + params.into() + }) +} diff --git a/rc_chat_room/src/operations/send_message.rs b/rc_chat_room/src/operations/send_message.rs new file mode 100644 index 0000000..c082460 --- /dev/null +++ b/rc_chat_room/src/operations/send_message.rs @@ -0,0 +1,51 @@ +use polariton::operation::{ParameterTable, Typed}; + +const CHANNEL_TYPE_PARAM_KEY: u8 = 1; // in; int +const MESSAGE_TEXT_PARAM_KEY: u8 = 2; // in; str +const CHANNEL_NAME_PARAM_KEY: u8 = 3; // in; str +const CHAT_LOCATION_PARAM_KEY: u8 = 29; // in; str + +pub fn send_public_message_handler(chat_system: crate::state::chat::ChatImpl) -> crate::SimpleChatFunc<2, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::chat::ChatImpl) -> Result) + Sync + Sync> { + crate::SimpleChatFunc::new(|params, user: &crate::UserTy, chat| { + let mut params = params.to_dict(); + if let Some(Typed::Int(channel_ty)) = params.remove(&CHANNEL_TYPE_PARAM_KEY) { + let channel_enum = crate::data::channel::ChatChannelType::from_u8(channel_ty as u8)?; + if let Some(Typed::Str(channel_name)) = params.remove(&CHANNEL_NAME_PARAM_KEY) { + if let Some(Typed::Str(message_text)) = params.remove(&MESSAGE_TEXT_PARAM_KEY) { + let chat_loc = if let Some(Typed::Str(chat_loc)) = params.remove(&CHAT_LOCATION_PARAM_KEY) { + chat_loc.string.clone() + } else { + "".to_owned() + }; + let user = user.user()?; + let chat_system = chat.system(); + log::debug!("Got message `{}` from user {} ({} @ {}/{:?})", message_text.string, user.token().uuid, chat_loc, channel_name.string, channel_enum); + chat_system.handle_public_message(user.as_ref().as_ref(), message_text.string, channel_name.string, channel_enum); + } + } + } + Ok(params.into()) + }, chat_system) +} + +pub const USERNAME_PARAM_KEY: u8 = 7; // in; str + +pub fn send_private_message_handler(chat_system: crate::state::chat::ChatImpl) -> crate::SimpleChatFunc<3, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::chat::ChatImpl) -> Result) + Sync + Sync> { + crate::SimpleChatFunc::new(|params, user: &crate::UserTy, chat| { + let mut params = params.to_dict(); + if let Some(Typed::Str(username)) = params.remove(&USERNAME_PARAM_KEY) { + if let Some(Typed::Str(message_text)) = params.remove(&MESSAGE_TEXT_PARAM_KEY) { + let chat_loc = if let Some(Typed::Str(chat_loc)) = params.remove(&CHAT_LOCATION_PARAM_KEY) { + chat_loc.string.clone() + } else { + "".to_owned() + }; + let user = user.user()?; + let chat_system = chat.system(); + log::debug!("Got message `{}` from user {} (@ {} to {})", message_text.string, user.token().uuid, chat_loc, username.string); + chat_system.handle_private_message(user.as_ref().as_ref(), message_text.string, username.string); + } + } + Ok(params.into()) + }, chat_system) +} diff --git a/rc_chat_room/src/operations/user_online.rs b/rc_chat_room/src/operations/user_online.rs new file mode 100644 index 0000000..b97f193 --- /dev/null +++ b/rc_chat_room/src/operations/user_online.rs @@ -0,0 +1,36 @@ +use crate::SimpleChatFunc; +use polariton::operation::{ParameterTable, Typed}; + +const USERNAME_PARAM_KEY: u8 = 22; // str; in & out +const DISPLAY_NAME_PARAM_KEY: u8 = 30; // str; out +const CAN_SEND_DM_PARAM_KEY: u8 = 27; // int; out + +#[allow(dead_code)] +#[repr(u8)] +enum CanSendMessageResult { + Ok = 0, + UserDoesNotExist = 1, + UserOffline = 2, +} + + +pub(super) fn is_online_provider(chat_system: crate::state::ChatImpl) -> SimpleChatFunc<14, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::ChatImpl) -> Result) + Sync + Sync> { + SimpleChatFunc::new(|params, _: &crate::UserTy, chat_system| { + let mut params = params.to_dict(); + if let Some(Typed::Str(user_name)) = params.get(&USERNAME_PARAM_KEY) { + let username = user_name.string.clone(); + params.insert(DISPLAY_NAME_PARAM_KEY, Typed::Str(username.clone().into())); + if chat_system.system().is_user_online(&username) { + params.insert(CAN_SEND_DM_PARAM_KEY, Typed::Int(CanSendMessageResult::Ok as _)); + Ok(params.into()) + } else { + log::debug!("User {} is not online", username); + params.insert(CAN_SEND_DM_PARAM_KEY, Typed::Int(CanSendMessageResult::UserOffline as _)); + //params.insert(CAN_SEND_DM_PARAM_KEY, Typed::Int(CanSendMessageResult::Ok as _)); + Ok(params.into()) + } + } else { + Err(rc_core::data::error_codes::ChatErrorCodes::UnexpectedError as _) + } + }, chat_system) +} diff --git a/rc_chat_room/src/persist/chat_user/chat_json.rs b/rc_chat_room/src/persist/chat_user/chat_json.rs new file mode 100644 index 0000000..5fc91c7 --- /dev/null +++ b/rc_chat_room/src/persist/chat_user/chat_json.rs @@ -0,0 +1,111 @@ +use serde::{Serialize, Deserialize}; +use polariton::operation::{Typed, Arr}; + +use crate::data::channel::{ChatChannelInfo, ChatChannelType}; + +pub struct ChatUserInfo { + data: std::sync::RwLock, + root: std::path::PathBuf, +} + +impl ChatUserInfo { + pub fn load(root: impl AsRef) -> std::io::Result { + let data = ChatUserData::load(root.as_ref())?; + Ok(Self { + data: std::sync::RwLock::new(data), + root: root.as_ref().to_path_buf(), + }) + } + + /*pub fn save(&self) -> std::io::Result<()> { + self.data.read().unwrap().save(&self.root) + }*/ + + pub fn default_load(root: impl AsRef) -> Self { + let data = ChatUserData::default_load(); + if let Err(e) = data.save(root.as_ref()) { + log::error!("Failed to save default chat data to {}: {}", root.as_ref().display(), e); + } + Self { + data: std::sync::RwLock::new(data), + root: root.as_ref().to_path_buf(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ChatUserData { + subscribed_channels: Vec, +} + +impl ChatUserData { + fn load(root: impl AsRef) -> std::io::Result { + let file = std::fs::File::open(root.as_ref().join(super::CHAT_USER_FILE))?; + let buffered = std::io::BufReader::new(file); + let result = serde_json::from_reader(buffered)?; + Ok(result) + } + + fn save(&self, root: impl AsRef) -> std::io::Result<()> { + let file = std::fs::File::create(root.as_ref().join(super::CHAT_USER_FILE))?; + let buffered = std::io::BufWriter::new(file); + serde_json::to_writer_pretty(buffered, self)?; + Ok(()) + } + + fn default_load() -> Self { + Self { + subscribed_channels: vec![ + "main".to_owned(), + "sys".to_owned(), + ] + } + } +} + +impl super::ChatUser for ChatUserInfo { + fn subscribed_channels(&self) -> polariton::operation::Typed<()> { + Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::HashMap, // hashtable + items: self.data.read().unwrap().subscribed_channels.iter().map(|name| ChatChannelInfo { + channel_name: name.to_owned(), + members: Vec::default(), + channel_ty: ChatChannelType::Public, + }.as_transmissible()).collect() + }) + } + + fn subscribed_channels_strings(&self) -> Vec { + self.data.read().unwrap().subscribed_channels.clone() + } + + fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Typed<()> { + if let crate::data::channel::ChatChannelType::Public = channel_ty { + let mut lock = self.data.write().unwrap(); + lock.subscribed_channels.push(channel.clone()); + if let Err(e) = lock.save(&self.root) { + log::error!("Failed to save chat data to {}: {}", self.root.display(), e); + } + } + crate::data::channel::ChatChannelInfo { + channel_name: channel, + members: Vec::default(), + channel_ty, + }.as_transmissible() + } + + fn remove_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> bool { + if let crate::data::channel::ChatChannelType::Public = channel_ty { + let mut lock = self.data.write().unwrap(); + if let Some(index) = lock.subscribed_channels.iter().position(|chann| chann == &channel) { + lock.subscribed_channels.swap_remove(index); + if let Err(e) = lock.save(&self.root) { + log::error!("Failed to save chat data to {}: {}", self.root.display(), e); + } else { + return true; + } + } + } + false + } +} diff --git a/rc_chat_room/src/persist/chat_user/mod.rs b/rc_chat_room/src/persist/chat_user/mod.rs new file mode 100644 index 0000000..cb57a59 --- /dev/null +++ b/rc_chat_room/src/persist/chat_user/mod.rs @@ -0,0 +1,8 @@ +mod chat_json; +pub use chat_json::ChatUserInfo; + +mod traits; +pub use traits::ChatUser; + +pub const CHAT_USER_FILE: &str = "chat.json"; +pub type ChatUserImpl = ChatUserInfo; diff --git a/rc_chat_room/src/persist/chat_user/traits.rs b/rc_chat_room/src/persist/chat_user/traits.rs new file mode 100644 index 0000000..0772cd1 --- /dev/null +++ b/rc_chat_room/src/persist/chat_user/traits.rs @@ -0,0 +1,8 @@ +use polariton::operation::Typed; + +pub trait ChatUser { + fn subscribed_channels(&self) -> Typed<()>; + fn subscribed_channels_strings(&self) -> Vec; + fn add_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> Typed<()>; + fn remove_subscribed_channel(&self, channel: String, channel_ty: crate::data::channel::ChatChannelType) -> bool; +} diff --git a/rc_chat_room/src/persist/config/chat.rs b/rc_chat_room/src/persist/config/chat.rs new file mode 100644 index 0000000..8f6607f --- /dev/null +++ b/rc_chat_room/src/persist/config/chat.rs @@ -0,0 +1,43 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ChatSystemConfig { + #[serde(default = "default_command_chann")] + pub command_channel: String, + pub commands: Vec, +} + +fn default_command_chann() -> String { + "sys".to_owned() +} + +impl ChatSystemConfig { + pub fn load(asset_root: impl AsRef) -> std::io::Result { + let file = std::fs::File::open(asset_root.as_ref().join(super::CHAT_CONFIG_FILE))?; + let buffered = std::io::BufReader::new(file); + let config = serde_json::from_reader(buffered)?; + Ok(config) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ChatCommand { + pub regex: String, + pub op: ChatOperation, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "type")] +pub enum ChatOperation { + BuiltIn(BuiltInChatOperation), + Custom, + #[serde(alias = "No-op")] + Nop, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "built_in")] +pub enum BuiltInChatOperation { + OnlineUsers, + TotalUsers, +} diff --git a/rc_chat_room/src/persist/config/mod.rs b/rc_chat_room/src/persist/config/mod.rs new file mode 100644 index 0000000..68ccf99 --- /dev/null +++ b/rc_chat_room/src/persist/config/mod.rs @@ -0,0 +1,4 @@ +mod chat; +pub use chat::{ChatSystemConfig, ChatCommand, ChatOperation, BuiltInChatOperation}; + +pub const CHAT_CONFIG_FILE: &str = "chat.json"; diff --git a/rc_chat_room/src/persist/mod.rs b/rc_chat_room/src/persist/mod.rs new file mode 100644 index 0000000..9015d83 --- /dev/null +++ b/rc_chat_room/src/persist/mod.rs @@ -0,0 +1,2 @@ +pub mod chat_user; +pub mod config; diff --git a/rc_chat_room/src/state/chat/chat.rs b/rc_chat_room/src/state/chat/chat.rs new file mode 100644 index 0000000..cf4021e --- /dev/null +++ b/rc_chat_room/src/state/chat/chat.rs @@ -0,0 +1,180 @@ +use std::collections::HashMap; + +#[derive(Clone)] +pub struct ChatProvider { + chat_system: std::sync::Arc>, +} + +impl ChatProvider { + pub fn new(asset_root: impl AsRef, data_root: impl AsRef) -> std::io::Result { + Ok(Self { + chat_system: std::sync::Arc::new(std::sync::RwLock::new(crate::state::chat::ChatSystem::new(asset_root, data_root)?)), + }) + } + + pub fn system(&self) -> std::sync::RwLockReadGuard<'_, crate::state::chat::ChatSystem> { + self.chat_system.read().unwrap() + } + + pub fn system_mut(&self) -> std::sync::RwLockWriteGuard<'_, crate::state::chat::ChatSystem> { + self.chat_system.write().unwrap() + } +} + +pub struct ChatSystem { + chats: HashMap, + online_users: HashMap, + config: super::ChatSystemConfig, +} + +impl ChatSystem { + fn cleanup(&mut self) -> usize { + let mut to_be_removed = Vec::new(); + for (key, val) in self.online_users.iter() { + if !val.is_online() { + to_be_removed.push(key.to_owned()); + } + } + for offline_user in to_be_removed.iter() { + self.online_users.remove(offline_user); + } + let total_removed_users = to_be_removed.len(); + + to_be_removed.clear(); + for (key, val) in self.chats.iter_mut() { + if val.is_empty_mut() { + to_be_removed.push(key.to_owned()); + } + } + for empty_room in to_be_removed.iter() { + self.chats.remove(empty_room); + } + + total_removed_users + to_be_removed.len() + } + + pub fn connect_user(&mut self, display_name: String, channels: Vec, event_tx: tokio::sync::mpsc::UnboundedSender) { + self.cleanup(); + let handle = super::UserHandle::from_strong_sender(event_tx, display_name.clone()); + self.online_users.insert(display_name, handle.clone()); + for channel in channels { + if let Some(chat) = self.chats.get_mut(&channel) { + chat.connect_user(handle.clone()); + } else { + let mut new_room = super::ChatRoom::new(channel.clone(), crate::data::channel::ChatChannelType::Public); + new_room.connect_user(handle.clone()); + self.chats.insert(channel, new_room); + } + } + } + + pub fn join_channel(&mut self, display_name: String, channel: String) { + if let Some(user_handle) = self.online_users.get(&display_name) { + if let Some(chat_room) = self.chats.get_mut(&channel) { + chat_room.connect_user(user_handle.to_owned()); + } + } + self.cleanup(); + } + + pub fn leave_channel(&mut self, display_name: String, channel: String) { + if let Some(chat_room) = self.chats.get_mut(&channel) { + chat_room.remove_user(&display_name); + } + } + + pub fn handle_public_message(&self, user: &dyn rc_core::persist::user::User<()>, text: String, channel: String, channel_ty: crate::data::channel::ChatChannelType) { + if self.config.is_command_channel(&channel) { + if let Some(user_handle) = self.online_users.get(&user.token().uuid) { + self.handle_public_command(user, text, user_handle, channel, channel_ty); + } + } else if let Some(room) = self.chats.get(&channel) { + let event_params = crate::events::chat_message::PublicMessage { + sender_name: user.token().uuid.clone(), + sender_display_name: user.token().uuid.clone(), + text, + is_dev: user.is_dev(), + is_mod: user.is_mod(), + is_admin: user.is_admin(), + channel_name: channel, + channel_ty, + }; + room.send_public_message(event_params); + } + } + + fn handle_public_command(&self, user: &dyn rc_core::persist::user::User<()>, text: String, handle: &super::UserHandle, channel: String, channel_ty: crate::data::channel::ChatChannelType) { + let event_params = crate::events::chat_message::PublicMessage { + sender_name: self.config.command_username().to_owned(), + sender_display_name: self.config.command_username().to_owned(), + text: self.config.perform_command(&text, self, user), + is_dev: false, + is_mod: false, + is_admin: false, + channel_name: channel, + channel_ty, + }; + tokio::spawn(Self::send_public_command_response(handle.to_owned(), event_params)); + } + + async fn send_public_command_response(handle: super::UserHandle, response: crate::events::chat_message::PublicMessage) { + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + let event = polariton::operation::Event { + code: 1, + params: response.as_event_params(), + }; + handle.send(polariton_server::ToSend::Data { data: polariton::packet::Data::Event(event), encrypt: true, channel: 0, reliable: true }); + } + + pub fn handle_private_message(&self, user: &dyn rc_core::persist::user::User<()>, text: String, recipient: String) { + if self.config.is_command_user(&recipient) { + if let Some(user_handle) = self.online_users.get(&user.token().uuid) { + self.handle_private_command(user, text, user_handle); + } + } else if let Some(recipient_handle) = self.online_users.get(&recipient) { + let private_msg = crate::events::chat_message::PrivateMessage { + sender_name: user.token().uuid.clone(), + sender_display_name: user.token().uuid.clone(), + text, + is_dev: user.is_dev(), + is_mod: user.is_mod(), + is_admin: user.is_admin(), + }; + recipient_handle.send_private_message(private_msg); + } + } + + fn handle_private_command(&self, user: &dyn rc_core::persist::user::User<()>, text: String, handle: &super::UserHandle) { + let event_params = crate::events::chat_message::PrivateMessage { + sender_name: self.config.command_username().to_owned(), + sender_display_name: self.config.command_username().to_owned(), + text: self.config.perform_command(&text, self, user), + is_dev: false, + is_mod: false, + is_admin: false, + }; + tokio::spawn(Self::send_private_command_response(handle.to_owned(), event_params)); + } + + async fn send_private_command_response(handle: super::UserHandle, response: crate::events::chat_message::PrivateMessage) { + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + handle.send_private_message(response); + } + + pub fn new(asset_root: impl AsRef, data_root: impl AsRef) -> std::io::Result { + let config_persist = crate::persist::config::ChatSystemConfig::load(&asset_root)?; + Ok(Self { + chats: HashMap::new(), + online_users: HashMap::new(), + config: super::ChatSystemConfig::from_persist(config_persist, asset_root.as_ref().to_path_buf(), data_root.as_ref().to_path_buf())?, + }) + } + + pub fn user_count(&self) -> usize { + self.online_users.values().filter(|x| x.is_online()).count() + } + + pub fn is_user_online(&self, display_name: &str) -> bool { + self.config.is_command_user(display_name) || self.online_users.get(display_name).map(|x| x.is_online()).unwrap_or(false) + } +} diff --git a/rc_chat_room/src/state/chat/config.rs b/rc_chat_room/src/state/chat/config.rs new file mode 100644 index 0000000..97bb594 --- /dev/null +++ b/rc_chat_room/src/state/chat/config.rs @@ -0,0 +1,144 @@ +pub struct ChatSystemConfig { + command_channel: String, + commands: Vec, + asset_root: std::path::PathBuf, + data_root: std::path::PathBuf, +} + +#[allow(dead_code)] +#[derive(Clone, Copy)] +struct CommandContext<'a, 'b, 'c> { + chat_system: &'a super::ChatSystem, + user: &'b dyn rc_core::persist::user::User<()>, + asset_root: &'c std::path::PathBuf, + data_root: &'c std::path::PathBuf, +} + +impl ChatSystemConfig { + pub fn from_persist(config: crate::persist::config::ChatSystemConfig, asset_root: std::path::PathBuf, data_root: std::path::PathBuf) -> std::io::Result { + let mut compiled_commands = Vec::with_capacity(config.commands.len()); + for (i, cmd) in config.commands.into_iter().enumerate() { + let compiled_command = ChatCommand::compile_command(cmd).map_err(|e| { + log::error!("Failed to load command {}: {}", i, e); + std::io::Error::new(std::io::ErrorKind::InvalidInput, e) + })?; + compiled_commands.push(compiled_command); + } + Ok(Self { + command_channel: config.command_channel, + commands: compiled_commands, + asset_root, + data_root, + }) + } + + pub fn perform_command(&self, text: &str, chat_system: &super::ChatSystem, user: &dyn rc_core::persist::user::User<()>,) -> String { + let ctx = CommandContext { + chat_system, + user, + asset_root: &self.asset_root, + data_root: &self.data_root, + }; + for cmd in self.commands.iter() { + if let Some(result) = cmd.perform_if_match(text, ctx) { + return result; + } + } + return "Invalid command".to_owned() + } + + pub fn is_command_channel(&self, channel: &str) -> bool { + self.command_channel == channel + } + + pub fn is_command_user(&self, username: &str) -> bool { + self.command_channel == username + } + + pub fn command_username(&self) -> &'_ str { + &self.command_channel + } +} + +pub struct ChatCommand { + regex: regex::Regex, + op: ChatOperation, +} + +impl ChatCommand { + fn compile_command(command: crate::persist::config::ChatCommand) -> Result { + Ok(Self { + regex: regex::RegexBuilder::new(&command.regex).build()?, + op: ChatOperation::from_persist(command.op) + }) + } + + fn perform_if_match(&self, text: &str, ctx: CommandContext) -> Option { + if let Some(cap) = self.regex.captures(text) { + Some(self.op.perform_command(cap, ctx)) + } else { + None + } + } +} + +enum ChatOperation { + BuiltIn(BuiltIn), + Custom, + Nop, +} + +impl ChatOperation { + fn from_persist(op: crate::persist::config::ChatOperation) -> Self { + match op { + crate::persist::config::ChatOperation::BuiltIn(b_in) => Self::BuiltIn(BuiltIn::from_persist(b_in)), + crate::persist::config::ChatOperation::Custom => Self::Custom, + crate::persist::config::ChatOperation::Nop => Self::Nop, + } + } + + fn perform_command<'a>(&self, _captures: regex::Captures<'a>, ctx: CommandContext) -> String { + match self { + Self::BuiltIn(b_in) => b_in.do_command(ctx), + Self::Custom => "{not implemented}".to_owned(), + Self::Nop => "{no op}".to_owned(), + } + } +} + +enum BuiltIn { + OnlineUsers, + TotalUsers, +} + +impl BuiltIn { + fn from_persist(b_in: crate::persist::config::BuiltInChatOperation) -> Self { + match b_in { + crate::persist::config::BuiltInChatOperation::OnlineUsers => Self::OnlineUsers, + crate::persist::config::BuiltInChatOperation::TotalUsers => Self::TotalUsers, + } + } + + fn do_command(&self, ctx: CommandContext) -> String { + match self { + Self::OnlineUsers => { + let online_count = ctx.chat_system.user_count(); + if online_count == 1 { + "1 user online".to_owned() + } else { + format!("{} users online", online_count) + } + }, + Self::TotalUsers => { + let user_path = ctx.data_root.join(rc_core::persist::user::USERS_DIR); + let user_count = user_path.read_dir().map_or(0, |dir| dir.count()).clamp(1, usize::MAX) - 1; + if user_count == 1 { + "1 user exists".to_owned() + } else { + format!("{} users exist", user_count) + } + + }, + } + } +} diff --git a/rc_chat_room/src/state/chat/mod.rs b/rc_chat_room/src/state/chat/mod.rs new file mode 100644 index 0000000..0e5cd7c --- /dev/null +++ b/rc_chat_room/src/state/chat/mod.rs @@ -0,0 +1,13 @@ +mod chat; +pub use chat::{ChatSystem, ChatProvider}; + +mod room; +pub use room::ChatRoom; + +mod user; +pub use user::UserHandle; + +mod config; +pub use config::{ChatSystemConfig}; + +pub type ChatImpl = ChatProvider; diff --git a/rc_chat_room/src/state/chat/room.rs b/rc_chat_room/src/state/chat/room.rs new file mode 100644 index 0000000..101dad8 --- /dev/null +++ b/rc_chat_room/src/state/chat/room.rs @@ -0,0 +1,92 @@ +pub struct ChatRoom { + name: String, + channel: crate::data::channel::ChatChannelType, + online_users: Vec, +} + +impl ChatRoom { + /// Remove offline users that are still in the list + fn cleanup(&mut self) -> usize { + let mut total_changes = 0; + let mut index = 0; + while self.online_users.get(index).is_some() { + if self.online_users.get(index).unwrap().is_online() { + index += 1; + } else { + self.online_users.swap_remove(index); + total_changes += 1; + } + } + total_changes + } + + pub fn is_empty(&self) -> bool { + for user in self.online_users.iter() { + if user.is_online() { + return false; + } + } + true + } + + pub fn is_empty_mut(&mut self) -> bool { + self.cleanup(); + self.is_empty() + } + + pub fn send_public_message(&self, message: crate::events::chat_message::PublicMessage) { + let event = polariton::operation::Event { + code: 1, + params: message.as_event_params(), + }; + for user in self.online_users.iter() { + user.send(polariton_server::ToSend::Data { + data: polariton::packet::Data::Event(event.clone()), + encrypt: true, + channel: 0, + reliable: true, + }); + } + } + + pub fn new(name: String, type_: crate::data::channel::ChatChannelType) -> Self { + Self { + name, + channel: type_, + online_users: Vec::new(), + } + } + + pub fn connect_user(&mut self, handle: super::UserHandle) { + self.cleanup(); + let event = polariton::operation::Event { + code: 1, + params: crate::events::chat_message::PublicMessage { + sender_name: "system".to_owned(), + sender_display_name: "system".to_owned(), + channel_name: self.name.clone(), + channel_ty: self.channel, + text: "joined".to_owned(), + is_dev: false, + is_mod: false, + is_admin: false, + }.as_event_params(), + }; + handle.send(polariton_server::ToSend::Data { + data: polariton::packet::Data::Event(event), + encrypt: true, + channel: 0, + reliable: true, + }); + self.online_users.push(handle); + } + + pub fn remove_user(&mut self, name: &str) -> bool { + if let Some(user_index) = self.online_users.iter().position(|x| name == x.name()) { + self.online_users.swap_remove(user_index); + true + } else { + false + } + } +} diff --git a/rc_chat_room/src/state/chat/user.rs b/rc_chat_room/src/state/chat/user.rs new file mode 100644 index 0000000..9524761 --- /dev/null +++ b/rc_chat_room/src/state/chat/user.rs @@ -0,0 +1,43 @@ +#[derive(Clone)] +pub struct UserHandle { + display_name: String, + event_tx: tokio::sync::mpsc::WeakUnboundedSender, +} + +impl UserHandle { + pub fn is_online(&self) -> bool { + self.event_tx.strong_count() != 0 + } + + pub fn from_strong_sender(event_tx: tokio::sync::mpsc::UnboundedSender, display_name: String) -> Self { + Self { + event_tx: event_tx.downgrade(), + display_name, + } + } + + pub fn send(&self, to_send: polariton_server::ToSend) -> bool { + if let Some(event_tx) = self.event_tx.upgrade() { + event_tx.send(to_send).is_ok() + } else { + false + } + } + + pub fn send_private_message(&self, message: crate::events::chat_message::PrivateMessage) { + let event = polariton::operation::Event { + code: 2, + params: message.as_event_params(), + }; + self.send(polariton_server::ToSend::Data { + data: polariton::packet::Data::Event(event.clone()), + encrypt: true, + channel: 0, + reliable: true, + }); + } + + pub fn name(&self) -> &'_ str { + &self.display_name + } +} diff --git a/rc_chat_room/src/state/mod.rs b/rc_chat_room/src/state/mod.rs new file mode 100644 index 0000000..794c082 --- /dev/null +++ b/rc_chat_room/src/state/mod.rs @@ -0,0 +1,2 @@ +pub mod chat; +pub use chat::ChatImpl; diff --git a/rc_core/src/data/error_codes.rs b/rc_core/src/data/error_codes.rs index 251eeb9..d024dc2 100644 --- a/rc_core/src/data/error_codes.rs +++ b/rc_core/src/data/error_codes.rs @@ -36,3 +36,27 @@ pub enum WebServicesError { UsernameTooShort = 206, SaleEnded = 207 } + +#[repr(i16)] +#[allow(dead_code)] +#[derive(Debug)] +pub enum ChatErrorCodes { + None = 0, + UnexpectedError = 1, + Flood = 2, + Muted = 3, + NotOnline = 4, + DoesNotExist = 5, + NoConnection = 6, + ModeratorsOnly = 7, + AdminsOnly = 8, + SanctionAlreadyExists = 9, + AlreadyWarned = 10, + NoSanctionExists = 11, + MaintenanceMode = 12, + ChannelExists = 13, + IncorrectPassword = 14, + ChannelNotExists = 15, + PasswordRequired = 16, + ChannelExpired = 17, +} diff --git a/rc_core/src/persist/chat.rs b/rc_core/src/persist/chat.rs new file mode 100644 index 0000000..bb6c33c --- /dev/null +++ b/rc_core/src/persist/chat.rs @@ -0,0 +1,15 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ChatConfig { + #[serde(default = "default_pub_channs")] + pub public_channels: Vec, +} + +fn default_pub_channs() -> Vec { + vec![ + "main".to_owned(), + "sys".to_owned(), + "openjam_worship".to_owned(), + ] +} diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index edfea5f..ea558a7 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -5,7 +5,7 @@ use serde::{Serialize, Deserialize}; use polariton::operation::{Typed, Dict}; use polariton::serdes::TypePrefix; -use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings}; +use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig}; const CUBE_CONFIG_FILENAME: &str = "config.json"; @@ -15,6 +15,7 @@ pub struct CubeConfig { movement: HashMap, lerp_value: f32, battle: BattleConfig, + chat: ChatConfig, settings: Settings, } @@ -247,4 +248,11 @@ impl super::ConfigProvider for CubeConfig { fn login_messages(&self) -> super::DevMessageProvider { super::DevMessageProvider::new(self.settings.banners.iter().map(|msg| (msg.message.clone(), msg.duration as i32)).collect()) } + + fn public_channels(&self) -> Typed { + Typed::Arr(polariton::operation::Arr { + ty: TypePrefix::Str, + items: self.chat.public_channels.iter().map(|s| Typed::Str(s.into())).collect(), + }) + } } diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index c196f2b..1eb3976 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -17,6 +17,7 @@ pub trait ConfigProvider { fn campaign_details(&self) -> CompleteCampaignProvider; fn client_config(&self) -> Typed; fn login_messages(&self) -> DevMessageProvider; + fn public_channels(&self) -> Typed; } pub struct CompleteCampaignProvider { diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index 29533ff..bbe0d59 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -29,6 +29,9 @@ pub use client_config::GameplaySettings; mod settings; pub use settings::Settings; +mod chat; +pub use chat::ChatConfig; + pub(self) const VALID_ROBOT: &[u8] = &[64, 0, 0, diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index ff6d311..52f0236 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -34,7 +34,7 @@ impl AccountProvider { } impl super::UserProvider for AccountProvider { - fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, String> { + fn authenticate(&self, token: super::UserToken, ext: std::collections::HashMap>) -> Result + Send + Sync>, String> { let new_root = self.root.join(&token.uuid); let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); @@ -46,6 +46,7 @@ impl super::UserProvider for AccountProvider { token, account: account_info, cubes: self.cubes.clone(), + extensions: ext, })) //Err("Unable to authenticate".to_string()) } @@ -124,6 +125,7 @@ struct UserData { token: super::UserToken, account: AccountInfo, cubes: std::sync::Arc>, + extensions: std::collections::HashMap>, } impl UserData { @@ -159,6 +161,10 @@ const INVALID_ROBOT_ERR: i16 = 140; const DATABASE_ERR: i16 = 8; impl super::User for UserData { + fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)> { + self.extensions.get(&ty).map(|x| x.as_ref()) + } + fn token(&self) -> &'_ super::UserToken { &self.token } @@ -312,7 +318,6 @@ impl super::User for UserData { ], }.as_transmissible()) } - } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index c334ca5..c47e73b 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -26,7 +26,7 @@ pub struct UserLoginInfo { } pub trait UserProvider { - fn authenticate(&self, user: UserToken) -> Result + Send + Sync>, String>; + fn authenticate(&self, user: UserToken, ext: std::collections::HashMap>) -> Result + Send + Sync>, String>; } pub trait UserAuthenticator { @@ -34,6 +34,7 @@ pub trait UserAuthenticator { } pub trait User { + fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)>; fn token(&self) -> &'_ super::UserToken; fn is_mod(&self) -> bool; fn is_admin(&self) -> bool; diff --git a/rc_core/src/state.rs b/rc_core/src/state.rs index 9c970e5..ce02458 100644 --- a/rc_core/src/state.rs +++ b/rc_core/src/state.rs @@ -8,6 +8,10 @@ pub struct UserState { impl UserState { pub fn update_with_auth(&self, auth_str: &str) -> bool { + self.update_with_auth_ext(auth_str, |_| Some(Default::default())) + } + + pub fn update_with_auth_ext Option>>>(&self, auth_str: &str, ext_f: F) -> bool { let mut lock = self.state.write().unwrap(); match &*lock { InitState::Unauthenticated(auth) => { @@ -21,7 +25,12 @@ impl UserState { token: splits[1].to_owned(), refresh_token: splits[2].to_owned(), }; - match auth.authenticate(token) { + let ext = if let Some(ext) = ext_f(&token) { + ext + } else { + return false; + }; + match auth.authenticate(token, ext) { Ok(user) => { *lock = InitState::Authenticated(std::sync::Arc::new(user)); true diff --git a/utils/cube_gen.py b/utils/cube_gen.py index 6caa81a..a22321f 100644 --- a/utils/cube_gen.py +++ b/utils/cube_gen.py @@ -445,6 +445,13 @@ def main(asset_in, cubes=None, weapons=None, movement=None): ], }, }, + "chat": { + "public_channels": [ + "main", + "sys", + "jam_club", + ], + }, "settings": { "banners": [{ "message": msg,