diff --git a/crates/lavina-core/migrations/3_dialogs.sql b/crates/lavina-core/migrations/3_dialogs.sql new file mode 100644 index 0000000..799628e --- /dev/null +++ b/crates/lavina-core/migrations/3_dialogs.sql @@ -0,0 +1,14 @@ +create table dialogs( + participant_1 integer not null, + participant_2 integer not null, + created_at timestamp not null, + message_count integer not null default 0, + primary key (participant_1, participant_2) +); + +create table dialog_messages( + dialog_id integer not null, + id integer not null, -- unique per dialog, sequential in one dialog + content string not null, + primary key (dialog_id, id) +); diff --git a/crates/lavina-core/src/dialog.rs b/crates/lavina-core/src/dialog.rs new file mode 100644 index 0000000..35157ea --- /dev/null +++ b/crates/lavina-core/src/dialog.rs @@ -0,0 +1,75 @@ +//! Domain of dialogs – conversations between two participants. +//! +//! Dialogs are different from rooms in that they are always between two participants. +//! There are no + +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::sync::RwLock as AsyncRwLock; + +use crate::player::PlayerId; +use crate::repo::Storage; + +/// Id of a conversation between two players. +/// +/// Dialogs are identified by the pair of participants' ids. The order of ids does not matter. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DialogId(PlayerId, PlayerId); +impl DialogId { + pub fn new(a: PlayerId, b: PlayerId) -> DialogId { + if a.as_inner() < b.as_inner() { + DialogId(a, b) + } else { + DialogId(b, a) + } + } + + pub fn as_inner(&self) -> (&PlayerId, &PlayerId) { + (&self.0, &self.1) + } + + pub fn into_inner(self) -> (PlayerId, PlayerId) { + (self.0, self.1) + } +} + +struct Dialog { + storage_id: u32, + id: DialogId, + message_count: u32, +} + +struct DialogRegistryInner { + dialogs: HashMap>, + storage: Storage, +} + +#[derive(Clone)] +pub struct DialogRegistry(Arc>); + +impl DialogRegistry { + pub fn new(storage: Storage) -> DialogRegistry { + DialogRegistry(Arc::new(AsyncRwLock::new(DialogRegistryInner { + dialogs: HashMap::new(), + storage, + }))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dialog_id_new() { + let a = PlayerId::from("a").unwrap(); + let b = PlayerId::from("b").unwrap(); + let id1 = DialogId::new(a.clone(), b.clone()); + let id2 = DialogId::new(a.clone(), b.clone()); + // Dialog ids are invariant with respect to the order of participants + assert_eq!(id1, id2); + assert_eq!(id1.as_inner(), (&a, &b)); + assert_eq!(id2.as_inner(), (&a, &b)); + } +} diff --git a/crates/lavina-core/src/lib.rs b/crates/lavina-core/src/lib.rs index ff52363..1e5b861 100644 --- a/crates/lavina-core/src/lib.rs +++ b/crates/lavina-core/src/lib.rs @@ -2,10 +2,12 @@ use anyhow::Result; use prometheus::Registry as MetricsRegistry; +use crate::dialog::DialogRegistry; use crate::player::PlayerRegistry; use crate::repo::Storage; use crate::room::RoomRegistry; +pub mod dialog; pub mod player; pub mod prelude; pub mod repo; @@ -18,6 +20,7 @@ mod table; pub struct LavinaCore { pub players: PlayerRegistry, pub rooms: RoomRegistry, + pub dialogs: DialogRegistry, } impl LavinaCore { @@ -25,7 +28,12 @@ impl LavinaCore { // TODO shutdown all services in reverse order on error let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics)?; - Ok(LavinaCore { players, rooms }) + let dialogs = DialogRegistry::new(storage.clone()); + Ok(LavinaCore { + players, + rooms, + dialogs, + }) } pub async fn shutdown(mut self) -> Result<()> {