diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index ebe697907..84515b9fe 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -15,7 +15,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::SiteView; +use lemmy_db_views::structs::{CustomEmojiView, SiteView}; use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::{error::LemmyError, version, ConnectionId}; @@ -64,8 +64,8 @@ impl Perform for LeaveAdmin { let all_languages = Language::read_all(context.pool()).await?; let discussion_languages = SiteLanguage::read_local(context.pool()).await?; - let taglines_res = Tagline::get_all(context.pool(), site_view.local_site.id).await?; - let taglines = taglines_res.is_empty().then_some(taglines_res); + let taglines = Tagline::get_all(context.pool(), site_view.local_site.id).await?; + let custom_emojis = CustomEmojiView::get_all(context.pool(), site_view.local_site.id).await?; Ok(GetSiteResponse { site_view, @@ -77,6 +77,7 @@ impl Perform for LeaveAdmin { all_languages, discussion_languages, taglines, + custom_emojis, }) } } diff --git a/crates/api_common/src/custom_emoji.rs b/crates/api_common/src/custom_emoji.rs new file mode 100644 index 000000000..4652529d6 --- /dev/null +++ b/crates/api_common/src/custom_emoji.rs @@ -0,0 +1,42 @@ +use crate::sensitive::Sensitive; +use lemmy_db_schema::newtypes::CustomEmojiId; +use lemmy_db_views::structs::CustomEmojiView; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateCustomEmoji { + pub category: String, + pub shortcode: String, + pub image_url: Url, + pub alt_text: String, + pub keywords: Vec, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EditCustomEmoji { + pub id: CustomEmojiId, + pub category: String, + pub image_url: Url, + pub alt_text: String, + pub keywords: Vec, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct DeleteCustomEmoji { + pub id: CustomEmojiId, + pub auth: Sensitive, +} + +#[derive(Serialize, Deserialize)] +pub struct DeleteCustomEmojiResponse { + pub id: CustomEmojiId, + pub success: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomEmojiResponse { + pub custom_emoji: CustomEmojiView, +} diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 29686aba6..3c9d71216 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -2,6 +2,7 @@ pub mod comment; pub mod community; #[cfg(feature = "full")] pub mod context; +pub mod custom_emoji; pub mod person; pub mod post; pub mod private_message; diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 0000ecd3d..920cc6ec6 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -14,6 +14,7 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{ CommentView, + CustomEmojiView, LocalUserView, PostView, RegistrationApplicationView, @@ -224,7 +225,8 @@ pub struct GetSiteResponse { pub federated_instances: Option, // Federation may be disabled pub all_languages: Vec, pub discussion_languages: Vec, - pub taglines: Option>, + pub taglines: Vec, + pub custom_emojis: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/websocket/mod.rs b/crates/api_common/src/websocket/mod.rs index 76f17ae9f..21c533854 100644 --- a/crates/api_common/src/websocket/mod.rs +++ b/crates/api_common/src/websocket/mod.rs @@ -118,6 +118,10 @@ pub enum UserOperationCrud { GetPrivateMessages, EditPrivateMessage, DeletePrivateMessage, + //Emojis + CreateCustomEmoji, + EditCustomEmoji, + DeleteCustomEmoji, } #[derive(EnumString, Display, Debug, Clone)] diff --git a/crates/api_crud/src/custom_emoji/create.rs b/crates/api_crud/src/custom_emoji/create.rs new file mode 100644 index 000000000..42b6c5d91 --- /dev/null +++ b/crates/api_crud/src/custom_emoji/create.rs @@ -0,0 +1,54 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + context::LemmyContext, + custom_emoji::{CreateCustomEmoji, CustomEmojiResponse}, + utils::{get_local_user_view_from_jwt, is_admin}, +}; +use lemmy_db_schema::source::{ + custom_emoji::{CustomEmoji, CustomEmojiInsertForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + local_site::LocalSite, +}; +use lemmy_db_views::structs::CustomEmojiView; +use lemmy_utils::{error::LemmyError, ConnectionId}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreateCustomEmoji { + type Response = CustomEmojiResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &CreateCustomEmoji = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let local_site = LocalSite::read(context.pool()).await?; + // Make sure user is an admin + is_admin(&local_user_view)?; + + let emoji_form = CustomEmojiInsertForm::builder() + .local_site_id(local_site.id) + .shortcode(data.shortcode.to_lowercase().trim().to_string()) + .alt_text(data.alt_text.to_string()) + .category(data.category.to_string()) + .image_url(data.clone().image_url.into()) + .build(); + let emoji = CustomEmoji::create(context.pool(), &emoji_form).await?; + let mut keywords = vec![]; + for keyword in &data.keywords { + let keyword_form = CustomEmojiKeywordInsertForm::builder() + .custom_emoji_id(emoji.id) + .keyword(keyword.to_lowercase().trim().to_string()) + .build(); + keywords.push(keyword_form); + } + CustomEmojiKeyword::create(context.pool(), keywords).await?; + let view = CustomEmojiView::get(context.pool(), emoji.id).await?; + Ok(CustomEmojiResponse { custom_emoji: view }) + } +} diff --git a/crates/api_crud/src/custom_emoji/delete.rs b/crates/api_crud/src/custom_emoji/delete.rs new file mode 100644 index 000000000..db8c973ec --- /dev/null +++ b/crates/api_crud/src/custom_emoji/delete.rs @@ -0,0 +1,33 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + context::LemmyContext, + custom_emoji::{DeleteCustomEmoji, DeleteCustomEmojiResponse}, + utils::{get_local_user_view_from_jwt, is_admin}, +}; +use lemmy_db_schema::source::custom_emoji::CustomEmoji; +use lemmy_utils::{error::LemmyError, ConnectionId}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeleteCustomEmoji { + type Response = DeleteCustomEmojiResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &DeleteCustomEmoji = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + CustomEmoji::delete(context.pool(), data.id).await?; + Ok(DeleteCustomEmojiResponse { + id: data.id, + success: true, + }) + } +} diff --git a/crates/api_crud/src/custom_emoji/mod.rs b/crates/api_crud/src/custom_emoji/mod.rs new file mode 100644 index 000000000..b9d8b5577 --- /dev/null +++ b/crates/api_crud/src/custom_emoji/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod delete; +mod update; diff --git a/crates/api_crud/src/custom_emoji/update.rs b/crates/api_crud/src/custom_emoji/update.rs new file mode 100644 index 000000000..6233cbe3c --- /dev/null +++ b/crates/api_crud/src/custom_emoji/update.rs @@ -0,0 +1,54 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + context::LemmyContext, + custom_emoji::{CustomEmojiResponse, EditCustomEmoji}, + utils::{get_local_user_view_from_jwt, is_admin}, +}; +use lemmy_db_schema::source::{ + custom_emoji::{CustomEmoji, CustomEmojiUpdateForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + local_site::LocalSite, +}; +use lemmy_db_views::structs::CustomEmojiView; +use lemmy_utils::{error::LemmyError, ConnectionId}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditCustomEmoji { + type Response = CustomEmojiResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &EditCustomEmoji = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let local_site = LocalSite::read(context.pool()).await?; + // Make sure user is an admin + is_admin(&local_user_view)?; + + let emoji_form = CustomEmojiUpdateForm::builder() + .local_site_id(local_site.id) + .alt_text(data.alt_text.to_string()) + .category(data.category.to_string()) + .image_url(data.clone().image_url.into()) + .build(); + let emoji = CustomEmoji::update(context.pool(), data.id, &emoji_form).await?; + CustomEmojiKeyword::delete(context.pool(), data.id).await?; + let mut keywords = vec![]; + for keyword in &data.keywords { + let keyword_form = CustomEmojiKeywordInsertForm::builder() + .custom_emoji_id(emoji.id) + .keyword(keyword.to_lowercase().trim().to_string()) + .build(); + keywords.push(keyword_form); + } + CustomEmojiKeyword::create(context.pool(), keywords).await?; + let view = CustomEmojiView::get(context.pool(), emoji.id).await?; + Ok(CustomEmojiResponse { custom_emoji: view }) + } +} diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index d37dfbee2..70d5d44d4 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -4,6 +4,7 @@ use lemmy_utils::{error::LemmyError, ConnectionId}; mod comment; mod community; +mod custom_emoji; mod post; mod private_message; mod site; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 51457b883..c89dbef70 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::source::{ language::Language, tagline::Tagline, }; -use lemmy_db_views::structs::SiteView; +use lemmy_db_views::structs::{CustomEmojiView, SiteView}; use lemmy_db_views_actor::structs::{ CommunityBlockView, CommunityFollowerView, @@ -88,8 +88,8 @@ impl PerformCrud for GetSite { let all_languages = Language::read_all(context.pool()).await?; let discussion_languages = SiteLanguage::read_local(context.pool()).await?; - let taglines_res = Tagline::get_all(context.pool(), site_view.local_site.id).await?; - let taglines = (!taglines_res.is_empty()).then_some(taglines_res); + let taglines = Tagline::get_all(context.pool(), site_view.local_site.id).await?; + let custom_emojis = CustomEmojiView::get_all(context.pool(), site_view.local_site.id).await?; Ok(GetSiteResponse { site_view, @@ -101,6 +101,7 @@ impl PerformCrud for GetSite { all_languages, discussion_languages, taglines, + custom_emojis, }) } } diff --git a/crates/apub/src/activities/unfederated.rs b/crates/apub/src/activities/unfederated.rs index 6cb4d44d9..e2fff292d 100644 --- a/crates/apub/src/activities/unfederated.rs +++ b/crates/apub/src/activities/unfederated.rs @@ -21,6 +21,13 @@ use lemmy_api_common::{ ListCommunitiesResponse, TransferCommunity, }, + custom_emoji::{ + CreateCustomEmoji, + CustomEmojiResponse, + DeleteCustomEmoji, + DeleteCustomEmojiResponse, + EditCustomEmoji, + }, person::{ AddAdmin, AddAdminResponse, @@ -354,3 +361,15 @@ impl SendActivity for ListCommentReports { impl SendActivity for ResolveCommentReport { type Response = CommentReportResponse; } + +impl SendActivity for CreateCustomEmoji { + type Response = CustomEmojiResponse; +} + +impl SendActivity for EditCustomEmoji { + type Response = CustomEmojiResponse; +} + +impl SendActivity for DeleteCustomEmoji { + type Response = DeleteCustomEmojiResponse; +} diff --git a/crates/db_schema/src/impls/custom_emoji.rs b/crates/db_schema/src/impls/custom_emoji.rs new file mode 100644 index 000000000..cce35dfc7 --- /dev/null +++ b/crates/db_schema/src/impls/custom_emoji.rs @@ -0,0 +1,60 @@ +use crate::{ + newtypes::CustomEmojiId, + schema::{ + custom_emoji::dsl::custom_emoji, + custom_emoji_keyword::dsl::{custom_emoji_id, custom_emoji_keyword}, + }, + source::{ + custom_emoji::{CustomEmoji, CustomEmojiInsertForm, CustomEmojiUpdateForm}, + custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm}, + }, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +impl CustomEmoji { + pub async fn create(pool: &DbPool, form: &CustomEmojiInsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(custom_emoji) + .values(form) + .get_result::(conn) + .await + } + pub async fn update( + pool: &DbPool, + emoji_id: CustomEmojiId, + form: &CustomEmojiUpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(custom_emoji.find(emoji_id)) + .set(form) + .get_result::(conn) + .await + } + pub async fn delete(pool: &DbPool, emoji_id: CustomEmojiId) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::delete(custom_emoji.find(emoji_id)) + .execute(conn) + .await + } +} + +impl CustomEmojiKeyword { + pub async fn create( + pool: &DbPool, + form: Vec, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + insert_into(custom_emoji_keyword) + .values(form) + .get_results::(conn) + .await + } + pub async fn delete(pool: &DbPool, emoji_id: CustomEmojiId) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::delete(custom_emoji_keyword.filter(custom_emoji_id.eq(emoji_id))) + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 456565adf..915d1c8e2 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -5,6 +5,7 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod custom_emoji; pub mod email_verification; pub mod federation_allowlist; pub mod federation_blocklist; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 7e2465bb7..da2c9b9e5 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -108,6 +108,10 @@ pub struct InstanceId(i32); #[cfg_attr(feature = "full", derive(DieselNewType))] pub struct LocalSiteId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +pub struct CustomEmojiId(i32); + #[repr(transparent)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 870754a29..b4fbefe2e 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -751,6 +751,27 @@ table! { } } +table! { + custom_emoji (id) { + id -> Int4, + local_site_id -> Int4, + shortcode -> Varchar, + image_url -> Text, + alt_text -> Text, + category -> Text, + published -> Timestamp, + updated -> Nullable, + } +} + +table! { + custom_emoji_keyword (id) { + id -> Int4, + custom_emoji_id -> Int4, + keyword -> Varchar, + } +} + joinable!(person_block -> person (person_id)); joinable!(comment -> person (creator_id)); @@ -836,6 +857,8 @@ joinable!(federation_blocklist -> instance (instance_id)); joinable!(local_site -> site (site_id)); joinable!(local_site_rate_limit -> local_site (local_site_id)); joinable!(tagline -> local_site (local_site_id)); +joinable!(custom_emoji -> local_site (local_site_id)); +joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); allow_tables_to_appear_in_same_query!( activity, @@ -896,5 +919,7 @@ allow_tables_to_appear_in_same_query!( federation_blocklist, local_site, local_site_rate_limit, - person_follower + person_follower, + custom_emoji, + custom_emoji_keyword, ); diff --git a/crates/db_schema/src/source/custom_emoji.rs b/crates/db_schema/src/source/custom_emoji.rs new file mode 100644 index 000000000..8a0928191 --- /dev/null +++ b/crates/db_schema/src/source/custom_emoji.rs @@ -0,0 +1,44 @@ +use crate::newtypes::{CustomEmojiId, DbUrl, LocalSiteId}; +#[cfg(feature = "full")] +use crate::schema::custom_emoji; +use serde::{Deserialize, Serialize}; +use typed_builder::TypedBuilder; + +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::local_site::LocalSite)) +)] +pub struct CustomEmoji { + pub id: CustomEmojiId, + pub local_site_id: LocalSiteId, + pub shortcode: String, + pub image_url: DbUrl, + pub alt_text: String, + pub category: String, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] +pub struct CustomEmojiInsertForm { + pub local_site_id: LocalSiteId, + pub shortcode: String, + pub image_url: DbUrl, + pub alt_text: String, + pub category: String, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = custom_emoji))] +pub struct CustomEmojiUpdateForm { + pub local_site_id: LocalSiteId, + pub image_url: DbUrl, + pub alt_text: String, + pub category: String, +} diff --git a/crates/db_schema/src/source/custom_emoji_keyword.rs b/crates/db_schema/src/source/custom_emoji_keyword.rs new file mode 100644 index 000000000..caebcd83f --- /dev/null +++ b/crates/db_schema/src/source/custom_emoji_keyword.rs @@ -0,0 +1,26 @@ +use crate::newtypes::CustomEmojiId; +#[cfg(feature = "full")] +use crate::schema::custom_emoji_keyword; +use serde::{Deserialize, Serialize}; +use typed_builder::TypedBuilder; + +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = custom_emoji_keyword))] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::custom_emoji::CustomEmoji)) +)] +pub struct CustomEmojiKeyword { + pub id: i32, + pub custom_emoji_id: CustomEmojiId, + pub keyword: String, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = custom_emoji_keyword))] +pub struct CustomEmojiKeywordInsertForm { + pub custom_emoji_id: CustomEmojiId, + pub keyword: String, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index dbc83211f..9aab4b90b 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -6,6 +6,8 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod custom_emoji; +pub mod custom_emoji_keyword; pub mod email_verification; pub mod federation_allowlist; pub mod federation_blocklist; diff --git a/crates/db_views/src/custom_emoji_view.rs b/crates/db_views/src/custom_emoji_view.rs new file mode 100644 index 000000000..66d583e32 --- /dev/null +++ b/crates/db_views/src/custom_emoji_view.rs @@ -0,0 +1,82 @@ +use crate::structs::CustomEmojiView; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::{CustomEmojiId, LocalSiteId}, + schema::{custom_emoji, custom_emoji_keyword}, + source::{custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword}, + utils::{get_conn, DbPool}, +}; +use std::collections::HashMap; + +type CustomEmojiTuple = (CustomEmoji, Option); + +impl CustomEmojiView { + pub async fn get(pool: &DbPool, emoji_id: CustomEmojiId) -> Result { + let conn = &mut get_conn(pool).await?; + let emojis = custom_emoji::table + .find(emoji_id) + .left_join( + custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), + ) + .select(( + custom_emoji::all_columns, + custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) + )) + .load::(conn) + .await?; + if let Some(emoji) = CustomEmojiView::from_tuple_to_vec(emojis) + .into_iter() + .next() + { + Ok(emoji) + } else { + Err(diesel::result::Error::NotFound) + } + } + + pub async fn get_all(pool: &DbPool, for_local_site_id: LocalSiteId) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let emojis = custom_emoji::table + .filter(custom_emoji::local_site_id.eq(for_local_site_id)) + .left_join( + custom_emoji_keyword::table.on(custom_emoji_keyword::custom_emoji_id.eq(custom_emoji::id)), + ) + .order(custom_emoji::category) + .then_order_by(custom_emoji::id) + .select(( + custom_emoji::all_columns, + custom_emoji_keyword::all_columns.nullable(), // (or all the columns if you want) + )) + .load::(conn) + .await?; + + Ok(CustomEmojiView::from_tuple_to_vec(emojis)) + } + + fn from_tuple_to_vec(items: Vec) -> Vec { + let mut result = Vec::new(); + let mut hash: HashMap> = HashMap::new(); + for item in &items { + let emoji_id: CustomEmojiId = item.0.id; + if let std::collections::hash_map::Entry::Vacant(e) = hash.entry(emoji_id) { + e.insert(Vec::new()); + result.push(CustomEmojiView { + custom_emoji: item.0.clone(), + keywords: Vec::new(), + }) + } + if let Some(item_keyword) = &item.1 { + if let Some(keywords) = hash.get_mut(&emoji_id) { + keywords.push(item_keyword.clone()) + } + } + } + for emoji in &mut result { + if let Some(keywords) = hash.get_mut(&emoji.custom_emoji.id) { + emoji.keywords = keywords.clone(); + } + } + result + } +} diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 9f1500237..8abf776ba 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -6,6 +6,8 @@ pub mod comment_report_view; #[cfg(feature = "full")] pub mod comment_view; #[cfg(feature = "full")] +pub mod custom_emoji_view; +#[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] pub mod post_report_view; diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 4521221f2..b3b2b1c7a 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -4,6 +4,8 @@ use lemmy_db_schema::{ comment::Comment, comment_report::CommentReport, community::Community, + custom_emoji::CustomEmoji, + custom_emoji_keyword::CustomEmojiKeyword, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, @@ -113,3 +115,8 @@ pub struct SiteView { pub local_site_rate_limit: LocalSiteRateLimit, pub counts: SiteAggregates, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomEmojiView { + pub custom_emoji: CustomEmoji, + pub keywords: Vec, +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7ed15150c..b46c1f8e2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -70,7 +70,7 @@ services: restart: always pictrs: - image: asonix/pictrs:0.3.1 + image: asonix/pictrs:0.4.0-beta.19 # this needs to match the pictrs url in lemmy.hjson hostname: pictrs # we can set options to pictrs like this, here we set max. image size and forced format for conversion @@ -82,6 +82,11 @@ services: - PICTRS__API_KEY=API_KEY - RUST_LOG=debug - RUST_BACKTRACE=full + - PICTRS__MEDIA__VIDEO_CODEC=vp9 + - PICTRS__MEDIA__GIF__MAX_WIDTH=256 + - PICTRS__MEDIA__GIF__MAX_HEIGHT=256 + - PICTRS__MEDIA__GIF__MAX_AREA=65536 + - PICTRS__MEDIA__GIF__MAX_FRAME_COUNT=400 user: 991:991 volumes: - ./volumes/pictrs:/mnt diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index ab512f49f..13576df10 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -22,7 +22,7 @@ services: pictrs: restart: always - image: asonix/pictrs:0.3.1 + image: asonix/pictrs:0.4.0-beta.19 user: 991:991 volumes: - ./volumes/pictrs_alpha:/mnt diff --git a/migrations/2023-02-11-173347_custom_emojis/down.sql b/migrations/2023-02-11-173347_custom_emojis/down.sql new file mode 100644 index 000000000..8994bb3be --- /dev/null +++ b/migrations/2023-02-11-173347_custom_emojis/down.sql @@ -0,0 +1,2 @@ +drop table custom_emoji_keyword; +drop table custom_emoji; \ No newline at end of file diff --git a/migrations/2023-02-11-173347_custom_emojis/up.sql b/migrations/2023-02-11-173347_custom_emojis/up.sql new file mode 100644 index 000000000..79a21b206 --- /dev/null +++ b/migrations/2023-02-11-173347_custom_emojis/up.sql @@ -0,0 +1,19 @@ +create table custom_emoji ( + id serial primary key, + local_site_id int references local_site on update cascade on delete cascade not null, + shortcode varchar(128) not null UNIQUE, + image_url text not null UNIQUE, + alt_text text not null, + category text not null, + published timestamp without time zone default now() not null, + updated timestamp without time zone +); + +create table custom_emoji_keyword ( + id serial primary key, + custom_emoji_id int references custom_emoji on update cascade on delete cascade not null, + keyword varchar(128) not null, + UNIQUE (custom_emoji_id, keyword) +); + +create index idx_custom_emoji_category on custom_emoji (id,category); diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 882efc8ce..8ab6980e5 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -31,6 +31,7 @@ use lemmy_api_common::{ TransferCommunity, }, context::LemmyContext, + custom_emoji::{CreateCustomEmoji, DeleteCustomEmoji, EditCustomEmoji}, person::{ AddAdmin, BanPerson, @@ -352,6 +353,16 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/community", web::post().to(route_post::)) .route("/post", web::post().to(route_post::)) .route("/comment", web::post().to(route_post::)), + ) + .service( + web::scope("/custom_emoji") + .wrap(rate_limit.message()) + .route("", web::post().to(route_post_crud::)) + .route("", web::put().to(route_post_crud::)) + .route( + "/delete", + web::post().to(route_post_crud::), + ), ), ); } diff --git a/src/api_routes_websocket.rs b/src/api_routes_websocket.rs index fdaba6ba5..b8d8f2c24 100644 --- a/src/api_routes_websocket.rs +++ b/src/api_routes_websocket.rs @@ -32,6 +32,7 @@ use lemmy_api_common::{ TransferCommunity, }, context::LemmyContext, + custom_emoji::{CreateCustomEmoji, DeleteCustomEmoji, EditCustomEmoji}, person::{ AddAdmin, BanPerson, @@ -389,6 +390,16 @@ pub async fn match_websocket_operation_crud( UserOperationCrud::GetComment => { do_websocket_operation_crud::(context, id, op, data).await } + // Emojis + UserOperationCrud::CreateCustomEmoji => { + do_websocket_operation_crud::(context, id, op, data).await + } + UserOperationCrud::EditCustomEmoji => { + do_websocket_operation_crud::(context, id, op, data).await + } + UserOperationCrud::DeleteCustomEmoji => { + do_websocket_operation_crud::(context, id, op, data).await + } } }