1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Implement basic chat functionality

This commit is contained in:
NGnius (Graham)
2025-04-14 21:17:41 -04:00
parent 4d2ddaeafb
commit 792a6362e3
37 changed files with 1119 additions and 34 deletions

3
Cargo.lock generated
View File

@@ -1834,6 +1834,9 @@ dependencies = [
"polariton_auth", "polariton_auth",
"polariton_server", "polariton_server",
"rc_core", "rc_core",
"regex",
"serde",
"serde_json",
"tokio", "tokio",
] ]

View File

@@ -0,0 +1,18 @@
{
"commands": [
{
"regex": "\\?online",
"op": {
"type": "BuiltIn",
"built_in": "OnlineUsers"
}
},
{
"regex": "\\?users",
"op": {
"type": "BuiltIn",
"built_in": "TotalUsers"
}
}
]
}

View File

@@ -14416,6 +14416,13 @@
] ]
} }
}, },
"chat": {
"public_channels": [
"main",
"sys",
"jam_club"
]
},
"settings": { "settings": {
"banners": [ "banners": [
{ {
@@ -14476,4 +14483,4 @@
} }
] ]
} }
} }

View File

@@ -16,3 +16,6 @@ polariton.workspace = true
polariton_auth = { version = "*", path = "../polariton_auth" } polariton_auth = { version = "*", path = "../polariton_auth" }
polariton_server.workspace = true polariton_server.workspace = true
rc_core = { version = "*", path = "../rc_core" } rc_core = { version = "*", path = "../rc_core" }
serde.workspace = true
serde_json.workspace = true
regex = "1"

View File

@@ -44,7 +44,7 @@ impl ChatChannelMember {
#[allow(dead_code)] #[allow(dead_code)]
#[repr(u8)] #[repr(u8)]
#[derive(Copy, Clone)] #[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
pub enum ChatChannelType { pub enum ChatChannelType {
None = 0, None = 0,
Public = 1, Public = 1,
@@ -57,6 +57,23 @@ pub enum ChatChannelType {
CustomGame = 8, CustomGame = 8,
} }
impl ChatChannelType {
pub fn from_u8(num: u8) -> Result<Self, i16> {
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)] #[allow(dead_code)]
#[repr(u8)] #[repr(u8)]
#[derive(Copy, Clone)] #[derive(Copy, Clone)]

View File

@@ -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<C>(&self) -> polariton::operation::ParameterTable<C> {
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<C>(&self) -> polariton::operation::ParameterTable<C> {
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()
}
}

View File

@@ -0,0 +1 @@
pub mod chat_message;

View File

@@ -1,8 +1,13 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
mod cli; mod cli;
mod state;
mod persist;
mod op_handler;
pub use op_handler::SimpleChatFunc;
mod data; mod data;
mod operations; mod operations;
mod events;
use polariton_auth::Handshake; use polariton_auth::Handshake;
use tokio::net; 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 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 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"); let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");

View File

@@ -0,0 +1,52 @@
use polariton::operation::{ParameterTable, OperationResponse};
use polariton_server::operations::{Operation, OperationCode};
pub struct SimpleChatFunc<const CODE: u8, U: Send + Sync, F: (Fn(ParameterTable<C>, &U, &crate::state::ChatImpl) -> Result<ParameterTable<C>, i16>) + Send + Sync, C: Send + Sync + 'static = ()> {
_user_ty: std::marker::PhantomData<U>,
_custom_ty: std::marker::PhantomData<C>,
chat: crate::state::ChatImpl,
func: F,
}
impl <C: Send + Sync + 'static, const CODE: u8, U: Send + Sync, F: (Fn(ParameterTable<C>, &U, &crate::state::ChatImpl) -> Result<ParameterTable<C>, i16>) + Send + Sync> SimpleChatFunc<CODE, U, F, C> {
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 <C: Send + Sync + 'static, const CODE: u8, U: Send + Sync, F: (Fn(ParameterTable<C>, &U, &crate::state::ChatImpl) -> Result<ParameterTable<C>, i16>) + Send + Sync> Operation<C> for SimpleChatFunc<CODE, U, F, C> {
type User = U;
fn handle(&self, p: polariton::operation::ParameterTable<C>, u: &Self::User) -> OperationResponse<C> {
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 <C: Send + Sync + 'static, const CODE: u8, U: Send + Sync, F: (Fn(ParameterTable<C>, &U, &crate::state::ChatImpl) -> Result<ParameterTable<C>, i16>) + Send + Sync> OperationCode for SimpleChatFunc<CODE, U, F, C> {
fn op_code() -> u8 {
CODE
}
}

View File

