forked from lavina/lavina
1
0
Fork 0

dialogs impl

This commit is contained in:
Nikita Vilunov 2024-04-22 20:27:33 +02:00
parent b80401bcdd
commit 772d38f7bb
6 changed files with 174 additions and 6 deletions

View File

@ -1,14 +1,17 @@
create table dialogs( create table dialogs(
id integer primary key autoincrement not null,
participant_1 integer not null, participant_1 integer not null,
participant_2 integer not null, participant_2 integer not null,
created_at timestamp not null, created_at timestamp not null,
message_count integer not null default 0, message_count integer not null default 0,
primary key (participant_1, participant_2) unique (participant_1, participant_2)
); );
create table dialog_messages( create table dialog_messages(
dialog_id integer not null, dialog_id integer not null,
id integer not null, -- unique per dialog, sequential in one dialog id integer not null, -- unique per dialog, sequential in one dialog
author_id integer not null,
content string not null, content string not null,
created_at timestamp not null,
primary key (dialog_id, id) primary key (dialog_id, id)
); );

View File

@ -8,7 +8,8 @@ use std::sync::Arc;
use tokio::sync::RwLock as AsyncRwLock; use tokio::sync::RwLock as AsyncRwLock;
use crate::player::PlayerId; use crate::player::{PlayerId, PlayerRegistry, Updates};
use crate::prelude::*;
use crate::repo::Storage; use crate::repo::Storage;
/// Id of a conversation between two players. /// Id of a conversation between two players.
@ -36,25 +37,84 @@ impl DialogId {
struct Dialog { struct Dialog {
storage_id: u32, storage_id: u32,
id: DialogId, player_storage_id_1: u32,
player_storage_id_2: u32,
message_count: u32, message_count: u32,
} }
struct DialogRegistryInner { struct DialogRegistryInner {
dialogs: HashMap<DialogId, AsyncRwLock<Dialog>>, dialogs: HashMap<DialogId, AsyncRwLock<Dialog>>,
players: Option<PlayerRegistry>,
storage: Storage, storage: Storage,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct DialogRegistry(Arc<AsyncRwLock<DialogRegistryInner>>); pub struct DialogRegistry(Arc<AsyncRwLock<DialogRegistryInner>>);
impl DialogRegistry {
pub async fn send_message(&self, from: PlayerId, to: PlayerId, body: Str) -> Result<()> {
let guard = self.0.read().await;
let id = DialogId::new(from.clone(), to.clone());
let dialog = guard.dialogs.get(&id);
if let Some(d) = dialog {
let mut d = d.write().await;
guard.storage.increment_dialog_message_count(d.storage_id).await?;
d.message_count += 1;
let Some(players) = &guard.players else {
tracing::error!("No player registry present");
return Ok(());
};
let Some(player) = players.get_player(&to).await else {
tracing::debug!("Player {to:?} not active, not sending message");
return Ok(());
};
let update = Updates::NewDialogMessage {
sender: from.clone(),
receiver: to.clone(),
body: body.clone(),
created_at: chrono::Utc::now(), // todo
};
player.update(update).await;
return Ok(());
}
drop(guard);
let mut guard = self.0.write().await;
let (p1, p2) = id.as_inner();
tracing::info!("Dialog {id:?} not found locally, trying to load from storage");
let stored_dialog = match guard.storage.retrieve_dialog(p1.as_inner(), p2.as_inner()).await? {
Some(t) => t,
None => {
tracing::info!("Dialog {id:?} does not exist, creating a new one in storage");
guard.storage.initialize_dialog(p1.as_inner(), p2.as_inner()).await?
}
};
tracing::info!("Dialog {id:?} loaded");
guard.storage.increment_dialog_message_count(stored_dialog.storage_id).await?;
let dialog = Dialog {
storage_id: stored_dialog.storage_id,
player_storage_id_1: stored_dialog.participant_1,
player_storage_id_2: stored_dialog.participant_2,
message_count: stored_dialog.message_count + 1,
};
guard.dialogs.insert(id.clone(), AsyncRwLock::new(dialog));
// TODO send message to the other player and persist it
return Ok(());
}
}
impl DialogRegistry { impl DialogRegistry {
pub fn new(storage: Storage) -> DialogRegistry { pub fn new(storage: Storage) -> DialogRegistry {
DialogRegistry(Arc::new(AsyncRwLock::new(DialogRegistryInner { DialogRegistry(Arc::new(AsyncRwLock::new(DialogRegistryInner {
dialogs: HashMap::new(), dialogs: HashMap::new(),
players: None,
storage, storage,
}))) })))
} }
pub async fn set_players(&self, players: PlayerRegistry) {
let mut guard = self.0.write().await;
guard.players = Some(players);
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -27,8 +27,9 @@ impl LavinaCore {
pub async fn new(mut metrics: MetricsRegistry, storage: Storage) -> Result<LavinaCore> { pub async fn new(mut metrics: MetricsRegistry, storage: Storage) -> Result<LavinaCore> {
// TODO shutdown all services in reverse order on error // TODO shutdown all services in reverse order on error
let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let rooms = RoomRegistry::new(&mut metrics, storage.clone())?;
let players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics)?;
let dialogs = DialogRegistry::new(storage.clone()); let dialogs = DialogRegistry::new(storage.clone());
let players = PlayerRegistry::empty(rooms.clone(), dialogs.clone(), storage.clone(), &mut metrics)?;
dialogs.set_players(players.clone()).await;
Ok(LavinaCore { Ok(LavinaCore {
players, players,
rooms, rooms,

View File

@ -16,6 +16,7 @@ use serde::Serialize;
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::dialog::DialogRegistry;
use crate::prelude::*; use crate::prelude::*;
use crate::repo::Storage; use crate::repo::Storage;
use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry}; use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry};
@ -174,6 +175,11 @@ pub enum ClientCommand {
GetRooms { GetRooms {
promise: Promise<Vec<RoomInfo>>, promise: Promise<Vec<RoomInfo>>,
}, },
SendDialogMessage {
recipient: PlayerId,
body: Str,
promise: Promise<()>,
},
} }
pub enum JoinResult { pub enum JoinResult {
@ -210,6 +216,12 @@ pub enum Updates {
}, },
/// The player was banned from the room and left it immediately. /// The player was banned from the room and left it immediately.
BannedFrom(RoomId), BannedFrom(RoomId),
NewDialogMessage {
sender: PlayerId,
receiver: PlayerId,
body: Str,
created_at: DateTime<Utc>,
},
} }
/// Handle to a player registry — a shared data structure containing information about players. /// Handle to a player registry — a shared data structure containing information about players.
@ -218,6 +230,7 @@ pub struct PlayerRegistry(Arc<RwLock<PlayerRegistryInner>>);
impl PlayerRegistry { impl PlayerRegistry {
pub fn empty( pub fn empty(
room_registry: RoomRegistry, room_registry: RoomRegistry,
dialogs: DialogRegistry,
storage: Storage, storage: Storage,
metrics: &mut MetricsRegistry, metrics: &mut MetricsRegistry,
) -> Result<PlayerRegistry> { ) -> Result<PlayerRegistry> {
@ -225,19 +238,25 @@ impl PlayerRegistry {
metrics.register(Box::new(metric_active_players.clone()))?; metrics.register(Box::new(metric_active_players.clone()))?;
let inner = PlayerRegistryInner { let inner = PlayerRegistryInner {
room_registry, room_registry,
dialogs,
storage, storage,
players: HashMap::new(), players: HashMap::new(),
metric_active_players, metric_active_players,
}; };
Ok(PlayerRegistry(Arc::new(RwLock::new(inner)))) Ok(PlayerRegistry(Arc::new(RwLock::new(inner))))
} }
pub async fn get_player(&self, id: &PlayerId) -> Option<PlayerHandle> {
let inner = self.0.read().await;
inner.players.get(id).map(|(handle, _)| handle.clone())
}
pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle { pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle {
let mut inner = self.0.write().await; let mut inner = self.0.write().await;
if let Some((handle, _)) = inner.players.get(id) { if let Some((handle, _)) = inner.players.get(id) {
handle.clone() handle.clone()
} else { } else {
let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone(), inner.storage.clone()).await; let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone(), inner.dialogs.clone(), inner.storage.clone()).await;
inner.players.insert(id.clone(), (handle.clone(), fiber)); inner.players.insert(id.clone(), (handle.clone(), fiber));
inner.metric_active_players.inc(); inner.metric_active_players.inc();
handle handle
@ -265,6 +284,7 @@ impl PlayerRegistry {
/// The player registry state representation. /// The player registry state representation.
struct PlayerRegistryInner { struct PlayerRegistryInner {
room_registry: RoomRegistry, room_registry: RoomRegistry,
dialogs: DialogRegistry,
storage: Storage, storage: Storage,
/// Active player actors. /// Active player actors.
players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>, players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>,
@ -281,10 +301,11 @@ struct Player {
rx: Receiver<ActorCommand>, rx: Receiver<ActorCommand>,
handle: PlayerHandle, handle: PlayerHandle,
rooms: RoomRegistry, rooms: RoomRegistry,
dialogs: DialogRegistry,
storage: Storage, storage: Storage,
} }
impl Player { impl Player {
async fn launch(player_id: PlayerId, rooms: RoomRegistry, storage: Storage) -> (PlayerHandle, JoinHandle<Player>) { async fn launch(player_id: PlayerId, rooms: RoomRegistry, dialogs: DialogRegistry, storage: Storage) -> (PlayerHandle, JoinHandle<Player>) {
let (tx, rx) = channel(32); let (tx, rx) = channel(32);
let handle = PlayerHandle { tx }; let handle = PlayerHandle { tx };
let handle_clone = handle.clone(); let handle_clone = handle.clone();
@ -301,6 +322,7 @@ impl Player {
rx, rx,
handle, handle,
rooms, rooms,
dialogs,
storage, storage,
}; };
let fiber = tokio::task::spawn(player.main_loop()); let fiber = tokio::task::spawn(player.main_loop());
@ -389,6 +411,14 @@ impl Player {
let result = self.get_rooms().await; let result = self.get_rooms().await;
let _ = promise.send(result); let _ = promise.send(result);
} }
ClientCommand::SendDialogMessage {
recipient,
body,
promise,
} => {
self.send_dialog_message(connection_id, recipient, body).await;
let _ = promise.send(());
}
} }
} }
@ -467,6 +497,18 @@ impl Player {
response response
} }
async fn send_dialog_message(&self, connection_id: ConnectionId, recipient: PlayerId, body: Str) {
let created_at = chrono::Utc::now();
self.dialogs.send_message(self.player_id.clone(), recipient.clone(), body.clone()).await.unwrap();
let update = Updates::NewDialogMessage {
sender: self.player_id.clone(),
receiver: recipient.clone(),
body,
created_at,
};
self.broadcast_update(update, connection_id).await;
}
/// Broadcasts an update to all connections except the one with the given id. /// Broadcasts an update to all connections except the one with the given id.
/// ///
/// This is called after handling a client command. /// This is called after handling a client command.

View File

@ -0,0 +1,61 @@
use anyhow::Result;
use sqlx::FromRow;
use crate::repo::Storage;
impl Storage {
pub async fn retrieve_dialog(&self, participant_1: &str, participant_2: &str) -> Result<Option<StoredDialog>> {
let mut executor = self.conn.lock().await;
let res = sqlx::query_as(
"select r.participant_1, r.participant_2, r.message_count
from rooms r join users u1 on r.participant_1 = u1.id join users u2 on r.participant_2 = u2.id
where u1.name = ? and u2.name = ?;",
)
.bind(participant_1)
.bind(participant_2)
.fetch_optional(&mut *executor)
.await?;
Ok(res)
}
pub async fn increment_dialog_message_count(&self, storage_id: u32) -> Result<()> {
let mut executor = self.conn.lock().await;
sqlx::query(
"update rooms set message_count = message_count + 1
where storage_id = ?;",
)
.bind(storage_id)
.execute(&mut *executor)
.await?;
Ok(())
}
pub async fn initialize_dialog(&self, participant_1: &str, participant_2: &str) -> Result<StoredDialog> {
let mut executor = self.conn.lock().await;
let res: StoredDialog = sqlx::query_as(
"insert into dialogs(participant_1, participant_2, created_at)
values (
select id from users where name = ?,
select id from users where name = ?,
now()
)
returning id, participant_1, participant_2, message_count;",
)
.bind(participant_1)
.bind(participant_2)
.fetch_one(&mut *executor)
.await?;
Ok(res)
}
}
#[derive(FromRow)]
pub struct StoredDialog {
pub storage_id: u32,
pub participant_1: u32,
pub participant_2: u32,
pub message_count: u32,
}

View File

@ -12,6 +12,7 @@ use tokio::sync::Mutex;
use crate::prelude::*; use crate::prelude::*;
mod dialog;
mod room; mod room;
mod user; mod user;