diff --git a/Cargo.lock b/Cargo.lock index 5dcdd045c..271397841 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2996,6 +2996,7 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "serde", + "serde_json", "serde_with", "serial_test", "tokio", diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 74369173b..6ddc67752 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -30,6 +30,7 @@ pub struct CreatePost { pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. pub custom_thumbnail: Option, + pub community_post_tags: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -124,6 +125,7 @@ pub struct EditPost { pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. pub custom_thumbnail: Option, + pub community_post_tags: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c715305bb..7f7328f65 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -283,3 +283,10 @@ impl InstanceId { self.0 } } + + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post id. +pub struct CommunityPostTagId(pub i32); \ No newline at end of file diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index fc418ec28..dab162a97 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -251,6 +251,18 @@ diesel::table! { } } +diesel::table! { + community_post_tag (id) { + id -> Int4, + ap_id -> Text, + community_id -> Int4, + name -> Text, + published -> Timestamptz, + updated -> Nullable, + deleted -> Nullable, + } +} + diesel::table! { custom_emoji (id) { id -> Int4, @@ -759,6 +771,13 @@ diesel::table! { } } +diesel::table! { + post_community_post_tag (post_id, community_post_tag_id) { + post_id -> Int4, + community_post_tag_id -> Int4, + } +} + diesel::table! { post_hide (person_id, post_id) { post_id -> Int4, @@ -974,6 +993,7 @@ diesel::joinable!(community_moderator -> community (community_id)); diesel::joinable!(community_moderator -> person (person_id)); diesel::joinable!(community_person_ban -> community (community_id)); diesel::joinable!(community_person_ban -> person (person_id)); +diesel::joinable!(community_post_tag -> community (community_id)); diesel::joinable!(custom_emoji -> local_site (local_site_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); @@ -1020,6 +1040,8 @@ diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); +diesel::joinable!(post_community_post_tag -> community_post_tag (community_post_tag_id)); +diesel::joinable!(post_community_post_tag -> post (post_id)); diesel::joinable!(post_hide -> person (person_id)); diesel::joinable!(post_hide -> post (post_id)); diesel::joinable!(post_like -> person (person_id)); @@ -1057,6 +1079,7 @@ diesel::allow_tables_to_appear_in_same_query!( community_language, community_moderator, community_person_ban, + community_post_tag, custom_emoji, custom_emoji_keyword, email_verification, @@ -1096,6 +1119,7 @@ diesel::allow_tables_to_appear_in_same_query!( person_post_aggregates, post, post_aggregates, + post_community_post_tag, post_hide, post_like, post_read, diff --git a/crates/db_schema/src/source/community_post_tag.rs b/crates/db_schema/src/source/community_post_tag.rs new file mode 100644 index 000000000..80555711e --- /dev/null +++ b/crates/db_schema/src/source/community_post_tag.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use ts_rs::TS; + +use crate::newtypes::{CommunityId, CommunityPostTagId}; + +/// A tag that can be assigned to a post within a community. +/// The tag object is created by the community moderators. +/// The assignment happens by the post creator and can be updated by the community moderators. +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct CommunityPostTag { + pub id: CommunityPostTagId, + pub ap_id: String, + pub community_id: CommunityId, + pub name: String, + pub published: DateTime, + pub updated: Option>, + pub deleted: Option> +} + + \ No newline at end of file diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index bbc8aafa2..73a1d8020 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -39,6 +39,7 @@ pub mod registration_application; pub mod secret; pub mod site; pub mod tagline; +pub mod community_post_tag; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). /// diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index b71c43495..d089ba7a1 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -508,6 +508,11 @@ pub mod functions { sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); sql_function!(fn set_config(setting_name: Text, new_value: Text, is_local: Bool) -> Text); + + sql_function! { + #[aggregate] + fn json_agg(obj: T) -> Json + } } pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*"; diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a..b2d2b2f8d 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -35,6 +35,7 @@ diesel-async = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } serde = { workspace = true } serde_with = { workspace = true } +serde_json = { workspace = true } tracing = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 0ec7e0a5d..63bb7b1b7 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -41,10 +41,13 @@ use lemmy_db_schema::{ post_like, post_read, post_saved, + post_community_post_tag, + community_post_tag }, source::{local_user::LocalUser, site::Site}, utils::{ functions::coalesce, + functions::json_agg, fuzzy_search, get_conn, limit_and_offset, @@ -214,6 +217,14 @@ fn queries<'a>() -> Queries< } else { Box::new(None::.into_sql::>()) }; + let community_post_tags: Box>> = + Box::new( + post_community_post_tag::table + .inner_join(community_post_tag::table) + .select(diesel::dsl::sql::("json_agg(community_post_tag.*)")) + .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) + .single_value(), + ); query .inner_join(person::table) @@ -247,6 +258,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - read_comments, post_aggregates::comments, ), + community_post_tags )) }; @@ -1764,6 +1776,7 @@ mod tests { hidden: false, saved: false, creator_blocked: false, + community_post_tags: None, }) } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 3c219d63f..55badcd06 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -3,23 +3,7 @@ use diesel::Queryable; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ - comment::Comment, - comment_report::CommentReport, - community::Community, - custom_emoji::CustomEmoji, - custom_emoji_keyword::CustomEmojiKeyword, - images::{ImageDetails, LocalImage}, - local_site::LocalSite, - local_site_rate_limit::LocalSiteRateLimit, - local_user::LocalUser, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::Person, - post::Post, - post_report::PostReport, - private_message::PrivateMessage, - private_message_report::PrivateMessageReport, - registration_application::RegistrationApplication, - site::Site, + comment::Comment, comment_report::CommentReport, community::Community, community_post_tag::CommunityPostTag, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, local_user_vote_display_mode::LocalUserVoteDisplayMode, person::Person, post::Post, post_report::PostReport, private_message::PrivateMessage, private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site }, SubscribedType, }; @@ -144,6 +128,7 @@ pub struct PostView { pub creator_blocked: bool, pub my_vote: Option, pub unread_comments: i64, + pub community_post_tags: Option } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] diff --git a/migrations/2024-08-17-144959_community-post-tags/down.sql b/migrations/2024-08-17-144959_community-post-tags/down.sql new file mode 100644 index 000000000..cbb0e9b81 --- /dev/null +++ b/migrations/2024-08-17-144959_community-post-tags/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +drop table post_community_post_tag; +drop table community_post_tag; \ No newline at end of file diff --git a/migrations/2024-08-17-144959_community-post-tags/up.sql b/migrations/2024-08-17-144959_community-post-tags/up.sql new file mode 100644 index 000000000..fd1633a6d --- /dev/null +++ b/migrations/2024-08-17-144959_community-post-tags/up.sql @@ -0,0 +1,17 @@ +-- a tag for a post, valid in a community. created by mods of a community +CREATE TABLE community_post_tag ( + id SERIAL PRIMARY KEY, + ap_id TEXT NOT NULL UNIQUE, + community_id INT NOT NULL REFERENCES community(id), + name TEXT NOT NULL, + published TIMESTAMPTZ NOT NULL, + updated TIMESTAMPTZ, + deleted TIMESTAMPTZ +); + +-- an association between a post and a community post tag. created/updated by the post author or mods of a community +CREATE TABLE post_community_post_tag ( + post_id INT NOT NULL REFERENCES post(id), + community_post_tag_id INT NOT NULL REFERENCES community_post_tag(id), + PRIMARY KEY (post_id, community_post_tag_id) +); diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 7b4b34158..673cb055e 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -183,11 +183,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/follow", web::post().to(follow_community)) .route("/block", web::post().to(block_community)) .route("/delete", web::post().to(delete_community)) + .route("/post_tags", web::get().to(get_community_post_tags)) // Mod Actions .route("/remove", web::post().to(remove_community)) .route("/transfer", web::post().to(transfer_community)) .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)), + .route("/mod", web::post().to(add_mod_to_community)) + .route("/post_tags", web::post().to(create_update_community_post_tag)) + .route("/post_tags/delete", web::post().to(delete_community_post_tag)), ) .service( web::scope("/federated_instances")