From e813fb7c697881ac2852cd529464f71a2aa46c3f Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Thu, 16 Feb 2023 22:49:17 +0100 Subject: [PATCH] implement room bans --- src/core/player.rs | 35 +++++++++++++++++++++++----- src/projections/irc/mod.rs | 47 +++++++++++++++++++++++++++++++------- src/protos/irc/client.rs | 2 +- src/protos/irc/server.rs | 23 +++++++++++++++---- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/core/player.rs b/src/core/player.rs index 896a9ec..5d25362 100644 --- a/src/core/player.rs +++ b/src/core/player.rs @@ -8,7 +8,7 @@ //! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor, //! so that they don't overload the room actor. use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, sync::{Arc, RwLock}, }; @@ -58,7 +58,7 @@ impl PlayerConnection { .await } - pub async fn join_room(&mut self, room_id: RoomId) -> Result { + pub async fn join_room(&mut self, room_id: RoomId) -> Result { self.player_handle .join_room(room_id, self.connection_id.clone()) .await @@ -96,7 +96,9 @@ impl PlayerConnection { pub async fn get_rooms(&self) -> Result> { let (promise, deferred) = oneshot(); - self.player_handle.send(PlayerCommand::GetRooms(promise)).await; + self.player_handle + .send(PlayerCommand::GetRooms(promise)) + .await; Ok(deferred.await?) } } @@ -141,7 +143,7 @@ impl PlayerHandle { &self, room_id: RoomId, connection_id: ConnectionId, - ) -> Result { + ) -> Result { let (promise, deferred) = oneshot(); let cmd = Cmd::JoinRoom { room_id, @@ -177,7 +179,7 @@ enum PlayerCommand { pub enum Cmd { JoinRoom { room_id: RoomId, - promise: Promise, + promise: Promise, }, LeaveRoom { room_id: RoomId, @@ -195,6 +197,11 @@ pub enum Cmd { }, } +pub enum JoinResult { + Success(RoomInfo), + Banned, +} + /// Player update event type which is sent to a player actor and from there to a connection handler. #[derive(Clone)] pub enum Updates { @@ -215,6 +222,8 @@ pub enum Updates { room_id: RoomId, former_member_id: PlayerId, }, + /// The player was banned from the room and left it immediately. + BannedFrom(RoomId), } /// Handle to a player registry — a shared data structure containing information about players. @@ -266,6 +275,7 @@ struct Player { player_id: PlayerId, connections: AnonTable>, my_rooms: HashMap, + banned_from: HashSet, rx: Receiver, handle: PlayerHandle, rooms: RoomRegistry, @@ -279,6 +289,7 @@ impl Player { player_id, connections: AnonTable::new(), my_rooms: HashMap::new(), + banned_from: HashSet::from([RoomId::from_bytes(b"empty".to_vec()).unwrap()]), rx, handle, rooms, @@ -312,6 +323,13 @@ impl Player { "Player received an update, broadcasting to {} connections", self.connections.len() ); + match update { + Updates::BannedFrom(ref room_id) => { + self.banned_from.insert(room_id.clone()); + self.my_rooms.remove(room_id); + } + _ => {} + } for (_, connection) in &self.connections { connection.send(update.clone()).await; } @@ -331,12 +349,17 @@ impl Player { async fn handle_cmd(&mut self, cmd: Cmd, connection_id: ConnectionId) { match cmd { Cmd::JoinRoom { room_id, promise } => { + if self.banned_from.contains(&room_id) { + promise.send(JoinResult::Banned); + return; + } + let room = self.rooms.get_or_create_room(room_id.clone()); room.subscribe(self.player_id.clone(), self.handle.clone()) .await; self.my_rooms.insert(room_id.clone(), room.clone()); let room_info = room.get_room_info().await; - promise.send(room_info); + promise.send(JoinResult::Success(room_info)); let update = Updates::RoomJoined { room_id, new_member_id: self.player_id.clone(), diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index bcb699a..5f0fe53 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -7,7 +7,7 @@ use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::oneshot::channel; -use crate::core::player::{PlayerConnection, PlayerId, PlayerRegistry, Updates}; +use crate::core::player::*; use crate::core::room::{RoomId, RoomInfo, RoomRegistry}; use crate::prelude::*; use crate::protos::irc::client::{client_message, ClientMessage}; @@ -320,6 +320,17 @@ async fn handle_update( .await?; writer.flush().await? } + Updates::BannedFrom(room_id) => { + // TODO think about the case when the user was banned, but was not in the room - no need to send PART in this case + ServerMessage { + tags: vec![], + sender: Some(player_id.as_bytes().clone()), + body: ServerMessageBody::Part(Chan::Global(room_id.as_bytes().clone())), + } + .write_async(writer) + .await?; + writer.flush().await? + } } Ok(()) } @@ -453,7 +464,10 @@ async fn handle_incoming_message( ServerMessage { tags: vec![], sender: Some(config.server_name.as_bytes().to_vec()), - body: ServerMessageBody::N221UserModeIs { client: user.nickname.clone(), modes: b"+r".to_vec() }, + body: ServerMessageBody::N221UserModeIs { + client: user.nickname.clone(), + modes: b"+r".to_vec(), + }, } .write_async(writer) .await?; @@ -461,12 +475,12 @@ async fn handle_incoming_message( } else { // TODO send 502 (not 401) if the user is not the sender } - }, + } Recipient::Chan(_) => { // TODO handle chan mode handling - }, + } } - }, + } cmd => { log::warn!("Not implemented handler for client command: {cmd:?}"); } @@ -478,7 +492,11 @@ async fn handle_incoming_message( Ok(()) } -fn user_to_who_msg(config: &ServerConfig, requestor: &RegisteredUser, target_user_nickname: &ByteVec) -> ServerMessageBody { +fn user_to_who_msg( + config: &ServerConfig, + requestor: &RegisteredUser, + target_user_nickname: &ByteVec, +) -> ServerMessageBody { // Username is equal to nickname let mut username = Vec::with_capacity(target_user_nickname.len() + 1); username.push(b'~'); @@ -511,8 +529,21 @@ async fn handle_join( match chan { Chan::Global(chan_name) => { let room_id = RoomId::from_bytes(chan_name.clone())?; - let room_info = user_handle.join_room(room_id).await?; - produce_on_join_cmd_messages(&config, &user, chan, &room_info, writer).await?; + if let JoinResult::Success(room_info) = user_handle.join_room(room_id).await? { + produce_on_join_cmd_messages(&config, &user, chan, &room_info, writer).await?; + } else { + ServerMessage { + tags: vec![], + sender: Some(config.server_name.as_bytes().to_vec()), + body: ServerMessageBody::N474BannedFromChan { + client: user.nickname.clone(), + chan: chan.clone(), + message: b"U dun goofed".to_vec(), + }, + } + .write_async(writer) + .await?; + } writer.flush().await?; } Chan::Local(_) => {} diff --git a/src/protos/irc/client.rs b/src/protos/irc/client.rs index 057fee8..9a316f5 100644 --- a/src/protos/irc/client.rs +++ b/src/protos/irc/client.rs @@ -143,7 +143,7 @@ fn client_message_mode(input: &[u8]) -> IResult<&[u8], ClientMessage> { let (input, _) = tag("MODE ")(input)?; let (input, target) = recipient(input)?; - Ok((input, ClientMessage::Mode { target } )) + Ok((input, ClientMessage::Mode { target })) } fn client_message_who(input: &[u8]) -> IResult<&[u8], ClientMessage> { diff --git a/src/protos/irc/server.rs b/src/protos/irc/server.rs index e449d69..3793501 100644 --- a/src/protos/irc/server.rs +++ b/src/protos/irc/server.rs @@ -124,6 +124,11 @@ pub enum ServerMessageBody { client: ByteVec, chan: Chan, }, + N474BannedFromChan { + client: ByteVec, + chan: Chan, + message: ByteVec, + }, } impl ServerMessageBody { @@ -192,9 +197,7 @@ impl ServerMessageBody { writer.write_all(&hostname).await?; writer.write_all(b" ").await?; writer.write_all(&softname).await?; - writer - .write_all(b" r CFILPQbcefgijklmnopqrstvz") - .await?; + writer.write_all(b" r CFILPQbcefgijklmnopqrstvz").await?; // TODO remove hardcoded modes } ServerMessageBody::N005ISupport { client, params } => { @@ -209,7 +212,7 @@ impl ServerMessageBody { writer.write_all(&client).await?; writer.write_all(b" ").await?; writer.write_all(&modes).await?; - }, + } ServerMessageBody::N315EndOfWho { client, mask, msg } => { writer.write_all(b"315 ").await?; writer.write_all(&client).await?; @@ -279,6 +282,18 @@ impl ServerMessageBody { chan.write_async(writer).await?; writer.write_all(b" :End of /NAMES list").await?; } + ServerMessageBody::N474BannedFromChan { + client, + chan, + message, + } => { + writer.write_all(b"474 ").await?; + writer.write_all(&client).await?; + writer.write_all(b" ").await?; + chan.write_async(writer).await?; + writer.write_all(b" :").await?; + writer.write_all(&message).await?; + } } Ok(()) }