diff --git a/Cargo.lock b/Cargo.lock index bf71fa9..c8ff42b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -832,6 +832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" dependencies = [ "bitflags", + "chrono", "fallible-iterator", "fallible-streaming-iterator", "hashlink", diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index 9ed4cd7..6b8684f 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -11,7 +11,7 @@ crossterm = "0.23.2" directories = "4.0.1" edit = "0.1.4" parking_lot = "0.12.1" -rusqlite = "0.27.0" +rusqlite = { version = "0.27.0", features = ["chrono"] } serde = { version = "1.0.137", features = ["derive"] } serde_json = "1.0.81" tokio = { version = "1.19.2", features = ["full"] } diff --git a/cove-tui/src/euph.rs b/cove-tui/src/euph.rs index b098144..08d4a57 100644 --- a/cove-tui/src/euph.rs +++ b/cove-tui/src/euph.rs @@ -4,6 +4,8 @@ use std::convert::Infallible; use tokio::sync::{mpsc, oneshot}; +pub use api::{Message, SessionView, Snowflake, Time, UserId}; + enum Request {} pub struct EuphRoom { diff --git a/cove-tui/src/euph/api/types.rs b/cove-tui/src/euph/api/types.rs index 03849d8..357db74 100644 --- a/cove-tui/src/euph/api/types.rs +++ b/cove-tui/src/euph/api/types.rs @@ -269,7 +269,7 @@ pub struct SessionView { /// A 13-character string, usually used as aunique identifier for some type of object. /// /// It is the base-36 encoding of an unsigned, 64-bit integer. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Snowflake(pub u64); impl Serialize for Snowflake { diff --git a/cove-tui/src/vault.rs b/cove-tui/src/vault.rs index 2fea77e..097185c 100644 --- a/cove-tui/src/vault.rs +++ b/cove-tui/src/vault.rs @@ -1,3 +1,4 @@ +mod euph; mod migrate; use std::path::Path; @@ -6,9 +7,11 @@ use std::{fs, thread}; use rusqlite::Connection; use tokio::sync::{mpsc, oneshot}; +use self::euph::{EuphRequest, EuphVault}; + enum Request { Close(oneshot::Sender<()>), - Nop, + Euph(EuphRequest), } pub struct Vault { @@ -21,24 +24,30 @@ impl Vault { let _ = self.tx.send(Request::Close(tx)).await; let _ = rx.await; } + + pub fn euph(&self, room: String) -> EuphVault { + EuphVault { + tx: self.tx.clone(), + room, + } + } } -fn run(conn: Connection, mut rx: mpsc::Receiver) -> anyhow::Result<()> { +fn run(conn: Connection, mut rx: mpsc::Receiver) { while let Some(request) = rx.blocking_recv() { match request { Request::Close(tx) => { println!("Optimizing vault"); - conn.execute_batch("PRAGMA optimize")?; + let _ = conn.execute_batch("PRAGMA optimize"); // Ensure `Vault::close` exits only after the sqlite connection // has been closed properly. drop(conn); drop(tx); break; } - Request::Nop => {} + Request::Euph(r) => r.perform(&conn), } } - Ok(()) } pub fn launch(path: &Path) -> rusqlite::Result { diff --git a/cove-tui/src/vault/euph.rs b/cove-tui/src/vault/euph.rs new file mode 100644 index 0000000..e1ce54c --- /dev/null +++ b/cove-tui/src/vault/euph.rs @@ -0,0 +1,329 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use rusqlite::{params, Connection, OptionalExtension}; +use tokio::sync::{mpsc, oneshot}; + +use crate::euph::Snowflake; +use crate::store::{Msg, MsgStore, Path, Tree}; + +use super::Request; + +#[derive(Debug, Clone)] +pub struct EuphMsg { + id: Snowflake, + parent: Option, + time: DateTime, + nick: String, + content: String, +} + +impl Msg for EuphMsg { + type Id = Snowflake; + + fn id(&self) -> Self::Id { + self.id + } + + fn parent(&self) -> Option { + self.parent + } + + fn time(&self) -> DateTime { + self.time + } + + fn nick(&self) -> String { + self.nick.clone() + } + + fn content(&self) -> String { + self.content.clone() + } +} + +impl From for Request { + fn from(r: EuphRequest) -> Self { + Self::Euph(r) + } +} + +pub struct EuphVault { + pub(super) tx: mpsc::Sender, + pub(super) room: String, +} + +#[async_trait] +impl MsgStore for EuphVault { + async fn path(&self, id: &Snowflake) -> Path { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::Path { + room: self.room.clone(), + id: *id, + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } + + async fn tree(&self, root: &Snowflake) -> Tree { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::Tree { + room: self.room.clone(), + root: *root, + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } + + async fn prev_tree(&self, root: &Snowflake) -> Option { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::PrevTree { + room: self.room.clone(), + root: *root, + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } + + async fn next_tree(&self, root: &Snowflake) -> Option { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::NextTree { + room: self.room.clone(), + root: *root, + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } + + async fn first_tree(&self) -> Option { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::FirstTree { + room: self.room.clone(), + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } + + async fn last_tree(&self) -> Option { + // TODO vault::Error + let (tx, rx) = oneshot::channel(); + let request = EuphRequest::LastTree { + room: self.room.clone(), + result: tx, + }; + let _ = self.tx.send(request.into()).await; + rx.await.unwrap() + } +} + +pub(super) enum EuphRequest { + Path { + room: String, + id: Snowflake, + result: oneshot::Sender>, + }, + Tree { + room: String, + root: Snowflake, + result: oneshot::Sender>, + }, + PrevTree { + room: String, + root: Snowflake, + result: oneshot::Sender>, + }, + NextTree { + room: String, + root: Snowflake, + result: oneshot::Sender>, + }, + FirstTree { + room: String, + result: oneshot::Sender>, + }, + LastTree { + room: String, + result: oneshot::Sender>, + }, +} + +impl EuphRequest { + pub(super) fn perform(self, conn: &Connection) { + let _ = match self { + EuphRequest::Path { room, id, result } => Self::path(conn, room, id, result), + EuphRequest::Tree { room, root, result } => Self::tree(conn, room, root, result), + EuphRequest::PrevTree { room, root, result } => { + Self::prev_tree(conn, room, root, result) + } + EuphRequest::NextTree { room, root, result } => { + Self::next_tree(conn, room, root, result) + } + EuphRequest::FirstTree { room, result } => Self::first_tree(conn, room, result), + EuphRequest::LastTree { room, result } => Self::last_tree(conn, room, result), + }; + } + + fn path( + conn: &Connection, + room: String, + id: Snowflake, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let path = conn + .prepare( + " + WITH RECURSIVE path (room, id) = ( + VALUES (?, ?) + UNION + SELECT (room, parent) + FROM euph_msgs + JOIN path USING (room, id) + ) + SELECT id + FROM path + ORDER BY id ASC + ", + )? + .query_map(params![room, id.0], |row| row.get(0).map(Snowflake))? + .collect::>()?; + let path = Path::new(path); + let _ = result.send(path); + Ok(()) + } + + fn tree( + conn: &Connection, + room: String, + root: Snowflake, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let msgs = conn + .prepare( + " + WITH RECURSIVE tree (room, id) = ( + VALUES (?, ?) + UNION + SELECT (euph_msgs.room, euph_msgs.id) + FROM euph_msgs + JOIN tree + ON tree.room = euph_msgs.room + AND tree.id = euph_msgs.parent + ) + SELECT (id, parent, time, name, content) + FROM euph_msg + JOIN tree USING (room, id) + ORDER BY id ASC + ", + )? + .query_map(params![room, root.0], |row| { + Ok(EuphMsg { + id: Snowflake(row.get(0)?), + parent: row.get::<_, Option>(1)?.map(Snowflake), + time: row.get(2)?, + nick: row.get(3)?, + content: row.get(4)?, + }) + })? + .collect::>()?; + let tree = Tree::new(root, msgs); + let _ = result.send(tree); + Ok(()) + } + + fn prev_tree( + conn: &Connection, + room: String, + root: Snowflake, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let tree = conn + .prepare( + " + SELECT id + FROM euph_trees + WHERE room = ? + AND id < ? + ORDER BY id DESC + LIMIT 1 + ", + )? + .query_row(params![room, root.0], |row| row.get(0).map(Snowflake)) + .optional()?; + let _ = result.send(tree); + Ok(()) + } + + fn next_tree( + conn: &Connection, + room: String, + root: Snowflake, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let tree = conn + .prepare( + " + SELECT id + FROM euph_trees + WHERE room = ? + AND id > ? + ORDER BY id ASC + LIMIT 1 + ", + )? + .query_row(params![room, root.0], |row| row.get(0).map(Snowflake)) + .optional()?; + let _ = result.send(tree); + Ok(()) + } + + fn first_tree( + conn: &Connection, + room: String, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let tree = conn + .prepare( + " + SELECT id + FROM euph_trees + WHERE room = ? + ORDER BY id ASC + LIMIT 1 + ", + )? + .query_row([room], |row| row.get(0).map(Snowflake)) + .optional()?; + let _ = result.send(tree); + Ok(()) + } + + fn last_tree( + conn: &Connection, + room: String, + result: oneshot::Sender>, + ) -> rusqlite::Result<()> { + let tree = conn + .prepare( + " + SELECT id + FROM euph_trees + WHERE room = ? + ORDER BY id DESC + LIMIT 1 + ", + )? + .query_row([room], |row| row.get(0).map(Snowflake)) + .optional()?; + let _ = result.send(tree); + Ok(()) + } +} diff --git a/cove-tui/src/vault/migrate.rs b/cove-tui/src/vault/migrate.rs index a32cbaf..46967f2 100644 --- a/cove-tui/src/vault/migrate.rs +++ b/cove-tui/src/vault/migrate.rs @@ -57,6 +57,20 @@ fn m1(tx: &mut Transaction) -> rusqlite::Result<()> { FOREIGN KEY (room, start) REFERENCES euph_msgs (room, start), FOREIGN KEY (room, end) REFERENCES euph_msgs (room, end) ) STRICT; + + CREATE VIEW euph_trees (room, id) AS + SELECT room, id + FROM euph_msgs + WHERE parent IS NULL + UNION + ( + SELECT room, parent + FROM euph_msgs + WHERE parent IS NOT NULL + EXCEPT + SELECT room, id + FROM euph_msgs + ) ", ) }