diff --git a/Cargo.lock b/Cargo.lock index 5ad7dc3..39b066f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,9 +648,9 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "contrasted" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b6cc9d477fe7331eca62e0b39b559a2cda428d9cf2292b5ad99fc9d950fca1" +checksum = "0ade94f58b0b9d71868d3dc34d065e9d027dc0d293f43d478616f7ef1160838f" [[package]] name = "cookie" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 73e5a1c..a9edbe5 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -32,4 +32,4 @@ regex = "1.11.1" serde_json = "1.0.140" mime_guess = "2.0.5" cf-turnstile = "0.2.0" -contrasted = "0.1.1" +contrasted = "0.1.2" diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs new file mode 100644 index 0000000..a73146a --- /dev/null +++ b/crates/core/src/database/channels.rs @@ -0,0 +1,134 @@ +use super::*; +use crate::cache::Cache; +use crate::model::{ + Error, Result, auth::User, permissions::FinePermission, + communities_permissions::CommunityPermission, channels::Channel, +}; +use crate::{auto_method, execute, get, query_row, query_rows, params}; + +#[cfg(feature = "sqlite")] +use rusqlite::Row; + +#[cfg(feature = "postgres")] +use tokio_postgres::Row; + +impl DataManager { + /// Get a [`Channel`] from an SQL row. + pub(crate) fn get_channel_from_row( + #[cfg(feature = "sqlite")] x: &Row<'_>, + #[cfg(feature = "postgres")] x: &Row, + ) -> Channel { + Channel { + id: get!(x->0(i64)) as usize, + community: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + created: get!(x->3(i64)) as usize, + minimum_role_read: get!(x->4(i32)) as u32, + minimum_role_write: get!(x->5(i32)) as u32, + position: get!(x->6(i32)) as usize, + } + } + + auto_method!(get_channel_by_id(usize)@get_channel_from_row -> "SELECT * FROM channels WHERE id = $1" --name="channel" --returns=Channel --cache-key-tmpl="atto.channel:{}"); + + /// Get all channels by user. + /// + /// # Arguments + /// * `community` - the ID of the community to fetch channels for + pub async fn get_channels_by_community(&self, community: 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 channels WHERE community = $1 ORDER BY position DESC", + &[&(community as i64)], + |x| { Self::get_channel_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("channel".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new channel in the database. + /// + /// # Arguments + /// * `data` - a mock [`Channel`] object to insert + pub async fn create_channel(&self, data: Channel) -> Result<()> { + let user = self.get_user_by_id(data.owner).await?; + + // check user permission in community + let membership = self + .get_membership_by_owner_community(user.id, data.community) + .await?; + + if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) + && !user.permissions.check(FinePermission::MANAGE_CHANNELS) + { + 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 channels VALUES ($1, $2, $3, $4, $5, $6, $7)", + params![ + &(data.id as i64), + &(data.community as i64), + &(data.owner as i64), + &(data.created as i64), + &(data.minimum_role_read as i32), + &(data.minimum_role_write as i32), + &(data.position as i32) + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(()) + } + + pub async fn delete_channel(&self, id: usize, user: User) -> Result<()> { + let channel = self.get_channel_by_id(id).await?; + + // check user permission in community + let membership = self + .get_membership_by_owner_community(user.id, channel.community) + .await?; + + if !membership.role.check(CommunityPermission::MANAGE_CHANNELS) { + 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 channels WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.channel:{}", id)).await; + Ok(()) + } + + auto_method!(update_channel_position(i32)@get_channel_by_id:MANAGE_COMMUNITIES -> "UPDATE channels SET position = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); + auto_method!(update_channel_minimum_role_read(i32)@get_channel_by_id:MANAGE_COMMUNITIES -> "UPDATE channels SET minimum_role_read = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); + auto_method!(update_channel_minimum_role_write(i32)@get_channel_by_id:MANAGE_COMMUNITIES -> "UPDATE channels SET minimum_role_write = $1 WHERE id = $2" --cache-key-tmpl="atto.channel:{}"); +} diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 6926f2b..2137092 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -28,6 +28,8 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap(); execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap(); execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap(); + execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap(); + execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap(); Ok(()) } diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index cb16682..67d5bb0 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -13,3 +13,5 @@ pub const CREATE_TABLE_USER_WARNINGS: &str = include_str!("./sql/create_user_war pub const CREATE_TABLE_REQUESTS: &str = include_str!("./sql/create_requests.sql"); pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sql"); pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql"); +pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql"); +pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql"); diff --git a/crates/core/src/database/drivers/sql/create_channels.sql b/crates/core/src/database/drivers/sql/create_channels.sql new file mode 100644 index 0000000..2198c71 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_channels.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS channels ( + id BIGINT NOT NULL PRIMARY KEY, + community BIGINT NOT NULL, + owner BIGINT NOT NULL, + created BIGINT NOT NULL, + minimum_role_read INT NOT NULL, + minimum_role_write INT NOT NULL, + position INT NOT NULL +) diff --git a/crates/core/src/database/drivers/sql/create_messages.sql b/crates/core/src/database/drivers/sql/create_messages.sql new file mode 100644 index 0000000..24096b6 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_messages.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS messages ( + id BIGINT NOT NULL PRIMARY KEY, + channel BIGINT NOT NULL, + owner BIGINT NOT NULL, + created BIGINT NOT NULL, + edited BIGINT NOT NULL, + content TEXT NOT NULL, + context TEXT NOT NULL +) diff --git a/crates/core/src/database/messages.rs b/crates/core/src/database/messages.rs new file mode 100644 index 0000000..3dc00bd --- /dev/null +++ b/crates/core/src/database/messages.rs @@ -0,0 +1,189 @@ +use super::*; +use crate::cache::Cache; +use crate::model::moderation::AuditLogEntry; +use crate::model::{ + Error, Result, auth::User, permissions::FinePermission, + communities_permissions::CommunityPermission, channels::Message, +}; +use crate::{auto_method, execute, get, query_row, query_rows, params}; + +#[cfg(feature = "sqlite")] +use rusqlite::Row; + +use tetratto_shared::unix_epoch_timestamp; +#[cfg(feature = "postgres")] +use tokio_postgres::Row; + +impl DataManager { + /// Get a [`Message`] from an SQL row. + pub(crate) fn get_message_from_row( + #[cfg(feature = "sqlite")] x: &Row<'_>, + #[cfg(feature = "postgres")] x: &Row, + ) -> Message { + Message { + id: get!(x->0(i64)) as usize, + channel: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + created: get!(x->3(i64)) as usize, + edited: get!(x->4(i64)) as usize, + content: get!(x->5(String)), + context: serde_json::from_str(&get!(x->6(String))).unwrap(), + } + } + + auto_method!(get_message_by_id(usize)@get_message_from_row -> "SELECT * FROM messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="atto.message:{}"); + + /// Get all messages by channel (paginated). + /// + /// # Arguments + /// * `channel` - the ID of the community to fetch channels for + /// * `batch` - the limit of items in each page + /// * `page` - the page number + pub async fn get_messages_by_channel( + &self, + channel: usize, + batch: usize, + page: 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 messages WHERE channel = $1 ORDER BY created DESC LIMIT $1 OFFSET $2", + &[&(channel as i64), &(batch as i64), &((page * batch) as i64)], + |x| { Self::get_message_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("message".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new message in the database. + /// + /// # Arguments + /// * `data` - a mock [`Message`] object to insert + pub async fn create_message(&self, data: Message) -> Result<()> { + let user = self.get_user_by_id(data.owner).await?; + let channel = self.get_channel_by_id(data.channel).await?; + + // check user permission in community + let membership = self + .get_membership_by_owner_community(user.id, channel.community) + .await?; + + if !membership.role.check_member() { + return Err(Error::NotAllowed); + } + + // check user permission to post in channel + let role = membership.role.bits(); + + if role < channel.minimum_role_write { + 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 messages VALUES ($1, $2, $3, $4, $5, $6, $7)", + params![ + &(data.id as i64), + &(data.channel as i64), + &(data.owner as i64), + &(data.created as i64), + &(data.edited as i64), + &data.content, + &serde_json::to_string(&data.context).unwrap() + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(()) + } + + pub async fn delete_message(&self, id: usize, user: User) -> Result<()> { + let message = self.get_message_by_id(id).await?; + let channel = self.get_channel_by_id(message.channel).await?; + + // check user permission in community + let membership = self + .get_membership_by_owner_community(user.id, channel.community) + .await?; + + if !membership.role.check(CommunityPermission::MANAGE_MESSAGES) + && !user.permissions.check(FinePermission::MANAGE_MESSAGES) + { + return Err(Error::NotAllowed); + } else if user.permissions.check(FinePermission::MANAGE_MESSAGES) { + self.create_audit_log_entry(AuditLogEntry::new( + user.id, + format!("invoked `delete_message` with x value `{id}`"), + )) + .await? + } + + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, "DELETE FROM messages WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.message:{}", id)).await; + Ok(()) + } + + pub async fn update_message_content(&self, id: usize, user: User, x: String) -> Result<()> { + let y = self.get_message_by_id(id).await?; + + if user.id != y.owner { + if !user.permissions.check(FinePermission::MANAGE_MESSAGES) { + return Err(Error::NotAllowed); + } else { + self.create_audit_log_entry(AuditLogEntry::new( + user.id, + format!("invoked `update_message_content` with x value `{id}`"), + )) + .await? + } + } + + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "UPDATE messages SET content = $1, edited = $2 WHERE id = $2", + params![&x, &(unix_epoch_timestamp() as i64), &(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // return + Ok(()) + } +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index b43d53d..a1d16b5 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -16,6 +16,11 @@ mod user_warnings; mod userblocks; mod userfollows; +#[cfg(feature = "redis")] +pub mod channels; +#[cfg(feature = "redis")] +pub mod messages; + #[cfg(feature = "sqlite")] pub use drivers::sqlite::*; diff --git a/crates/core/src/model/channels.rs b/crates/core/src/model/channels.rs index d1c6e34..0c3c523 100644 --- a/crates/core/src/model/channels.rs +++ b/crates/core/src/model/channels.rs @@ -17,12 +17,12 @@ pub struct Channel { /// The position of this channel in the UI. /// /// Top (0) to bottom. - pub order: usize, + pub position: usize, } impl Channel { /// Create a new [`Channel`]. - pub fn new(community: usize, owner: usize, order: usize) -> Self { + pub fn new(community: usize, owner: usize, position: usize) -> Self { Self { id: AlmostSnowflake::new(1234567890) .to_string() @@ -33,7 +33,7 @@ impl Channel { created: unix_epoch_timestamp() as usize, minimum_role_read: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(), minimum_role_write: (CommunityPermission::DEFAULT | CommunityPermission::MEMBER).bits(), - order, + position, } } } diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index c051ee0..0073e60 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -30,6 +30,8 @@ bitflags! { const SUPPORTER = 1 << 19; const MANAGE_REQUESTS = 1 << 20; const MANAGE_QUESTIONS = 1 << 21; + const MANAGE_CHANNELS = 1 << 22; + const MANAGE_MESSAGES = 1 << 23; const _ = !0; }