@@ -1,14 +1,17 @@
use polariton_server::operations::SimpleFunc; use crate::SimpleChatFunc;
use polariton::operation::{ParameterTable, Typed, Arr}; use crate::persist::chat_user::ChatUser;
use polariton::operation::ParameterTable;
use crate::data::channel::*;
const PARAM_KEY: u8 = 18; const PARAM_KEY: u8 = 18;
pub(super) fn all_channels_provider() -> SimpleFunc<11, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> { pub(super) fn all_channels_provider(chat_system: crate::state::ChatImpl) -> SimpleChatFunc<11, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy, &crate::state::ChatImpl) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| { SimpleChatFunc::new(|params, user: &crate::UserTy, _chat_system: &crate::state::ChatImpl| {
let mut params = params.to_dict(); 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::<ChatUserImpl>()).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 ty: polariton::serdes::TypePrefix::HashMap, // hashtable
items: vec![ items: vec![
ChatChannelInfo { ChatChannelInfo {
@@ -52,7 +55,7 @@ pub(super) fn all_channels_provider() -> SimpleFunc<11, crate::UserTy, impl (Fn(
channel_ty: ChatChannelType::Custom, channel_ty: ChatChannelType::Custom,
}.as_transmissible(), }.as_transmissible(),
], ],
})); }));*/
Ok(params.into()) Ok(params.into())
}) }, chat_system)
} }

View File

@@ -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<ParameterTable, i16>) + 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<ParameterTable, i16>) + 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)
}

View File

@@ -2,15 +2,29 @@ mod more_auth;
mod chat_ignores; mod chat_ignores;
mod pending_sanctions; mod pending_sanctions;
mod all_joined_channels; mod all_joined_channels;
mod send_message;
mod public_channels;
mod join_channel;
mod user_online;
use polariton_server::operations::OperationsHandler; use polariton_server::operations::OperationsHandler;
pub fn handler() -> OperationsHandler<crate::UserTy> { pub fn handler(chat_system: crate::state::chat::ChatImpl, data_root: impl AsRef<std::path::Path>, conf: &rc_core::persist::config::ConfigImpl) -> OperationsHandler<crate::UserTy> {
OperationsHandler::new() OperationsHandler::new()
.add(more_auth::MoreLobbyAuth) .add(more_auth::MoreLobbyAuth::new(chat_system.clone(), data_root))
.add(chat_ignores::ignores_provider()) .add(chat_ignores::ignores_provider())
.add(pending_sanctions::pending_sanctions_checker()) .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(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()) //.add(polariton_server::operations::Ack::<00000, _>::default())
} }
pub(self) fn get_chat_user<'a, C>(user: &'a dyn rc_core::persist::user::User<C>) -> &'a crate::persist::chat_user::ChatUserImpl {
user.ext(std::any::TypeId::of::<crate::persist::chat_user::ChatUserImpl>()).unwrap().downcast_ref().unwrap()
}

View File

@@ -1,10 +1,52 @@
use polariton::operation::Typed; use polariton::operation::Typed;
use polariton_server::operations::{Operation, OperationCode}; 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 { impl MoreLobbyAuth {
const AUTH_PAYLOAD_KEY: u8 = 245; const AUTH_PAYLOAD_KEY: u8 = 245;
pub fn new(chat_system: crate::state::chat::ChatImpl, root: impl AsRef<std::path::Path>) -> Self {
Self {
chat_system,
root: root.as_ref().to_path_buf(),
}
}
fn build_ext_map(&self, token: &rc_core::persist::user::UserToken) -> Option<std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>> {
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::<ChatUserImpl>(), Box::new(data) as _);
Some(map)
}
fn do_auth<C>(&self, params: std::collections::HashMap<u8, Typed<C>>, user: &crate::UserTy) -> Result<polariton::operation::ParameterTable<C>, 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 <C: Send + 'static> Operation<C> for MoreLobbyAuth { impl <C: Send + 'static> Operation<C> for MoreLobbyAuth {
@@ -12,24 +54,24 @@ impl <C: Send + 'static> Operation<C> for MoreLobbyAuth {
fn handle(&self, params: polariton::operation::ParameterTable<C>, user: &Self::User) -> polariton::operation::OperationResponse<C> { fn handle(&self, params: polariton::operation::ParameterTable<C>, user: &Self::User) -> polariton::operation::OperationResponse<C> {
let params_dict = params.to_dict(); let params_dict = params.to_dict();
if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) { match self.do_auth(params_dict, user) {
if user.update_with_auth(&auth_payload.string) { Ok(params) => {
let mut resp_params = std::collections::HashMap::new(); polariton::operation::OperationResponse {
resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); code: Self::op_code(),
return polariton::operation::OperationResponse {
code: 230,
return_code: 0, return_code: 0,
message: polariton::operation::Typed::Null, 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(),
}
} }
} }

View File

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

View File

@@ -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<ParameterTable, i16>) + 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 {
"<unknown location>".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<ParameterTable, i16>) + 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 {
"<unknown location>".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)
}

View File

@@ -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<ParameterTable, i16>) + 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)
}

