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:
180
rc_chat_room/src/state/chat/chat.rs
Normal file
180
rc_chat_room/src/state/chat/chat.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
144
rc_chat_room/src/state/chat/config.rs
Normal file
144
rc_chat_room/src/state/chat/config.rs
Normal 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)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
13
rc_chat_room/src/state/chat/mod.rs
Normal file
13
rc_chat_room/src/state/chat/mod.rs
Normal 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;
|
||||
92
rc_chat_room/src/state/chat/room.rs
Normal file
92
rc_chat_room/src/state/chat/room.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
rc_chat_room/src/state/chat/user.rs
Normal file
43
rc_chat_room/src/state/chat/user.rs
Normal 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
|
||||
}
|
||||
}
|
||||
2
rc_chat_room/src/state/mod.rs
Normal file
2
rc_chat_room/src/state/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod chat;
|
||||
pub use chat::ChatImpl;
|
||||
Reference in New Issue
Block a user