add: notifications table

add: query_rows macro
fix: postgres driver
This commit is contained in:
trisua 2025-03-25 18:18:33 -04:00
parent 0ea6b25138
commit 81005a6e1c
14 changed files with 258 additions and 33 deletions

View file

@ -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<Token>) -> "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);
}

View file

@ -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()));

View file

@ -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");

View file

@ -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<T, F>(
conn: &Connection<'_>,
sql: &str,
params: &[&(dyn ToSql + Sync)],
mut f: F,
) -> Result<Vec<T>>
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
};
}

View file

@ -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
)

View file

@ -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
)

View file

@ -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(())
};
}

View file

@ -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,

View file

@ -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)) }
);

View file

@ -3,6 +3,7 @@ mod common;
mod drivers;
mod entries;
mod memberships;
mod notifications;
mod pages;
mod reactions;

View file

@ -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<Vec<Notification>> {
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(())
}
}

View file

@ -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

View file

@ -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)) }
);

View file

@ -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::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
title,
content,
owner,
}
}
}