diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index cee3713..c5f77cb 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -21,8 +21,8 @@ impl DataManager { #[cfg(feature = "postgres")] x: &Row, ) -> User { User { - id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize, + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, username: get!(x->2(String)), password: get!(x->3(String)), salt: get!(x->4(String)), @@ -87,7 +87,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", &[ &data.id.to_string().as_str(), &data.created.to_string().as_str(), @@ -96,7 +96,8 @@ impl DataManager { &data.salt.as_str(), &serde_json::to_string(&data.settings).unwrap().as_str(), &serde_json::to_string(&data.tokens).unwrap().as_str(), - &(FinePermission::DEFAULT.bits()).to_string().as_str() + &(FinePermission::DEFAULT.bits()).to_string().as_str(), + &0.to_string().as_str() ] ); @@ -138,4 +139,7 @@ impl DataManager { } auto_method!(update_user_tokens(Vec) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}"); + + auto_method!(incr_user_notifications() -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --reactions-key-tmpl="atto.user.notification_count:{}" --incr); + auto_method!(decr_user_notifications() -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --reactions-key-tmpl="atto.user.notification_count:{}" --decr); } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 100bf22..9356840 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -13,11 +13,12 @@ impl DataManager { Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; - execute!(&conn, common::CREATE_TABLE_USERS, []).unwrap(); - execute!(&conn, common::CREATE_TABLE_PAGES, []).unwrap(); - execute!(&conn, common::CREATE_TABLE_ENTRIES, []).unwrap(); - execute!(&conn, common::CREATE_TABLE_MEMBERSHIPS, []).unwrap(); - execute!(&conn, common::CREATE_TABLE_REACTIONS, []).unwrap(); + execute!(&conn, common::CREATE_TABLE_USERS).unwrap(); + execute!(&conn, common::CREATE_TABLE_PAGES).unwrap(); + execute!(&conn, common::CREATE_TABLE_ENTRIES).unwrap(); + execute!(&conn, common::CREATE_TABLE_MEMBERSHIPS).unwrap(); + execute!(&conn, common::CREATE_TABLE_REACTIONS).unwrap(); + execute!(&conn, common::CREATE_TABLE_NOTIFICATIONS).unwrap(); Ok(()) } @@ -32,7 +33,9 @@ macro_rules! auto_method { Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; - let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) }); + let res = query_row!(&conn, $query, &[&(id as i64)], |x| { + Ok(Self::$select_fn(x)) + }); if res.is_err() { return Err(Error::GeneralNotFound($name_.to_string())); @@ -49,7 +52,9 @@ macro_rules! auto_method { Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; - let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) }); + let res = query_row!(&conn, $query, &[&(id as i64)], |x| { + Ok(Self::$select_fn(x)) + }); if res.is_err() { return Err(Error::GeneralNotFound($name_.to_string())); diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index d638b5d..787224a 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -3,3 +3,4 @@ pub const CREATE_TABLE_PAGES: &str = include_str!("./sql/create_pages.sql"); pub const CREATE_TABLE_ENTRIES: &str = include_str!("./sql/create_entries.sql"); pub const CREATE_TABLE_MEMBERSHIPS: &str = include_str!("./sql/create_memberships.sql"); pub const CREATE_TABLE_REACTIONS: &str = include_str!("./sql/create_reactions.sql"); +pub const CREATE_TABLE_NOTIFICATIONS: &str = include_str!("./sql/create_notifications.sql"); diff --git a/crates/core/src/database/drivers/postgres.rs b/crates/core/src/database/drivers/postgres.rs index bdd0e00..369b692 100644 --- a/crates/core/src/database/drivers/postgres.rs +++ b/crates/core/src/database/drivers/postgres.rs @@ -10,6 +10,7 @@ use bb8_postgres::{ PostgresConnectionManager, bb8::{Pool, PooledConnection}, }; +use std::collections::HashMap; use tetratto_l10n::{LangFile, read_langs}; use tokio_postgres::{Config as PgConfig, NoTls, Row, types::ToSql}; @@ -91,6 +92,38 @@ macro_rules! query_row { }; } +pub async fn query_rows_helper( + conn: &Connection<'_>, + sql: &str, + params: &[&(dyn ToSql + Sync)], + mut f: F, +) -> Result> +where + F: FnMut(&Row) -> T, +{ + let query = conn.prepare(sql).await.unwrap(); + let res = conn.query(&query, params).await; + + if let Ok(rows) = res { + let mut out = Vec::new(); + + for row in rows { + out.push(f(&row)); + } + + return Ok(out); + } else { + Err(res.unwrap_err()) + } +} + +#[macro_export] +macro_rules! query_rows { + ($conn:expr, $sql:expr, $params:expr, $f:expr) => { + crate::database::query_rows_helper($conn, $sql, $params, $f).await + }; +} + pub async fn execute_helper( conn: &Connection<'_>, sql: &str, @@ -106,4 +139,8 @@ macro_rules! execute { ($conn:expr, $sql:expr, $params:expr) => { crate::database::execute_helper($conn, $sql, $params).await }; + + ($conn:expr, $sql:expr) => { + crate::database::execute_helper($conn, $sql, &[]).await + }; } diff --git a/crates/core/src/database/drivers/sql/create_notifications.sql b/crates/core/src/database/drivers/sql/create_notifications.sql new file mode 100644 index 0000000..7be3b27 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_notifications.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS notifications ( + id INTEGER NOT NULL PRIMARY KEY, + created INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + owner INTEGER NOT NULL +) diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 2b7420e..59c1b0f 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -6,5 +6,7 @@ CREATE TABLE IF NOT EXISTS users ( salt TEXT NOT NULL, settings TEXT NOT NULL, tokens TEXT NOT NULL, - permissions INTEGER NOT NULL + permissions INTEGER NOT NULL, + -- counts + notification_count INTEGER NOT NULL ) diff --git a/crates/core/src/database/drivers/sqlite.rs b/crates/core/src/database/drivers/sqlite.rs index b117387..7fd8b85 100644 --- a/crates/core/src/database/drivers/sqlite.rs +++ b/crates/core/src/database/drivers/sqlite.rs @@ -42,7 +42,6 @@ impl DataManager { } } -#[cfg(feature = "sqlite")] #[macro_export] macro_rules! get { ($row:ident->$idx:literal($t:tt)) => { @@ -58,9 +57,32 @@ macro_rules! query_row { }}; } +#[macro_export] +macro_rules! query_rows { + ($conn:expr, $sql:expr, $params:expr, $f:expr) => {{ + let mut query = $conn.prepare($sql).unwrap(); + + if let Ok(mut rows) = query.query($params) { + let mut out = Vec::new(); + + while let Some(row) = rows.next().unwrap() { + out.push($f(&row)); + } + + Ok(out) + } else { + Err(Error::Unknown) + } + }}; +} + #[macro_export] macro_rules! execute { ($conn:expr, $sql:expr, $params:expr) => { $conn.prepare($sql).unwrap().execute($params) }; + + ($conn:expr, $sql:expr) => { + $conn.prepare($sql).unwrap().execute(()) + }; } diff --git a/crates/core/src/database/entries.rs b/crates/core/src/database/entries.rs index f08bd33..5666a53 100644 --- a/crates/core/src/database/entries.rs +++ b/crates/core/src/database/entries.rs @@ -20,11 +20,11 @@ impl DataManager { #[cfg(feature = "postgres")] x: &Row, ) -> JournalEntry { JournalEntry { - id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize, + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, content: get!(x->2(String)), - owner: get!(x->3(u64)) as usize, - journal: get!(x->4(u64)) as usize, + owner: get!(x->3(i64)) as usize, + journal: get!(x->4(i64)) as usize, context: serde_json::from_str(&get!(x->5(String))).unwrap(), // likes likes: get!(x->6(i64)) as isize, diff --git a/crates/core/src/database/memberships.rs b/crates/core/src/database/memberships.rs index 8cc7149..59557dc 100644 --- a/crates/core/src/database/memberships.rs +++ b/crates/core/src/database/memberships.rs @@ -19,10 +19,10 @@ impl DataManager { #[cfg(feature = "postgres")] x: &Row, ) -> JournalPageMembership { JournalPageMembership { - id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize, - owner: get!(x->2(u64)) as usize, - journal: get!(x->3(u64)) as usize, + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + journal: get!(x->3(i64)) as usize, role: JournalPermission::from_bits(get!(x->4(u32))).unwrap(), } } @@ -43,7 +43,7 @@ impl DataManager { let res = query_row!( &conn, "SELECT * FROM memberships WHERE owner = $1 AND journal = $2", - &[&owner, &journal], + &[&(owner as i64), &(journal as i64)], |x| { Ok(Self::get_membership_from_row(x)) } ); diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index dc02fcd..8527943 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -3,6 +3,7 @@ mod common; mod drivers; mod entries; mod memberships; +mod notifications; mod pages; mod reactions; diff --git a/crates/core/src/database/notifications.rs b/crates/core/src/database/notifications.rs new file mode 100644 index 0000000..3ba8f39 --- /dev/null +++ b/crates/core/src/database/notifications.rs @@ -0,0 +1,117 @@ +use super::*; +use crate::cache::Cache; +use crate::model::{Error, Result, auth::Notification, auth::User, permissions::FinePermission}; +use crate::{auto_method, execute, get, query_row, query_rows}; + +#[cfg(feature = "sqlite")] +use rusqlite::Row; + +#[cfg(feature = "postgres")] +use tokio_postgres::Row; + +impl DataManager { + /// Get a [`Reaction`] from an SQL row. + pub(crate) fn get_notification_from_row( + #[cfg(feature = "sqlite")] x: &Row<'_>, + #[cfg(feature = "postgres")] x: &Row, + ) -> Notification { + Notification { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + title: get!(x->2(String)), + content: get!(x->3(String)), + owner: get!(x->4(i64)) as usize, + } + } + + auto_method!(get_notification_by_id()@get_notification_from_row -> "SELECT * FROM notifications WHERE id = $1" --name="notification" --returns=Notification --cache-key-tmpl="atto.notification:{}"); + + /// Get a reaction by `owner` and `asset`. + pub async fn get_notifications_by_owner(&self, owner: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM notifications WHERE owner = $1", + &[&(owner as i64)], + |x| { Self::get_notification_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("reactions".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new notification in the database. + /// + /// # Arguments + /// * `data` - a mock [`Reaction`] object to insert + pub async fn create_notification(&self, data: Notification) -> Result<()> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO reactions VALUES ($1, $2, $3, $4, $5)", + &[ + &data.id.to_string().as_str(), + &data.created.to_string().as_str(), + &data.title.to_string().as_str(), + &data.content.to_string().as_str(), + &data.owner.to_string().as_str() + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // incr notification count + self.incr_user_notifications(data.owner).await.unwrap(); + + // return + Ok(()) + } + + pub async fn delete_notification(&self, id: usize, user: User) -> Result<()> { + let notification = self.get_notification_by_id(id).await?; + + if user.id != notification.owner { + if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); + } + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "DELETE FROM notification WHERE id = $1", + &[&id.to_string()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.notification:{}", id)).await; + + // decr notification count + // self.decr_user_notifications(notification.owner) + // .await + // .unwrap(); + + // return + Ok(()) + } +} diff --git a/crates/core/src/database/pages.rs b/crates/core/src/database/pages.rs index 8748444..ba62181 100644 --- a/crates/core/src/database/pages.rs +++ b/crates/core/src/database/pages.rs @@ -24,11 +24,11 @@ impl DataManager { #[cfg(feature = "postgres")] x: &Row, ) -> JournalPage { JournalPage { - id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize, + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, title: get!(x->2(String)), prompt: get!(x->3(String)), - owner: get!(x->4(u64)) as usize, + owner: get!(x->4(i64)) as usize, read_access: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(), write_access: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), // likes diff --git a/crates/core/src/database/reactions.rs b/crates/core/src/database/reactions.rs index f196c51..3b9227a 100644 --- a/crates/core/src/database/reactions.rs +++ b/crates/core/src/database/reactions.rs @@ -1,7 +1,11 @@ use super::*; use crate::cache::Cache; -use crate::model::reactions::AssetType; -use crate::model::{Error, Result, auth::User, permissions::FinePermission, reactions::Reaction}; +use crate::model::{ + Error, Result, + auth::User, + permissions::FinePermission, + reactions::{AssetType, Reaction}, +}; use crate::{auto_method, execute, get, query_row}; #[cfg(feature = "sqlite")] @@ -17,12 +21,12 @@ impl DataManager { #[cfg(feature = "postgres")] x: &Row, ) -> Reaction { Reaction { - id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize, - owner: get!(x->2(u64)) as usize, - asset: get!(x->3(u64)) as usize, + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + asset: get!(x->3(i64)) as usize, asset_type: serde_json::from_str(&get!(x->4(String))).unwrap(), - is_like: if get!(x->5(u8)) == 1 { true } else { false }, + is_like: if get!(x->5(i8)) == 1 { true } else { false }, } } @@ -42,7 +46,7 @@ impl DataManager { let res = query_row!( &conn, "SELECT * FROM reactions WHERE owner = $1 AND asset = $2", - &[&owner, &asset], + &[&(owner as i64), &(asset as i64)], |x| { Ok(Self::get_reaction_from_row(x)) } ); diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 5700325..51501a3 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -72,3 +72,28 @@ impl User { self.password == hash_salted(against, self.salt.clone()) } } + +#[derive(Debug, Serialize)] +pub struct Notification { + pub id: usize, + pub created: usize, + pub title: String, + pub content: String, + pub owner: usize, +} + +impl Notification { + /// Returns a new [`Notification`]. + pub fn new(title: String, content: String, owner: usize) -> Self { + Self { + id: AlmostSnowflake::new(1234567890) + .to_string() + .parse::() + .unwrap(), + created: unix_epoch_timestamp() as usize, + title, + content, + owner, + } + } +}