//! Domain of chat participants. //! //! Player is a single user account, which is used to participate in chats, //! including sending messages, receiving messaged, retrieving history and running privileged actions. //! A player corresponds to a single user account. Usually a person has only one account, //! but it is possible to have multiple accounts for one person and therefore multiple player entities. //! //! 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, sync::{Arc, RwLock}, }; use prometheus::{IntGauge, Registry as MetricsRegistry}; use tokio::{ sync::mpsc::{channel, Receiver, Sender}, sync::oneshot::{channel as oneshot, Sender as OneshotSender}, task::JoinHandle, }; use crate::{ core::room::{RoomId, RoomRegistry}, prelude::*, util::table::{AnonTable, Key as AnonKey}, }; /// Opaque player identifier. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct PlayerId(pub ByteVec); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ConnectionId(pub AnonKey); pub struct PlayerConnection { pub connection_id: ConnectionId, pub receiver: Receiver, player_handle: PlayerHandle, } impl PlayerConnection { pub async fn send_message(&mut self, room_id: RoomId, body: String) { self.player_handle .send_message(room_id, self.connection_id.clone(), body) .await } pub async fn join_room(&mut self, room_id: RoomId) { self.player_handle.join_room(room_id).await } } /// Handle to a player actor. #[derive(Clone)] pub struct PlayerHandle { tx: Sender, } impl PlayerHandle { pub async fn subscribe(&mut self) -> PlayerConnection { let (sender, receiver) = channel(32); let (promise, deferred) = oneshot(); self.tx .send(PlayerCommand::AddSocket { sender, promise }) .await; let connection_id = deferred.await.unwrap(); PlayerConnection { connection_id, player_handle: self.clone(), receiver, } } pub async fn send_message( &mut self, room_id: RoomId, connection_id: ConnectionId, body: String, ) { self.tx .send(PlayerCommand::SendMessage { room_id, connection_id, body, }) .await; } pub async fn join_room(&mut self, room_id: RoomId) { self.tx.send(PlayerCommand::JoinRoom { room_id }).await; } pub async fn receive_message( &mut self, room_id: RoomId, author: PlayerId, connection_id: ConnectionId, body: String, ) { self.tx .send(PlayerCommand::IncomingMessage { room_id, author, connection_id, body, }) .await; } } /// Player update event type which is sent to a connection handler. pub enum Updates { RoomJoined { room_id: RoomId, }, NewMessage { author_id: PlayerId, connection_id: ConnectionId, room_id: RoomId, body: String, }, } enum PlayerCommand { AddSocket { sender: Sender, promise: OneshotSender, }, JoinRoom { room_id: RoomId, }, SendMessage { room_id: RoomId, connection_id: ConnectionId, body: String, }, IncomingMessage { room_id: RoomId, connection_id: ConnectionId, author: PlayerId, body: String, }, } /// Handle to a player registry — a shared data structure containing information about players. #[derive(Clone)] pub struct PlayerRegistry(Arc>); impl PlayerRegistry { pub fn empty( room_registry: RoomRegistry, metrics: &mut MetricsRegistry, ) -> Result { let metric_active_players = IntGauge::new("chat_players_active", "Number of alive player actors")?; metrics.register(Box::new(metric_active_players.clone()))?; let inner = PlayerRegistryInner { room_registry, players: HashMap::new(), metric_active_players, }; Ok(PlayerRegistry(Arc::new(RwLock::new(inner)))) } pub async fn get_or_create_player(&mut self, id: PlayerId) -> PlayerHandle { let player = Player { sockets: AnonTable::new(), }; let mut inner = self.0.write().unwrap(); if let Some((handle, _)) = inner.players.get(&id) { handle.clone() } else { let (handle, fiber) = player.launch(id.clone(), inner.room_registry.clone()); inner.players.insert(id, (handle.clone(), fiber)); inner.metric_active_players.inc(); handle } } pub async fn connect_to_player(&mut self, id: PlayerId) -> PlayerConnection { let mut player_handle = self.get_or_create_player(id).await; player_handle.subscribe().await } } /// The player registry state representation. struct PlayerRegistryInner { room_registry: RoomRegistry, players: HashMap)>, metric_active_players: IntGauge, } /// Player actor inner state representation. struct Player { sockets: AnonTable>, } impl Player { fn launch( mut self, player_id: PlayerId, mut rooms: RoomRegistry, ) -> (PlayerHandle, JoinHandle) { let (tx, mut rx) = channel(32); let handle = PlayerHandle { tx }; let handle_clone = handle.clone(); let fiber = tokio::task::spawn(async move { while let Some(cmd) = rx.recv().await { match cmd { PlayerCommand::AddSocket { sender, promise } => { let connection_id = self.sockets.insert(sender); promise.send(ConnectionId(connection_id)); } PlayerCommand::JoinRoom { room_id } => { let mut room = rooms.get_or_create_room(room_id); room.subscribe(player_id.clone(), handle.clone()).await; } PlayerCommand::SendMessage { room_id, connection_id, body, } => { let room = rooms.get_room(room_id); match room { Some(mut room) => { room.send_message(player_id.clone(), connection_id, body) .await; } None => { tracing::info!("no room found"); } } } PlayerCommand::IncomingMessage { room_id, author, connection_id, body, } => { tracing::info!("Handling incoming message"); for socket in &self.sockets { log::info!("Send message to socket"); socket .send(Updates::NewMessage { author_id: author.clone(), connection_id: connection_id.clone(), room_id: room_id.clone(), body: body.clone(), }) .await; } } } } self }); (handle_clone, fiber) } }