View File

@@ -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<ChatUserData>,
root: std::path::PathBuf,
}
impl ChatUserInfo {
pub fn load(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
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<std::path::Path>) -> 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<String>,
}
impl ChatUserData {
fn load(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
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::path::Path>) -> 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<String> {
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
}
}

View File

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

View File

@@ -0,0 +1,8 @@
use polariton::operation::Typed;
pub trait ChatUser {
fn subscribed_channels(&self) -> Typed<()>;
fn subscribed_channels_strings(&self) -> Vec<String>;
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;
}

View File

@@ -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<ChatCommand>,
}
fn default_command_chann() -> String {
"sys".to_owned()
}
impl ChatSystemConfig {
pub fn load(asset_root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
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,
}

View File

@@ -0,0 +1,4 @@
mod chat;
pub use chat::{ChatSystemConfig, ChatCommand, ChatOperation, BuiltInChatOperation};
pub const CHAT_CONFIG_FILE: &str = "chat.json";

View File

@@ -0,0 +1,2 @@
pub mod chat_user;
pub mod config;

View File

@@ -0,0 +1,180 @@
use std::collections::HashMap;
#[derive(Clone)]
pub struct ChatProvider {
chat_system: std::sync::Arc<std::sync::RwLock<crate::state::chat::ChatSystem>>,
}
impl ChatProvider {
pub fn new(asset_root: impl AsRef<std::path::Path>, data_root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
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<String, super::ChatRoom>,
online_users: HashMap<String, super::UserHandle>,
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<String>, event_tx: tokio::sync::mpsc::UnboundedSender<polariton_server::ToSend>) {
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<std::path::Path>, data_root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
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)
}
}

View File

@@ -0,0 +1,144 @@
pub struct ChatSystemConfig {
command_channel: String,
commands: Vec<ChatCommand>,
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<Self> {
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<Self, regex::Error> {
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<String> {
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)
}
},
}
}
}

View File

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

View File

@@ -0,0 +1,92 @@
pub struct ChatRoom {
name: String,
channel: crate::data::channel::ChatChannelType,
online_users: Vec<super::UserHandle>,
}
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
}
}
}

View File

@@ -0,0 +1,43 @@
#[derive(Clone)]
pub struct UserHandle {
display_name: String,
event_tx: tokio::sync::mpsc::WeakUnboundedSender<polariton_server::ToSend>,
}
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<polariton_server::ToSend>, 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
}
}

View File

@@ -0,0 +1,2 @@
pub mod chat;
pub use chat::ChatImpl;

View File

@@ -36,3 +36,27 @@ pub enum WebServicesError {
UsernameTooShort = 206, UsernameTooShort = 206,
SaleEnded = 207 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,
}

View File

@@ -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<String>,
}
fn default_pub_channs() -> Vec<String> {
vec![
"main".to_owned(),
"sys".to_owned(),
"openjam_worship".to_owned(),
]
}

View File

@@ -5,7 +5,7 @@ use serde::{Serialize, Deserialize};
use polariton::operation::{Typed, Dict}; use polariton::operation::{Typed, Dict};
use polariton::serdes::TypePrefix; 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"; const CUBE_CONFIG_FILENAME: &str = "config.json";
@@ -15,6 +15,7 @@ pub struct CubeConfig {
movement: HashMap<ItemCategory, MovementCategoryData>, movement: HashMap<ItemCategory, MovementCategoryData>,
lerp_value: f32, lerp_value: f32,
battle: BattleConfig, battle: BattleConfig,
chat: ChatConfig,
settings: Settings, settings: Settings,
} }
@@ -247,4 +248,11 @@ impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
fn login_messages(&self) -> super::DevMessageProvider<C> { fn login_messages(&self) -> super::DevMessageProvider<C> {
super::DevMessageProvider::new(self.settings.banners.iter().map(|msg| (msg.message.clone(), msg.duration as i32)).collect()) super::DevMessageProvider::new(self.settings.banners.iter().map(|msg| (msg.message.clone(), msg.duration as i32)).collect())
} }
fn public_channels(&self) -> Typed<C> {
Typed::Arr(polariton::operation::Arr {
ty: TypePrefix::Str,
items: self.chat.public_channels.iter().map(|s| Typed::Str(s.into())).collect(),
})
}
} }

View File

