add: user follows, user blocks, ip bans

TODO: implement user following API endpoints
TODO: implement user blocking API endpoints
TODO: don't allow blocked users to interact with the users who blocked them
This commit is contained in:
trisua 2025-03-25 21:19:55 -04:00
parent 81005a6e1c
commit 559ce19932
25 changed files with 628 additions and 127 deletions

View file

@ -29,10 +29,14 @@ impl DataManager {
settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(),
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
permissions: FinePermission::from_bits(get!(x->7(u32))).unwrap(),
// counts
notification_count: get!(x->8(i64)) as usize,
follower_count: get!(x->9(i64)) as usize,
following_count: get!(x->10(i64)) as usize,
}
}
auto_method!(get_user_by_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
auto_method!(get_user_by_id(usize)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
/// Get a user given just their auth token.
@ -87,7 +91,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
@ -97,6 +101,8 @@ impl DataManager {
&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(),
&0.to_string().as_str(),
&0.to_string().as_str(),
&0.to_string().as_str()
]
);
@ -114,7 +120,7 @@ impl DataManager {
/// * `id` - the ID of the user
/// * `password` - the current password of the user
/// * `force` - if we should delete even if the given password is incorrect
pub async fn delete_user(&self, id: &str, password: &str, force: bool) -> Result<()> {
pub async fn delete_user(&self, id: usize, password: &str, force: bool) -> Result<()> {
let user = self.get_user_by_id(id).await?;
if (hash_salted(password.to_string(), user.salt) != user.password) && !force {
@ -140,6 +146,12 @@ 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);
auto_method!(incr_user_notifications() -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr);
auto_method!(decr_user_notifications() -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr);
auto_method!(incr_user_follower_count() -> "UPDATE users SET follower_count = follower_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr);
auto_method!(decr_user_follower_count() -> "UPDATE users SET follower_count = follower_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr);
auto_method!(incr_user_following_count() -> "UPDATE users SET following_count = following_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr);
auto_method!(decr_user_following_count() -> "UPDATE users SET following_count = following_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr);
}

View file

@ -19,6 +19,9 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_MEMBERSHIPS).unwrap();
execute!(&conn, common::CREATE_TABLE_REACTIONS).unwrap();
execute!(&conn, common::CREATE_TABLE_NOTIFICATIONS).unwrap();
execute!(&conn, common::CREATE_TABLE_USERFOLLOWS).unwrap();
execute!(&conn, common::CREATE_TABLE_USERBLOCKS).unwrap();
execute!(&conn, common::CREATE_TABLE_IPBANS).unwrap();
Ok(())
}
@ -358,7 +361,7 @@ macro_rules! auto_method {
}
};
($name:ident() -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal --reactions-key-tmpl=$reactions_key_tmpl:literal --incr) => {
($name:ident() -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal --incr) => {
pub async fn $name(&self, id: usize) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
@ -372,13 +375,12 @@ macro_rules! auto_method {
}
self.2.remove(format!($cache_key_tmpl, id)).await;
self.2.remove(format!($reactions_key_tmpl, id)).await;
Ok(())
}
};
($name:ident() -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal --reactions-key-tmpl=$reactions_key_tmpl:literal --decr) => {
($name:ident() -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal --decr) => {
pub async fn $name(&self, id: usize) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
@ -392,7 +394,6 @@ macro_rules! auto_method {
}
self.2.remove(format!($cache_key_tmpl, id)).await;
self.2.remove(format!($reactions_key_tmpl, id)).await;
Ok(())
}

View file

@ -4,3 +4,6 @@ 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");
pub const CREATE_TABLE_USERFOLLOWS: &str = include_str!("./sql/create_userfollows.sql");
pub const CREATE_TABLE_USERBLOCKS: &str = include_str!("./sql/create_userblocks.sql");
pub const CREATE_TABLE_IPBANS: &str = include_str!("./sql/create_ipbans.sql");

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS ipbans (
ip TEXT NOT NULL,
created INTEGER NOT NULL PRIMARY KEY,
reason TEXT NOT NULL,
moderator TEXT NOT NULL
)

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS userblocks (
id INTEGER NOT NULL PRIMARY KEY,
created INTEGER NOT NULL,
initiator INTEGER NOT NULL,
receiver INTEGER NOT NULL
)

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS userfollows (
id INTEGER NOT NULL PRIMARY KEY,
created INTEGER NOT NULL,
initiator INTEGER NOT NULL,
receiver INTEGER NOT NULL
)

View file

@ -8,5 +8,7 @@ CREATE TABLE IF NOT EXISTS users (
tokens TEXT NOT NULL,
permissions INTEGER NOT NULL,
-- counts
notification_count INTEGER NOT NULL
notification_count INTEGER NOT NULL,
follower_count INTEGER NOT NULL,
following_count INTEGER NOT NULL
)

View file

@ -0,0 +1,90 @@
use super::*;
use crate::cache::Cache;
use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`IpBan`] from an SQL row.
pub(crate) fn get_ipban_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> IpBan {
IpBan {
ip: get!(x->0(String)),
created: get!(x->1(i64)) as usize,
reason: get!(x->2(String)),
moderator: get!(x->3(i64)) as usize,
}
}
auto_method!(get_ipban_by_ip(&str)@get_ipban_from_row -> "SELECT * FROM ipbans WHERE ip = $1" --name="ip ban" --returns=IpBan --cache-key-tmpl="atto.ipban:{}");
/// Create a new user block in the database.
///
/// # Arguments
/// * `data` - a mock [`IpBan`] object to insert
pub async fn create_ipban(&self, data: IpBan) -> Result<()> {
let user = self.get_user_by_id(data.moderator).await?;
// ONLY moderators can create ip bans
if !user.permissions.check(FinePermission::MANAGE_BANS) {
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,
"INSERT INTO ipbans VALUES ($1, $2, $3, $4)",
&[
&data.ip.as_str(),
&data.created.to_string().as_str(),
&data.reason.as_str(),
&data.moderator.to_string().as_str()
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(())
}
pub async fn delete_ipban(&self, id: usize, user: User) -> Result<()> {
// ONLY moderators can manage ip bans
if !user.permissions.check(FinePermission::MANAGE_BANS) {
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 ipbans WHERE id = $1",
&[&id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.ipban:{}", id)).await;
// return
Ok(())
}
}

View file

@ -1,12 +1,12 @@
use super::*;
use crate::cache::Cache;
use crate::model::journal::JournalPageMembership;
use crate::model::journal::JournalMembership;
use crate::model::journal_permissions::JournalPermission;
use crate::model::{
Error, Result,
auth::User,
journal::JournalPage,
journal::{JournalPageReadAccess, JournalPageWriteAccess},
journal::Journal,
journal::{JournalReadAccess, JournalWriteAccess},
permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
@ -18,12 +18,12 @@ use rusqlite::Row;
use tokio_postgres::Row;
impl DataManager {
/// Get a [`JournalPage`] from an SQL row.
/// Get a [`Journal`] from an SQL row.
pub(crate) fn get_page_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> JournalPage {
JournalPage {
) -> Journal {
Journal {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
title: get!(x->2(String)),
@ -37,13 +37,13 @@ impl DataManager {
}
}
auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=JournalPage --cache-key-tmpl="atto.page:{}");
auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=Journal --cache-key-tmpl="atto.page:{}");
/// Create a new journal page in the database.
///
/// # Arguments
/// * `data` - a mock [`JournalPage`] object to insert
pub async fn create_page(&self, data: JournalPage) -> Result<()> {
/// * `data` - a mock [`Journal`] object to insert
pub async fn create_page(&self, data: Journal) -> Result<()> {
// check values
if data.title.len() < 2 {
return Err(Error::DataTooShort("title".to_string()));
@ -82,7 +82,7 @@ impl DataManager {
}
// add journal page owner as admin
self.create_membership(JournalPageMembership::new(
self.create_membership(JournalMembership::new(
data.owner,
data.id,
JournalPermission::ADMINISTRATOR,
@ -97,11 +97,11 @@ impl DataManager {
auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1" --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET prompt = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_read_access(JournalPageReadAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_write_access(JournalPageWriteAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_read_access(JournalReadAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
auto_method!(update_page_write_access(JournalWriteAccess)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
auto_method!(incr_page_likes() -> "UPDATE pages SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --reactions-key-tmpl="atto.entry.likes:{}" --incr);
auto_method!(incr_page_dislikes() -> "UPDATE pages SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --reactions-key-tmpl="atto.entry.dislikes:{}" --incr);
auto_method!(decr_page_likes() -> "UPDATE pages SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --reactions-key-tmpl="atto.entry.likes:{}" --decr);
auto_method!(decr_page_dislikes() -> "UPDATE pages SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --reactions-key-tmpl="atto.entry.dislikes:{}" --decr);
auto_method!(incr_page_likes() -> "UPDATE pages SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --incr);
auto_method!(incr_page_dislikes() -> "UPDATE pages SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --incr);
auto_method!(decr_page_likes() -> "UPDATE pages SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --decr);
auto_method!(decr_page_dislikes() -> "UPDATE pages SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.pages:{}" --decr);
}

View file

@ -1,8 +1,8 @@
use super::*;
use crate::cache::Cache;
use crate::model::{
Error, Result, auth::User, journal::JournalPageMembership,
journal_permissions::JournalPermission, permissions::FinePermission,
Error, Result, auth::User, journal::JournalMembership, journal_permissions::JournalPermission,
permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
@ -17,8 +17,8 @@ impl DataManager {
pub(crate) fn get_membership_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> JournalPageMembership {
JournalPageMembership {
) -> JournalMembership {
JournalMembership {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize,
@ -27,14 +27,14 @@ impl DataManager {
}
}
auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="journal membership" --returns=JournalPageMembership --cache-key-tmpl="atto.membership:{}");
auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="journal membership" --returns=JournalMembership --cache-key-tmpl="atto.membership:{}");
/// Get a journal page membership by `owner` and `journal`.
/// Get a journal membership by `owner` and `journal`.
pub async fn get_membership_by_owner_journal(
&self,
owner: usize,
journal: usize,
) -> Result<JournalPageMembership> {
) -> Result<JournalMembership> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -54,11 +54,11 @@ impl DataManager {
Ok(res.unwrap())
}
/// Create a new journal page membership in the database.
/// Create a new journal membership in the database.
///
/// # Arguments
/// * `data` - a mock [`JournalPageMembership`] object to insert
pub async fn create_membership(&self, data: JournalPageMembership) -> Result<()> {
/// * `data` - a mock [`JournalMembership`] object to insert
pub async fn create_membership(&self, data: JournalMembership) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),

View file

@ -1,11 +1,14 @@
mod auth;
mod common;
mod drivers;
mod entries;
mod ipbans;
mod journals;
mod memberships;
mod notifications;
mod pages;
mod posts;
mod reactions;
mod userblocks;
mod userfollows;
#[cfg(feature = "sqlite")]
pub use drivers::sqlite::*;

View file

@ -10,7 +10,7 @@ use rusqlite::Row;
use tokio_postgres::Row;
impl DataManager {
/// Get a [`Reaction`] from an SQL row.
/// Get a [`Notification`] from an SQL row.
pub(crate) fn get_notification_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
@ -41,7 +41,7 @@ impl DataManager {
);
if res.is_err() {
return Err(Error::GeneralNotFound("reactions".to_string()));
return Err(Error::GeneralNotFound("notification".to_string()));
}
Ok(res.unwrap())

View file

@ -1,8 +1,8 @@
use super::*;
use crate::cache::Cache;
use crate::model::journal::JournalEntryContext;
use crate::model::journal::JournalPostContext;
use crate::model::{
Error, Result, auth::User, journal::JournalEntry, journal::JournalPageWriteAccess,
Error, Result, auth::User, journal::JournalPost, journal::JournalWriteAccess,
permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
@ -15,11 +15,11 @@ use tokio_postgres::Row;
impl DataManager {
/// Get a [`JournalEntry`] from an SQL row.
pub(crate) fn get_entry_from_row(
pub(crate) fn get_post_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> JournalEntry {
JournalEntry {
) -> JournalPost {
JournalPost {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
content: get!(x->2(String)),
@ -32,13 +32,13 @@ impl DataManager {
}
}
auto_method!(get_entry_by_id()@get_entry_from_row -> "SELECT * FROM entries WHERE id = $1" --name="journal entry" --returns=JournalEntry --cache-key-tmpl="atto.entry:{}");
auto_method!(get_post_by_id()@get_post_from_row -> "SELECT * FROM entries WHERE id = $1" --name="journal entry" --returns=JournalPost --cache-key-tmpl="atto.entry:{}");
/// Create a new journal entry in the database.
///
/// # Arguments
/// * `data` - a mock [`JournalEntry`] object to insert
pub async fn create_entry(&self, data: JournalEntry) -> Result<()> {
pub async fn create_entry(&self, data: JournalPost) -> Result<()> {
// check values
if data.content.len() < 2 {
return Err(Error::DataTooShort("content".to_string()));
@ -53,12 +53,12 @@ impl DataManager {
};
match page.write_access {
JournalPageWriteAccess::Owner => {
JournalWriteAccess::Owner => {
if data.owner != page.owner {
return Err(Error::NotAllowed);
}
}
JournalPageWriteAccess::Joined => {
JournalWriteAccess::Joined => {
if let Err(_) = self
.get_membership_by_owner_journal(data.owner, page.id)
.await
@ -95,12 +95,12 @@ impl DataManager {
Ok(())
}
auto_method!(delete_entry()@get_entry_by_id:MANAGE_JOURNAL_ENTRIES -> "DELETE FROM entries WHERE id = $1" --cache-key-tmpl="atto.entry:{}");
auto_method!(update_entry_content(String)@get_entry_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE entries SET content = $1 WHERE id = $2" --cache-key-tmpl="atto.entry:{}");
auto_method!(update_entry_context(JournalEntryContext)@get_entry_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE entries SET context = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.entry:{}");
auto_method!(delete_entry()@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "DELETE FROM entries WHERE id = $1" --cache-key-tmpl="atto.entry:{}");
auto_method!(update_post_content(String)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE entries SET content = $1 WHERE id = $2" --cache-key-tmpl="atto.entry:{}");
auto_method!(update_post_context(JournalPostContext)@get_post_by_id:MANAGE_JOURNAL_ENTRIES -> "UPDATE entries SET context = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.entry:{}");
auto_method!(incr_entry_likes() -> "UPDATE entries SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --reactions-key-tmpl="atto.entry.likes:{}" --incr);
auto_method!(incr_entry_dislikes() -> "UPDATE entries SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --reactions-key-tmpl="atto.entry.dislikes:{}" --incr);
auto_method!(decr_entry_likes() -> "UPDATE entries SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --reactions-key-tmpl="atto.entry.likes:{}" --decr);
auto_method!(decr_entry_dislikes() -> "UPDATE entries SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --reactions-key-tmpl="atto.entry.dislikes:{}" --decr);
auto_method!(incr_post_likes() -> "UPDATE entries SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --incr);
auto_method!(incr_post_dislikes() -> "UPDATE entries SET likes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --incr);
auto_method!(decr_post_likes() -> "UPDATE entries SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --decr);
auto_method!(decr_post_dislikes() -> "UPDATE entries SET likes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.entry:{}" --decr);
}

View file

@ -51,13 +51,13 @@ impl DataManager {
);
if res.is_err() {
return Err(Error::GeneralNotFound("reactions".to_string()));
return Err(Error::GeneralNotFound("reaction".to_string()));
}
Ok(res.unwrap())
}
/// Create a new journal page membership in the database.
/// Create a new journal membership in the database.
///
/// # Arguments
/// * `data` - a mock [`Reaction`] object to insert
@ -86,13 +86,13 @@ impl DataManager {
// incr corresponding
match data.asset_type {
AssetType::JournalPage => {
AssetType::Journal => {
if let Err(e) = self.incr_page_likes(data.id).await {
return Err(e);
}
}
AssetType::JournalEntry => {
if let Err(e) = self.incr_entry_likes(data.id).await {
if let Err(e) = self.incr_post_likes(data.id).await {
return Err(e);
}
}
@ -130,13 +130,13 @@ impl DataManager {
// decr corresponding
match reaction.asset_type {
AssetType::JournalPage => {
AssetType::Journal => {
if let Err(e) = self.decr_page_likes(reaction.asset).await {
return Err(e);
}
}
AssetType::JournalEntry => {
if let Err(e) = self.decr_entry_likes(reaction.asset).await {
if let Err(e) = self.decr_post_likes(reaction.asset).await {
return Err(e);
}
}

View file

@ -0,0 +1,137 @@
use super::*;
use crate::cache::Cache;
use crate::model::{Error, Result, auth::User, auth::UserBlock, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`UserBlock`] from an SQL row.
pub(crate) fn get_userblock_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> UserBlock {
UserBlock {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
initiator: get!(x->2(i64)) as usize,
receiver: get!(x->3(i64)) as usize,
}
}
auto_method!(get_userblock_by_id()@get_userblock_from_row -> "SELECT * FROM userblocks WHERE id = $1" --name="user block" --returns=UserBlock --cache-key-tmpl="atto.userblock:{}");
/// Get a user block by `initiator` and `receiver` (in that order).
pub async fn get_userblock_by_initiator_receiver(
&self,
initiator: usize,
receiver: usize,
) -> Result<UserBlock> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM userblocks WHERE initator = $1 AND receiver = $2",
&[&(initiator as i64), &(receiver as i64)],
|x| { Ok(Self::get_userblock_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("user block".to_string()));
}
Ok(res.unwrap())
}
/// Get a user block by `receiver` and `initiator` (in that order).
pub async fn get_userblock_by_receiver_initiator(
&self,
receiver: usize,
initiator: usize,
) -> Result<UserBlock> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM userblocks WHERE receiver = $1 AND initiator = $2",
&[&(receiver as i64), &(initiator as i64)],
|x| { Ok(Self::get_userblock_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("user block".to_string()));
}
Ok(res.unwrap())
}
/// Create a new user block in the database.
///
/// # Arguments
/// * `data` - a mock [`UserBlock`] object to insert
pub async fn create_userblock(&self, data: UserBlock) -> 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 userblocks VALUES ($1, $2, $3, $4)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
&data.initiator.to_string().as_str(),
&data.receiver.to_string().as_str()
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(())
}
pub async fn delete_userblock(&self, id: usize, user: User) -> Result<()> {
let block = self.get_userblock_by_id(id).await?;
if user.id != block.initiator {
// only the initiator (or moderators) can delete user blocks!
if !user.permissions.check(FinePermission::MANAGE_FOLLOWS) {
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 userblocks WHERE id = $1",
&[&id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.userblock:{}", id)).await;
// return
Ok(())
}
}

View file

@ -0,0 +1,152 @@
use super::*;
use crate::cache::Cache;
use crate::model::{Error, Result, auth::User, auth::UserFollow, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`UserFollow`] from an SQL row.
pub(crate) fn get_userfollow_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> UserFollow {
UserFollow {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
initiator: get!(x->2(i64)) as usize,
receiver: get!(x->3(i64)) as usize,
}
}
auto_method!(get_userfollow_by_id()@get_userfollow_from_row -> "SELECT * FROM userfollows WHERE id = $1" --name="user follow" --returns=UserFollow --cache-key-tmpl="atto.userfollow:{}");
/// Get a user follow by `initiator` and `receiver` (in that order).
pub async fn get_userfollow_by_initiator_receiver(
&self,
initiator: usize,
receiver: usize,
) -> Result<UserFollow> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM userfollows WHERE initator = $1 AND receiver = $2",
&[&(initiator as i64), &(receiver as i64)],
|x| { Ok(Self::get_userfollow_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("user follow".to_string()));
}
Ok(res.unwrap())
}
/// Get a user follow by `receiver` and `initiator` (in that order).
pub async fn get_userfollow_by_receiver_initiator(
&self,
receiver: usize,
initiator: usize,
) -> Result<UserFollow> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM userfollows WHERE receiver = $1 AND initiator = $2",
&[&(receiver as i64), &(initiator as i64)],
|x| { Ok(Self::get_userfollow_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("user follow".to_string()));
}
Ok(res.unwrap())
}
/// Create a new user follow in the database.
///
/// # Arguments
/// * `data` - a mock [`UserFollow`] object to insert
pub async fn create_userfollow(&self, data: UserFollow) -> 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 userfollows VALUES ($1, $2, $3, $4)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
&data.initiator.to_string().as_str(),
&data.receiver.to_string().as_str()
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// incr counts
self.incr_user_following_count(data.initiator)
.await
.unwrap();
self.incr_user_follower_count(data.receiver).await.unwrap();
// return
Ok(())
}
pub async fn delete_userfollow(&self, id: usize, user: User) -> Result<()> {
let follow = self.get_userfollow_by_id(id).await?;
if (user.id != follow.initiator) && (user.id != follow.receiver) {
if !user.permissions.check(FinePermission::MANAGE_FOLLOWS) {
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 userfollows WHERE id = $1",
&[&id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.userfollow:{}", id)).await;
// decr counts
self.incr_user_following_count(follow.initiator)
.await
.unwrap();
self.incr_user_follower_count(follow.receiver)
.await
.unwrap();
// return
Ok(())
}
}