diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index 38d4752..89c7302 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -42,6 +42,11 @@ pub async fn register_request( .unwrap_or("") .to_string(); + // check for ip ban + if let Ok(_) = data.get_ipban_by_ip(&real_ip).await { + return (None, Json(Error::NotAllowed.into())); + } + // ... let mut user = User::new(props.username, props.password); let (initial_token, t) = User::create_token(&real_ip); @@ -90,6 +95,11 @@ pub async fn login_request( .unwrap_or("") .to_string(); + // check for ip ban + if let Ok(_) = data.get_ipban_by_ip(&real_ip).await { + return (None, Json(Error::NotAllowed.into())); + } + // verify password let user = match data.get_user_by_username(&props.username).await { Ok(ua) => ua, diff --git a/crates/app/src/routes/api/v1/journal/pages.rs b/crates/app/src/routes/api/v1/journal/journals.rs similarity index 87% rename from crates/app/src/routes/api/v1/journal/pages.rs rename to crates/app/src/routes/api/v1/journal/journals.rs index 744a9de..03c45ce 100644 --- a/crates/app/src/routes/api/v1/journal/pages.rs +++ b/crates/app/src/routes/api/v1/journal/journals.rs @@ -1,19 +1,19 @@ use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{ApiReturn, Error, journal::JournalPage}; +use tetratto_core::model::{ApiReturn, Error, journal::Journal}; use crate::{ State, get_user_from_token, routes::api::v1::{ - CreateJournalPage, UpdateJournalPagePrompt, UpdateJournalPageReadAccess, - UpdateJournalPageTitle, UpdateJournalPageWriteAccess, + CreateJournal, UpdateJournalPrompt, UpdateJournalReadAccess, UpdateJournalTitle, + UpdateJournalWriteAccess, }, }; pub async fn create_request( jar: CookieJar, Extension(data): Extension, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data) { @@ -22,7 +22,7 @@ pub async fn create_request( }; match data - .create_page(JournalPage::new(req.title, req.prompt, user.id)) + .create_page(Journal::new(req.title, req.prompt, user.id)) .await { Ok(_) => Json(ApiReturn { @@ -59,7 +59,7 @@ pub async fn update_title_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data) { @@ -81,7 +81,7 @@ pub async fn update_prompt_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data) { @@ -103,7 +103,7 @@ pub async fn update_read_access_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data) { @@ -125,7 +125,7 @@ pub async fn update_write_access_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data) { diff --git a/crates/app/src/routes/api/v1/journal/mod.rs b/crates/app/src/routes/api/v1/journal/mod.rs index dd2f7f4..5f8de73 100644 --- a/crates/app/src/routes/api/v1/journal/mod.rs +++ b/crates/app/src/routes/api/v1/journal/mod.rs @@ -1,2 +1,2 @@ -pub mod entries; -pub mod pages; +pub mod journals; +pub mod posts; diff --git a/crates/app/src/routes/api/v1/journal/entries.rs b/crates/app/src/routes/api/v1/journal/posts.rs similarity index 90% rename from crates/app/src/routes/api/v1/journal/entries.rs rename to crates/app/src/routes/api/v1/journal/posts.rs index ce103d3..78cd6da 100644 --- a/crates/app/src/routes/api/v1/journal/entries.rs +++ b/crates/app/src/routes/api/v1/journal/posts.rs @@ -1,6 +1,6 @@ use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{ApiReturn, Error, journal::JournalEntry}; +use tetratto_core::model::{ApiReturn, Error, journal::JournalPost}; use crate::{ State, get_user_from_token, @@ -19,7 +19,7 @@ pub async fn create_request( }; match data - .create_entry(JournalEntry::new(req.content, req.journal, user.id)) + .create_entry(JournalPost::new(req.content, req.journal, user.id)) .await { Ok(_) => Json(ApiReturn { @@ -64,7 +64,7 @@ pub async fn update_content_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_entry_content(id, user, req.content).await { + match data.update_post_content(id, user, req.content).await { Ok(_) => Json(ApiReturn { ok: true, message: "Entry updated".to_string(), @@ -86,7 +86,7 @@ pub async fn update_context_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_entry_context(id, user, req.context).await { + match data.update_post_context(id, user, req.context).await { Ok(_) => Json(ApiReturn { ok: true, message: "Entry updated".to_string(), diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 3b99781..c0ede8e 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -8,7 +8,7 @@ use axum::{ }; use serde::Deserialize; use tetratto_core::model::{ - journal::{JournalEntryContext, JournalPageReadAccess, JournalPageWriteAccess}, + journal::{JournalPostContext, JournalReadAccess, JournalWriteAccess}, reactions::AssetType, }; @@ -18,35 +18,35 @@ pub fn routes() -> Router { .route("/reactions", post(reactions::create_request)) .route("/reactions/{id}", get(reactions::get_request)) .route("/reactions/{id}", delete(reactions::delete_request)) - // journal pages - .route("/pages", post(journal::pages::create_request)) - .route("/pages/{id}", delete(journal::pages::delete_request)) + // journal journals + .route("/journals", post(journal::journals::create_request)) + .route("/journals/{id}", delete(journal::journals::delete_request)) .route( - "/pages/{id}/title", - post(journal::pages::update_title_request), + "/journals/{id}/title", + post(journal::journals::update_title_request), ) .route( - "/pages/{id}/prompt", - post(journal::pages::update_prompt_request), + "/journals/{id}/prompt", + post(journal::journals::update_prompt_request), ) .route( - "/pages/{id}/access/read", - post(journal::pages::update_read_access_request), + "/journals/{id}/access/read", + post(journal::journals::update_read_access_request), ) .route( - "/pages/{id}/access/write", - post(journal::pages::update_write_access_request), + "/journals/{id}/access/write", + post(journal::journals::update_write_access_request), ) - // journal entries - .route("/entries", post(journal::entries::create_request)) - .route("/entries/{id}", delete(journal::entries::delete_request)) + // journal posts + .route("/posts", post(journal::posts::create_request)) + .route("/posts/{id}", delete(journal::posts::delete_request)) .route( - "/entries/{id}/content", - post(journal::entries::update_content_request), + "/posts/{id}/content", + post(journal::posts::update_content_request), ) .route( - "/entries/{id}/context", - post(journal::entries::update_context_request), + "/posts/{id}/context", + post(journal::posts::update_context_request), ) // auth // global @@ -79,29 +79,29 @@ pub struct AuthProps { } #[derive(Deserialize)] -pub struct CreateJournalPage { +pub struct CreateJournal { pub title: String, pub prompt: String, } #[derive(Deserialize)] -pub struct UpdateJournalPageTitle { +pub struct UpdateJournalTitle { pub title: String, } #[derive(Deserialize)] -pub struct UpdateJournalPagePrompt { +pub struct UpdateJournalPrompt { pub prompt: String, } #[derive(Deserialize)] -pub struct UpdateJournalPageReadAccess { - pub access: JournalPageReadAccess, +pub struct UpdateJournalReadAccess { + pub access: JournalReadAccess, } #[derive(Deserialize)] -pub struct UpdateJournalPageWriteAccess { - pub access: JournalPageWriteAccess, +pub struct UpdateJournalWriteAccess { + pub access: JournalWriteAccess, } #[derive(Deserialize)] @@ -117,7 +117,7 @@ pub struct UpdateJournalEntryContent { #[derive(Deserialize)] pub struct UpdateJournalEntryContext { - pub context: JournalEntryContext, + pub context: JournalPostContext, } #[derive(Deserialize)] diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index c5f77cb..e2e1965 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -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) -> "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); } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 9356840..8f4df07 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -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(()) } diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index 787224a..9ae2b9c 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -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"); diff --git a/crates/core/src/database/drivers/sql/create_ipbans.sql b/crates/core/src/database/drivers/sql/create_ipbans.sql new file mode 100644 index 0000000..02a5553 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_ipbans.sql @@ -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 +) diff --git a/crates/core/src/database/drivers/sql/create_userblocks.sql b/crates/core/src/database/drivers/sql/create_userblocks.sql new file mode 100644 index 0000000..7336baa --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_userblocks.sql @@ -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 +) diff --git a/crates/core/src/database/drivers/sql/create_userfollows.sql b/crates/core/src/database/drivers/sql/create_userfollows.sql new file mode 100644 index 0000000..e6804bb --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_userfollows.sql @@ -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 +) diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 59c1b0f..f1098ac 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -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 ) diff --git a/crates/core/src/database/ipbans.rs b/crates/core/src/database/ipbans.rs new file mode 100644 index 0000000..c6df8ed --- /dev/null +++ b/crates/core/src/database/ipbans.rs @@ -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(()) + } +} diff --git a/crates/core/src/database/pages.rs b/crates/core/src/database/journals.rs similarity index 75% rename from crates/core/src/database/pages.rs rename to crates/core/src/database/journals.rs index ba62181..f6b962f 100644 --- a/crates/core/src/database/pages.rs +++ b/crates/core/src/database/journals.rs @@ -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); } diff --git a/crates/core/src/database/memberships.rs b/crates/core/src/database/memberships.rs index 59557dc..d38e24d 100644 --- a/crates/core/src/database/memberships.rs +++ b/crates/core/src/database/memberships.rs @@ -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 { + ) -> Result { 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())), diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 8527943..374c122 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -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::*; diff --git a/crates/core/src/database/notifications.rs b/crates/core/src/database/notifications.rs index 3ba8f39..90e4404 100644 --- a/crates/core/src/database/notifications.rs +++ b/crates/core/src/database/notifications.rs @@ -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()) diff --git a/crates/core/src/database/entries.rs b/crates/core/src/database/posts.rs similarity index 59% rename from crates/core/src/database/entries.rs rename to crates/core/src/database/posts.rs index 5666a53..b4d92cc 100644 --- a/crates/core/src/database/entries.rs +++ b/crates/core/src/database/posts.rs @@ -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); } diff --git a/crates/core/src/database/reactions.rs b/crates/core/src/database/reactions.rs index 3b9227a..fb767ec 100644 --- a/crates/core/src/database/reactions.rs +++ b/crates/core/src/database/reactions.rs @@ -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); } } diff --git a/crates/core/src/database/userblocks.rs b/crates/core/src/database/userblocks.rs new file mode 100644 index 0000000..874606d --- /dev/null +++ b/crates/core/src/database/userblocks.rs @@ -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 { + 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 { + 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(()) + } +} diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs new file mode 100644 index 0000000..97bd40b --- /dev/null +++ b/crates/core/src/database/userfollows.rs @@ -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 { + 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 { + 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(()) + } +} diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 51501a3..9d90c16 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -19,6 +19,9 @@ pub struct User { pub settings: UserSettings, pub tokens: Vec, pub permissions: FinePermission, + pub notification_count: usize, + pub follower_count: usize, + pub following_count: usize, } #[derive(Debug, Serialize, Deserialize)] @@ -48,6 +51,9 @@ impl User { settings: UserSettings::default(), tokens: Vec::new(), permissions: FinePermission::DEFAULT, + notification_count: 0, + follower_count: 0, + following_count: 0, } } @@ -97,3 +103,69 @@ impl Notification { } } } + +#[derive(Serialize, Deserialize)] +pub struct UserFollow { + pub id: usize, + pub created: usize, + pub initiator: usize, + pub receiver: usize, +} + +impl UserFollow { + /// Create a new [`UserFollow`]. + pub fn new(initiator: usize, receiver: usize) -> Self { + Self { + id: AlmostSnowflake::new(1234567890) + .to_string() + .parse::() + .unwrap(), + created: unix_epoch_timestamp() as usize, + initiator, + receiver, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct UserBlock { + pub id: usize, + pub created: usize, + pub initiator: usize, + pub receiver: usize, +} + +impl UserBlock { + /// Create a new [`UserBlock`]. + pub fn new(initiator: usize, receiver: usize) -> Self { + Self { + id: AlmostSnowflake::new(1234567890) + .to_string() + .parse::() + .unwrap(), + created: unix_epoch_timestamp() as usize, + initiator, + receiver, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct IpBan { + pub ip: String, + pub created: usize, + pub reason: String, + pub moderator: usize, +} + +impl IpBan { + /// Create a new [`IpBan`]. + pub fn new(ip: String, moderator: usize, reason: String) -> Self { + Self { + ip, + created: unix_epoch_timestamp() as usize, + reason, + moderator, + } + } +} diff --git a/crates/core/src/model/journal.rs b/crates/core/src/model/journal.rs index 2ba5c36..64ad203 100644 --- a/crates/core/src/model/journal.rs +++ b/crates/core/src/model/journal.rs @@ -4,7 +4,7 @@ use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp}; use super::journal_permissions::JournalPermission; #[derive(Serialize, Deserialize)] -pub struct JournalPage { +pub struct Journal { pub id: usize, pub created: usize, pub title: String, @@ -12,18 +12,18 @@ pub struct JournalPage { /// The ID of the owner of the journal page. pub owner: usize, /// Who can read the journal page. - pub read_access: JournalPageReadAccess, + pub read_access: JournalReadAccess, /// Who can write to the journal page (create journal entries belonging to it). /// /// The owner of the journal page (and moderators) are the ***only*** people /// capable of removing entries. - pub write_access: JournalPageWriteAccess, + pub write_access: JournalWriteAccess, pub likes: isize, pub dislikes: isize, } -impl JournalPage { - /// Create a new [`JournalPage`]. +impl Journal { + /// Create a new [`Journal`]. pub fn new(title: String, prompt: String, owner: usize) -> Self { Self { id: AlmostSnowflake::new(1234567890) @@ -34,17 +34,17 @@ impl JournalPage { title, prompt, owner, - read_access: JournalPageReadAccess::default(), - write_access: JournalPageWriteAccess::default(), + read_access: JournalReadAccess::default(), + write_access: JournalWriteAccess::default(), likes: 0, dislikes: 0, } } } -/// Who can read a [`JournalPage`]. +/// Who can read a [`Journal`]. #[derive(Serialize, Deserialize, PartialEq, Eq)] -pub enum JournalPageReadAccess { +pub enum JournalReadAccess { /// Everybody can view the journal page from the owner's profile. Everybody, /// Only people with the link to the journal page. @@ -53,15 +53,15 @@ pub enum JournalPageReadAccess { Private, } -impl Default for JournalPageReadAccess { +impl Default for JournalReadAccess { fn default() -> Self { Self::Everybody } } -/// Who can write to a [`JournalPage`]. +/// Who can write to a [`Journal`]. #[derive(Serialize, Deserialize, PartialEq, Eq)] -pub enum JournalPageWriteAccess { +pub enum JournalWriteAccess { /// Everybody (authenticated + anonymous users). Everybody, /// Authenticated users only. @@ -74,14 +74,14 @@ pub enum JournalPageWriteAccess { Owner, } -impl Default for JournalPageWriteAccess { +impl Default for JournalWriteAccess { fn default() -> Self { Self::Authenticated } } #[derive(Serialize, Deserialize)] -pub struct JournalPageMembership { +pub struct JournalMembership { pub id: usize, pub created: usize, pub owner: usize, @@ -89,7 +89,7 @@ pub struct JournalPageMembership { pub role: JournalPermission, } -impl JournalPageMembership { +impl JournalMembership { pub fn new(owner: usize, journal: usize, role: JournalPermission) -> Self { Self { id: AlmostSnowflake::new(1234567890) @@ -105,11 +105,11 @@ impl JournalPageMembership { } #[derive(Serialize, Deserialize)] -pub struct JournalEntryContext { +pub struct JournalPostContext { pub comments_enabled: bool, } -impl Default for JournalEntryContext { +impl Default for JournalPostContext { fn default() -> Self { Self { comments_enabled: true, @@ -118,21 +118,21 @@ impl Default for JournalEntryContext { } #[derive(Serialize, Deserialize)] -pub struct JournalEntry { +pub struct JournalPost { pub id: usize, pub created: usize, pub content: String, /// The ID of the owner of this entry. pub owner: usize, - /// The ID of the [`JournalPage`] this entry belongs to. + /// The ID of the [`Journal`] this entry belongs to. pub journal: usize, /// Extra information about the journal entry. - pub context: JournalEntryContext, + pub context: JournalPostContext, pub likes: isize, pub dislikes: isize, } -impl JournalEntry { +impl JournalPost { /// Create a new [`JournalEntry`]. pub fn new(content: String, journal: usize, owner: usize) -> Self { Self { @@ -144,7 +144,7 @@ impl JournalEntry { content, owner, journal, - context: JournalEntryContext::default(), + context: JournalPostContext::default(), likes: 0, dislikes: 0, } diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 527be07..d5423f2 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -21,6 +21,7 @@ bitflags! { const VIEW_AUDIT_LOG = 1 << 10; const MANAGE_MEMBERSHIPS = 1 << 11; const MANAGE_REACTIONS = 1 << 12; + const MANAGE_FOLLOWS = 1 << 13; const _ = !0; } diff --git a/crates/core/src/model/reactions.rs b/crates/core/src/model/reactions.rs index 697f72d..6e5c591 100644 --- a/crates/core/src/model/reactions.rs +++ b/crates/core/src/model/reactions.rs @@ -4,7 +4,7 @@ use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp}; /// All of the items which support reactions. #[derive(Serialize, Deserialize)] pub enum AssetType { - JournalPage, + Journal, JournalEntry, }