@@ -17,6 +17,7 @@ pub trait ConfigProvider<C: Clone> {
fn campaign_details(&self) -> CompleteCampaignProvider; fn campaign_details(&self) -> CompleteCampaignProvider;
fn client_config(&self) -> Typed<C>; fn client_config(&self) -> Typed<C>;
fn login_messages(&self) -> DevMessageProvider<C>; fn login_messages(&self) -> DevMessageProvider<C>;
fn public_channels(&self) -> Typed<C>;
} }
pub struct CompleteCampaignProvider { pub struct CompleteCampaignProvider {

View File

@@ -29,6 +29,9 @@ pub use client_config::GameplaySettings;
mod settings; mod settings;
pub use settings::Settings; pub use settings::Settings;
mod chat;
pub use chat::ChatConfig;
pub(self) const VALID_ROBOT: &[u8] = &[64, pub(self) const VALID_ROBOT: &[u8] = &[64,
0, 0,
0, 0,

View File

@@ -34,7 +34,7 @@ impl AccountProvider {
} }
impl <C: Clone> super::UserProvider<C> for AccountProvider { impl <C: Clone> super::UserProvider<C> for AccountProvider {
fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + Send + Sync>, String> { fn authenticate(&self, token: super::UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn super::User<C> + Send + Sync>, String> {
let new_root = self.root.join(&token.uuid); let new_root = self.root.join(&token.uuid);
let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret);
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
@@ -46,6 +46,7 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
token, token,
account: account_info, account: account_info,
cubes: self.cubes.clone(), cubes: self.cubes.clone(),
extensions: ext,
})) }))
//Err("Unable to authenticate".to_string()) //Err("Unable to authenticate".to_string())
} }
@@ -124,6 +125,7 @@ struct UserData {
token: super::UserToken, token: super::UserToken,
account: AccountInfo, account: AccountInfo,
cubes: std::sync::Arc<Vec<u32>>, cubes: std::sync::Arc<Vec<u32>>,
extensions: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>,
} }
impl UserData { impl UserData {
@@ -159,6 +161,10 @@ const INVALID_ROBOT_ERR: i16 = 140;
const DATABASE_ERR: i16 = 8; const DATABASE_ERR: i16 = 8;
impl <C: Clone> super::User<C> for UserData { impl <C: Clone> super::User<C> 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 { fn token(&self) -> &'_ super::UserToken {
&self.token &self.token
} }
@@ -312,7 +318,6 @@ impl <C: Clone> super::User<C> for UserData {
], ],
}.as_transmissible()) }.as_transmissible())
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]

View File

@@ -26,7 +26,7 @@ pub struct UserLoginInfo {
} }
pub trait UserProvider<C> { pub trait UserProvider<C> {
fn authenticate(&self, user: UserToken) -> Result<Box<dyn User<C> + Send + Sync>, String>; fn authenticate(&self, user: UserToken, ext: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>) -> Result<Box<dyn User<C> + Send + Sync>, String>;
} }
pub trait UserAuthenticator { pub trait UserAuthenticator {
@@ -34,6 +34,7 @@ pub trait UserAuthenticator {
} }
pub trait User<C> { pub trait User<C> {
fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)>;
fn token(&self) -> &'_ super::UserToken; fn token(&self) -> &'_ super::UserToken;
fn is_mod(&self) -> bool; fn is_mod(&self) -> bool;
fn is_admin(&self) -> bool; fn is_admin(&self) -> bool;

View File

@@ -8,6 +8,10 @@ pub struct UserState<C: Clone = ()> {
impl <C: Clone> UserState<C> { impl <C: Clone> UserState<C> {
pub fn update_with_auth(&self, auth_str: &str) -> bool { 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<F: FnOnce(&crate::persist::user::UserToken) -> Option<std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>>>(&self, auth_str: &str, ext_f: F) -> bool {
let mut lock = self.state.write().unwrap(); let mut lock = self.state.write().unwrap();
match &*lock { match &*lock {
InitState::Unauthenticated(auth) => { InitState::Unauthenticated(auth) => {
@@ -21,7 +25,12 @@ impl <C: Clone> UserState<C> {
token: splits[1].to_owned(), token: splits[1].to_owned(),
refresh_token: splits[2].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) => { Ok(user) => {
*lock = InitState::Authenticated(std::sync::Arc::new(user)); *lock = InitState::Authenticated(std::sync::Arc::new(user));
true true

View File

@@ -445,6 +445,13 @@ def main(asset_in, cubes=None, weapons=None, movement=None):
], ],
}, },
}, },
"chat": {
"public_channels": [
"main",
"sys",
"jam_club",
],
},
"settings": { "settings": {
"banners": [{ "banners": [{
"message": msg, "message": msg,