From 2467a0af12c3abb434fe6860146b9feac64dfd5c Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 12 Dec 2024 14:38:16 +0000 Subject: [PATCH 01/23] Consider remote instance as dead if it returns any status 4xx or 5xx (#5256) * Consider remote instance as dead if it returns any status 4xx or 5xx (ref #3134) * remove dbg --- src/scheduled_tasks.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 52962877f..3406bf694 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -579,13 +579,13 @@ async fn build_update_instance_form( // This is the only kind of error that means the instance is dead return None; }; + let status = res.status(); + if status.is_client_error() || status.is_server_error() { + return None; + } // In this block, returning `None` is ignored, and only means not writing nodeinfo to db async { - if res.status().is_client_error() { - return None; - } - let node_info_url = res .json::() .await From 6a9f924d2047b126900ccb813e194145eb5012ce Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 12 Dec 2024 15:03:55 +0000 Subject: [PATCH 02/23] More test coverage for user deletion (#5259) --- api_tests/src/user.spec.ts | 10 ++++++++++ crates/api_crud/src/user/my_user.rs | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index f7f80aecb..d1d6144f5 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -74,6 +74,9 @@ test("Set some user settings, check that they are federated", async () => { test("Delete user", async () => { let user = await registerUser(alpha, alphaUrl); + let user_profile = await getMyUser(user); + let person_id = user_profile.local_user_view.person.id; + let actor_id = user_profile.local_user_view.person.actor_id; // make a local post and comment let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541")) @@ -101,6 +104,10 @@ test("Delete user", async () => { expect(remoteComment).toBeDefined(); await deleteUser(user); + await expect(getMyUser(user)).rejects.toStrictEqual(Error("incorrect_login")); + await expect(getPersonDetails(user, person_id)).rejects.toStrictEqual( + Error("not_found"), + ); // check that posts and comments are marked as deleted on other instances. // use get methods to avoid refetching from origin instance @@ -118,6 +125,9 @@ test("Delete user", async () => { (await getComments(alpha, remoteComment.post_id)).comments[0].comment .deleted, ).toBe(true); + await expect( + getPersonDetails(user, remoteComment.creator_id), + ).rejects.toStrictEqual(Error("not_found")); }); test("Requests with invalid auth should be treated as unauthenticated", async () => { diff --git a/crates/api_crud/src/user/my_user.rs b/crates/api_crud/src/user/my_user.rs index 805c9dabb..f7a92eb99 100644 --- a/crates/api_crud/src/user/my_user.rs +++ b/crates/api_crud/src/user/my_user.rs @@ -1,5 +1,5 @@ use actix_web::web::{Data, Json}; -use lemmy_api_common::{context::LemmyContext, site::MyUserInfo}; +use lemmy_api_common::{context::LemmyContext, site::MyUserInfo, utils::check_user_valid}; use lemmy_db_schema::source::{ actor_language::LocalUserLanguage, community_block::CommunityBlock, @@ -15,6 +15,8 @@ pub async fn get_my_user( local_user_view: LocalUserView, context: Data, ) -> LemmyResult> { + check_user_valid(&local_user_view.person)?; + // Build the local user with parallel queries and add it to site response let person_id = local_user_view.person.id; let local_user_id = local_user_view.local_user.id; From 8d91543a13c753827f221acc700cc41e541cadf2 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 12 Dec 2024 15:06:38 +0000 Subject: [PATCH 03/23] Allow admins to view deleted users (fixes #5249) (#5258) * Allow admins to view deleted users (fixes #5249) * remove check --- crates/api/src/community/ban.rs | 2 +- crates/api/src/local_user/ban_person.rs | 2 +- crates/api/src/local_user/block.rs | 2 +- crates/api_common/src/utils.rs | 2 -- crates/apub/src/api/read_person.rs | 8 +++-- crates/apub/src/api/resolve_object.rs | 2 +- crates/db_views_actor/src/community_view.rs | 4 +-- crates/db_views_actor/src/person_view.rs | 34 +++++++++++++-------- crates/utils/src/error.rs | 5 ++- 9 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 8689d2563..547838fa7 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -110,7 +110,7 @@ pub async fn ban_from_community( ModBanFromCommunity::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; + let person_view = PersonView::read(&mut context.pool(), data.person_id, false).await?; ActivityChannel::submit_activity( SendActivityData::BanFromCommunity { diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index f929433f0..715bd206d 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -88,7 +88,7 @@ pub async fn ban_from_site( ModBan::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), person.id).await?; + let person_view = PersonView::read(&mut context.pool(), person.id, false).await?; ban_nonlocal_user_from_local_communities( &local_user_view, diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 80532e897..3aee554d4 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -48,7 +48,7 @@ pub async fn user_block_person( .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; } - let person_view = PersonView::read(&mut context.pool(), target_id).await?; + let person_view = PersonView::read(&mut context.pool(), target_id, false).await?; Ok(Json(BlockPersonResponse { person_view, blocked: data.block, diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 21154c823..80f559edb 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -123,8 +123,6 @@ pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> { check_user_valid(&local_user_view.person)?; if !local_user_view.local_user.admin { Err(LemmyErrorType::NotAnAdmin)? - } else if local_user_view.person.banned { - Err(LemmyErrorType::Banned)? } else { Ok(()) } diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index fac68cd63..72dce8140 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -4,7 +4,7 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ context::LemmyContext, person::{GetPersonDetails, GetPersonDetailsResponse}, - utils::{check_private_instance, read_site_for_actor}, + utils::{check_private_instance, is_admin, read_site_for_actor}, }; use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type}; use lemmy_db_views::{ @@ -45,7 +45,11 @@ pub async fn read_person( // You don't need to return settings for the user, since this comes back with GetSite // `my_user` - let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; + let is_admin = local_user_view + .as_ref() + .map(|l| is_admin(l).is_ok()) + .unwrap_or_default(); + let person_view = PersonView::read(&mut context.pool(), person_details_id, is_admin).await?; let sort = data.sort; let page = data.page; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index 04d489592..8d2cd384f 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -60,7 +60,7 @@ async fn convert_response( } }, SearchableObjects::PersonOrCommunity(pc) => match *pc { - UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?), + UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id, is_admin).await?), UserOrCommunity::Community(c) => { res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?) } diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index f6ce82d37..8bcf50ba3 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -188,7 +188,7 @@ impl CommunityView { let is_mod = CommunityModeratorView::check_is_community_moderator(pool, community_id, person_id).await; if is_mod.is_ok() - || PersonView::read(pool, person_id) + || PersonView::read(pool, person_id, false) .await .is_ok_and(|t| t.is_admin) { @@ -206,7 +206,7 @@ impl CommunityView { let is_mod_of_any = CommunityModeratorView::is_community_moderator_of_any(pool, person_id).await; if is_mod_of_any.is_ok() - || PersonView::read(pool, person_id) + || PersonView::read(pool, person_id, false) .await .is_ok_and(|t| t.is_admin) { diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index 39d1ac27c..b90ab7811 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -58,12 +58,11 @@ fn post_to_person_sort_type(sort: PostSortType) -> PersonSortType { } fn queries<'a>( -) -> Queries, impl ListFn<'a, PersonView, ListMode>> { +) -> Queries, impl ListFn<'a, PersonView, ListMode>> { let all_joins = move |query: person::BoxedQuery<'a, Pg>| { query .inner_join(person_aggregates::table) .left_join(local_user::table) - .filter(person::deleted.eq(false)) .select(( person::all_columns, person_aggregates::all_columns, @@ -71,14 +70,17 @@ fn queries<'a>( )) }; - let read = move |mut conn: DbConn<'a>, person_id: PersonId| async move { - all_joins(person::table.find(person_id).into_boxed()) - .first(&mut conn) - .await + let read = move |mut conn: DbConn<'a>, params: (PersonId, bool)| async move { + let (person_id, is_admin) = params; + let mut query = all_joins(person::table.find(person_id).into_boxed()); + if !is_admin { + query = query.filter(person::deleted.eq(false)); + } + query.first(&mut conn).await }; let list = move |mut conn: DbConn<'a>, mode: ListMode| async move { - let mut query = all_joins(person::table.into_boxed()); + let mut query = all_joins(person::table.into_boxed()).filter(person::deleted.eq(false)); match mode { ListMode::Admins => { query = query @@ -135,8 +137,12 @@ fn queries<'a>( } impl PersonView { - pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result { - queries().read(pool, person_id).await + pub async fn read( + pool: &mut DbPool<'_>, + person_id: PersonId, + is_admin: bool, + ) -> Result { + queries().read(pool, (person_id, is_admin)).await } pub async fn admins(pool: &mut DbPool<'_>) -> Result, Error> { @@ -243,9 +249,13 @@ mod tests { ) .await?; - let read = PersonView::read(pool, data.alice.id).await; + let read = PersonView::read(pool, data.alice.id, false).await; assert!(read.is_err()); + // only admin can view deleted users + let read = PersonView::read(pool, data.alice.id, true).await; + assert!(read.is_ok()); + let list = PersonQuery { sort: Some(PostSortType::New), ..Default::default() @@ -303,10 +313,10 @@ mod tests { assert_length!(1, list); assert_eq!(list[0].person.id, data.alice.id); - let is_admin = PersonView::read(pool, data.alice.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.alice.id, false).await?.is_admin; assert!(is_admin); - let is_admin = PersonView::read(pool, data.bob.id).await?.is_admin; + let is_admin = PersonView::read(pool, data.bob.id, false).await?.is_admin; assert!(!is_admin); cleanup(data, pool).await diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index f45bc271f..4f28aaa32 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -113,7 +113,6 @@ pub enum LemmyErrorType { SystemErrLogin, CouldntSetAllRegistrationsAccepted, CouldntSetAllEmailVerified, - Banned, BlockedUrl, CouldntGetComments, CouldntGetPosts, @@ -328,9 +327,9 @@ cfg_if! { #[test] fn deserializes_no_message() -> LemmyResult<()> { - let err = LemmyError::from(LemmyErrorType::Banned).error_response(); + let err = LemmyError::from(LemmyErrorType::BlockedUrl).error_response(); let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?; - assert_eq!(&json, "{\"error\":\"banned\"}"); + assert_eq!(&json, "{\"error\":\"blocked_url\"}"); Ok(()) } From d346890b1f3eda453833ceba67f4c685e4f6e162 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Tue, 17 Dec 2024 15:01:53 +0000 Subject: [PATCH 04/23] Increase metadata fetch limit to 1 MB (fixes #5208) (#5266) --- crates/api_common/src/request.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index c6f86b806..02e889872 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -51,9 +51,11 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder { #[tracing::instrument(skip_all)] pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult { info!("Fetching site metadata for url: {}", url); - // We only fetch the first 64kB of data in order to not waste bandwidth especially for large - // binary files - let bytes_to_fetch = 64 * 1024; + // We only fetch the first MB of data in order to not waste bandwidth especially for large + // binary files. This high limit is particularly needed for youtube, which includes a lot of + // javascript code before the opengraph tags. Mastodon also uses a 1 MB limit: + // https://github.com/mastodon/mastodon/blob/295ad6f19a016b3f16e1201ffcbb1b3ad6b455a2/app/lib/request.rb#L213 + let bytes_to_fetch = 1024 * 1024; let response = context .client() .get(url.as_str()) From a2a5cb091a2892793179760275cb56529891bcb6 Mon Sep 17 00:00:00 2001 From: phiresky Date: Wed, 18 Dec 2024 14:54:35 +0100 Subject: [PATCH 05/23] Community post tags (part 1) (#4997) * partial post tags implementation * fixes * fix lints * schema fix * chore: restructure / rename tag tables * chore: fix post view tests * format * lint * expect used * chore: update code to maybe final version * add ts-rs optionals * remove error context * clippy --- Cargo.lock | 2 + crates/api_common/src/post.rs | 6 +- crates/db_schema/src/impls/mod.rs | 1 + crates/db_schema/src/impls/tag.rs | 53 ++ crates/db_schema/src/newtypes.rs | 6 + crates/db_schema/src/schema.rs | 25 + crates/db_schema/src/source/mod.rs | 1 + crates/db_schema/src/source/tag.rs | 57 ++ crates/db_schema/src/utils.rs | 5 + crates/db_views/Cargo.toml | 2 + crates/db_views/src/lib.rs | 2 + crates/db_views/src/post_tags_view.rs | 30 + crates/db_views/src/post_view.rs | 654 +++++++++++------- crates/db_views/src/structs.rs | 13 + .../down.sql | 4 + .../up.sql | 23 + 16 files changed, 648 insertions(+), 236 deletions(-) create mode 100644 crates/db_schema/src/impls/tag.rs create mode 100644 crates/db_schema/src/source/tag.rs create mode 100644 crates/db_views/src/post_tags_view.rs create mode 100644 migrations/2024-12-17-144959_community-post-tags/down.sql create mode 100644 migrations/2024-12-17-144959_community-post-tags/up.sql diff --git a/Cargo.lock b/Cargo.lock index eebb1ce1a..c7215d79f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2685,8 +2685,10 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "serde", + "serde_json", "serde_with", "serial_test", + "test-context", "tokio", "tracing", "ts-rs", diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 405de3a92..fb16c8aa8 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,5 +1,5 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId, TagId}, ListingType, PostFeatureType, PostSortType, @@ -37,6 +37,8 @@ pub struct CreatePost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, @@ -164,6 +166,8 @@ pub struct EditPost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index d4ea47800..2d7a16c2c 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -35,4 +35,5 @@ pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; +pub mod tag; pub mod tagline; diff --git a/crates/db_schema/src/impls/tag.rs b/crates/db_schema/src/impls/tag.rs new file mode 100644 index 000000000..c0171e04c --- /dev/null +++ b/crates/db_schema/src/impls/tag.rs @@ -0,0 +1,53 @@ +use crate::{ + newtypes::TagId, + schema::{post_tag, tag}, + source::tag::{PostTagInsertForm, Tag, TagInsertForm}, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; + +#[async_trait] +impl Crud for Tag { + type InsertForm = TagInsertForm; + + type UpdateForm = TagInsertForm; + + type IdType = TagId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(tag::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + pid: TagId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(tag::table.find(pid)) + .set(form) + .get_result::(conn) + .await + } +} + +impl PostTagInsertForm { + pub async fn insert_tag_associations( + pool: &mut DbPool<'_>, + tags: &[PostTagInsertForm], + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + insert_into(post_tag::table) + .values(tags) + .execute(conn) + .await?; + Ok(()) + } +} diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c28be8222..963f847a5 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -283,3 +283,9 @@ 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 internal tag id. +pub struct TagId(pub i32); diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 66a65d143..77122f7cb 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -826,6 +826,14 @@ diesel::table! { } } +diesel::table! { + post_tag (post_id, tag_id) { + post_id -> Int4, + tag_id -> Int4, + published -> Timestamptz, + } +} + diesel::table! { private_message (id) { id -> Int4, @@ -951,6 +959,18 @@ diesel::table! { } } +diesel::table! { + tag (id) { + id -> Int4, + ap_id -> Text, + name -> Text, + community_id -> Int4, + published -> Timestamptz, + updated -> Nullable, + deleted -> Bool, + } +} + diesel::table! { tagline (id) { id -> Int4, @@ -1032,6 +1052,8 @@ diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); +diesel::joinable!(post_tag -> post (post_id)); +diesel::joinable!(post_tag -> tag (tag_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); @@ -1039,6 +1061,7 @@ diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); +diesel::joinable!(tag -> community (community_id)); diesel::allow_tables_to_appear_in_same_query!( admin_allow_instance, @@ -1098,6 +1121,7 @@ diesel::allow_tables_to_appear_in_same_query!( post_actions, post_aggregates, post_report, + post_tag, private_message, private_message_report, received_activity, @@ -1108,5 +1132,6 @@ diesel::allow_tables_to_appear_in_same_query!( site, site_aggregates, site_language, + tag, tagline, ); diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 86def9691..6230d004d 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -40,6 +40,7 @@ pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; +pub mod tag; pub mod tagline; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). diff --git a/crates/db_schema/src/source/tag.rs b/crates/db_schema/src/source/tag.rs new file mode 100644 index 000000000..265d864c3 --- /dev/null +++ b/crates/db_schema/src/source/tag.rs @@ -0,0 +1,57 @@ +use crate::newtypes::{CommunityId, DbUrl, PostId, TagId}; +#[cfg(feature = "full")] +use crate::schema::{post_tag, tag}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +/// 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. +/// +/// A tag is a federatable object that gives additional context to another object, which can be +/// displayed and filtered on currently, we only have community post tags, which is a tag that is +/// created by post authors as well as mods of a community, to categorize a post. in the future we +/// may add more tag types, depending on the requirements, this will lead to either expansion of +/// this table (community_id optional, addition of tag_type enum) or split of this table / creation +/// of new tables. +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = tag))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct Tag { + pub id: TagId, + pub ap_id: DbUrl, + pub name: String, + /// the community that owns this tag + pub community_id: CommunityId, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub updated: Option>, + pub deleted: bool, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = tag))] +pub struct TagInsertForm { + pub ap_id: DbUrl, + pub name: String, + pub community_id: CommunityId, + // default now + pub published: Option>, + pub updated: Option>, + pub deleted: bool, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = post_tag))] +pub struct PostTagInsertForm { + pub post_id: PostId, + pub tag_id: TagId, +} diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index aa213887e..5bbf007ae 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -547,6 +547,11 @@ pub mod functions { // really this function is variadic, this just adds the two-argument version define_sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); + + define_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..8b0669ff9 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 } @@ -46,3 +47,4 @@ serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url = { workspace = true } +test-context = "0.3.0" diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93c7409d..3c1fcd84a 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -14,6 +14,8 @@ pub mod local_user_view; #[cfg(feature = "full")] pub mod post_report_view; #[cfg(feature = "full")] +pub mod post_tags_view; +#[cfg(feature = "full")] pub mod post_view; #[cfg(feature = "full")] pub mod private_message_report_view; diff --git a/crates/db_views/src/post_tags_view.rs b/crates/db_views/src/post_tags_view.rs new file mode 100644 index 000000000..5d1492567 --- /dev/null +++ b/crates/db_views/src/post_tags_view.rs @@ -0,0 +1,30 @@ +//! see post_view.rs for the reason for this json decoding +use crate::structs::PostTags; +use diesel::{ + deserialize::FromSql, + pg::{Pg, PgValue}, + serialize::ToSql, + sql_types::{self, Nullable}, +}; + +impl FromSql, Pg> for PostTags { + fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value::(value)?) + } + fn from_nullable_sql( + bytes: Option<::RawValue<'_>>, + ) -> diesel::deserialize::Result { + match bytes { + Some(bytes) => Self::from_sql(bytes), + None => Ok(Self { tags: vec![] }), + } + } +} + +impl ToSql, Pg> for PostTags { + fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + >::to_sql(&value, &mut out.reborrow()) + } +} diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index c6d1b036f..6ed89e364 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,7 +5,9 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, + sql_types, BoolExpressionMethods, + BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -32,6 +34,8 @@ use lemmy_db_schema::{ post, post_actions, post_aggregates, + post_tag, + tag, }, source::{ community::{CommunityFollower, CommunityFollowerState}, @@ -80,6 +84,31 @@ fn queries<'a>() -> Queries< // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { + // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. + // This is a simple way to join m rows into n rows without duplicating the data and getting + // complex diesel types. In pure SQL you would usually do this either using a LEFT JOIN + then + // aggregating the results in the application code. But this results in a lot of duplicate + // data transferred (since each post will be returned once per tag that it has) and more + // complicated application code. The diesel docs suggest doing three separate sequential queries + // in this case (see https://diesel.rs/guides/relations.html#many-to-many-or-mn ): First fetch + // the posts, then fetch all relevant post-tag-association tuples from the db, and then fetch + // all the relevant tag objects. + // + // If we want to filter by post tag we will have to add + // separate logic below since this subquery can't affect filtering, but it is simple (`WHERE + // exists (select 1 from post_community_post_tags where community_post_tag_id in (1,2,3,4)`). + let post_tags: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + > = Box::new( + post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post_aggregates::post_id)) + .filter(tag::deleted.eq(false)) + .single_value(), + ); query .inner_join(person::table) .inner_join(community::table) @@ -136,6 +165,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), + post_tags, )) }; @@ -603,11 +633,13 @@ impl<'a> PostQuery<'a> { } } +#[allow(clippy::indexing_slicing)] +#[expect(clippy::expect_used)] #[cfg(test)] mod tests { use crate::{ post_view::{PaginationCursorData, PostQuery, PostView}, - structs::LocalUserView, + structs::{LocalUserView, PostTags}, }; use chrono::Utc; use diesel_async::SimpleAsyncConnection; @@ -651,29 +683,33 @@ mod tests { PostUpdateForm, }, site::Site, + tag::{PostTagInsertForm, Tag, TagInsertForm}, }, traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, - utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, + utils::{build_db_pool, get_conn, uplete, ActualDbPool, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use pretty_assertions::assert_eq; use serial_test::serial; use std::time::{Duration, Instant}; + use test_context::{test_context, AsyncTestContext}; use url::Url; const POST_WITH_ANOTHER_TITLE: &str = "Another title"; const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; const POST: &str = "post"; + const POST_WITH_TAGS: &str = "post with tags"; fn names(post_views: &[PostView]) -> Vec<&str> { post_views.iter().map(|i| i.post.name.as_str()).collect() } struct Data { + pool: ActualDbPool, inserted_instance: Instance, local_user_view: LocalUserView, blocked_local_user_view: LocalUserView, @@ -681,10 +717,19 @@ mod tests { inserted_community: Community, inserted_post: Post, inserted_bot_post: Post, + inserted_post_with_tags: Post, + tag_1: Tag, + tag_2: Tag, site: Site, } impl Data { + fn pool(&self) -> ActualDbPool { + self.pool.clone() + } + pub fn pool2(&self) -> DbPool<'_> { + DbPool::Pool(&self.pool) + } fn default_post_query(&self) -> PostQuery<'_> { PostQuery { sort: Some(PostSortType::New), @@ -692,129 +737,206 @@ mod tests { ..Default::default() } } - } - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + async fn setup() -> LemmyResult { + let actual_pool = build_db_pool()?; + let pool = &mut (&actual_pool).into(); + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); - let inserted_person = Person::create(pool, &new_person).await?; + let inserted_person = Person::create(pool, &new_person).await?; - let local_user_form = LocalUserInsertForm { - admin: Some(true), - ..LocalUserInsertForm::test_form(inserted_person.id) - }; - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let local_user_form = LocalUserInsertForm { + admin: Some(true), + ..LocalUserInsertForm::test_form(inserted_person.id) + }; + let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let new_bot = PersonInsertForm { - bot_account: Some(true), - ..PersonInsertForm::test_form(inserted_instance.id, "mybot") - }; + let new_bot = PersonInsertForm { + bot_account: Some(true), + ..PersonInsertForm::test_form(inserted_instance.id, "mybot") + }; - let inserted_bot = Person::create(pool, &new_bot).await?; + let inserted_bot = Person::create(pool, &new_bot).await?; - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test_community_3".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; + let new_community = CommunityInsertForm::new( + inserted_instance.id, + "test_community_3".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &new_community).await?; - // Test a person block, make sure the post query doesn't include their post - let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); + // Test a person block, make sure the post query doesn't include their post + let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); - let inserted_blocked_person = Person::create(pool, &blocked_person).await?; + let inserted_blocked_person = Person::create(pool, &blocked_person).await?; - let inserted_blocked_local_user = LocalUser::create( - pool, - &LocalUserInsertForm::test_form(inserted_blocked_person.id), - vec![], - ) - .await?; - - let post_from_blocked_person = PostInsertForm { - language_id: Some(LanguageId(1)), - ..PostInsertForm::new( - POST_BY_BLOCKED_PERSON.to_string(), - inserted_blocked_person.id, - inserted_community.id, + let inserted_blocked_local_user = LocalUser::create( + pool, + &LocalUserInsertForm::test_form(inserted_blocked_person.id), + vec![], ) - }; - Post::create(pool, &post_from_blocked_person).await?; + .await?; - // block that person - let person_block = PersonBlockForm { - person_id: inserted_person.id, - target_id: inserted_blocked_person.id, - }; + let post_from_blocked_person = PostInsertForm { + language_id: Some(LanguageId(1)), + ..PostInsertForm::new( + POST_BY_BLOCKED_PERSON.to_string(), + inserted_blocked_person.id, + inserted_community.id, + ) + }; + Post::create(pool, &post_from_blocked_person).await?; - PersonBlock::block(pool, &person_block).await?; + // block that person + let person_block = PersonBlockForm { + person_id: inserted_person.id, + target_id: inserted_blocked_person.id, + }; - // A sample post - let new_post = PostInsertForm { - language_id: Some(LanguageId(47)), - ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) - }; - let inserted_post = Post::create(pool, &new_post).await?; + PersonBlock::block(pool, &person_block).await?; - let new_bot_post = PostInsertForm::new( - POST_BY_BOT.to_string(), - inserted_bot.id, - inserted_community.id, - ); - let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + // Two community post tags + let tag_1 = Tag::create( + pool, + &TagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag1", inserted_community.actor_id))?.into(), + name: "Test Tag 1".into(), + community_id: inserted_community.id, + published: None, + updated: None, + deleted: false, + }, + ) + .await?; + let tag_2 = Tag::create( + pool, + &TagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag2", inserted_community.actor_id))?.into(), + name: "Test Tag 2".into(), + community_id: inserted_community.id, + published: None, + updated: None, + deleted: false, + }, + ) + .await?; - let local_user_view = LocalUserView { - local_user: inserted_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_person, - counts: Default::default(), - }; - let blocked_local_user_view = LocalUserView { - local_user: inserted_blocked_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_blocked_person, - counts: Default::default(), - }; + // A sample post + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) + }; - let site = Site { - id: Default::default(), - name: String::new(), - sidebar: None, - published: Default::default(), - updated: None, - icon: None, - banner: None, - description: None, - actor_id: Url::parse("http://example.com")?.into(), - last_refreshed_at: Default::default(), - inbox_url: Url::parse("http://example.com")?.into(), - private_key: None, - public_key: String::new(), - instance_id: Default::default(), - content_warning: None, - }; + let inserted_post = Post::create(pool, &new_post).await?; - Ok(Data { - inserted_instance, - local_user_view, - blocked_local_user_view, - inserted_bot, - inserted_community, - inserted_post, - inserted_bot_post, - site, - }) + let new_bot_post = PostInsertForm::new( + POST_BY_BOT.to_string(), + inserted_bot.id, + inserted_community.id, + ); + let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + + // A sample post with tags + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new( + POST_WITH_TAGS.to_string(), + inserted_person.id, + inserted_community.id, + ) + }; + + let inserted_post_with_tags = Post::create(pool, &new_post).await?; + let inserted_tags = vec![ + PostTagInsertForm { + post_id: inserted_post_with_tags.id, + tag_id: tag_1.id, + }, + PostTagInsertForm { + post_id: inserted_post_with_tags.id, + tag_id: tag_2.id, + }, + ]; + PostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; + + let local_user_view = LocalUserView { + local_user: inserted_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_person, + counts: Default::default(), + }; + let blocked_local_user_view = LocalUserView { + local_user: inserted_blocked_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_blocked_person, + counts: Default::default(), + }; + + let site = Site { + id: Default::default(), + name: String::new(), + sidebar: None, + published: Default::default(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("http://example.com")?.into(), + last_refreshed_at: Default::default(), + inbox_url: Url::parse("http://example.com")?.into(), + private_key: None, + public_key: String::new(), + instance_id: Default::default(), + content_warning: None, + }; + + Ok(Data { + pool: actual_pool, + inserted_instance, + local_user_view, + blocked_local_user_view, + inserted_bot, + inserted_community, + inserted_post, + inserted_bot_post, + inserted_post_with_tags, + tag_1, + tag_2, + site, + }) + } + async fn teardown(data: Data) -> LemmyResult<()> { + let pool = &mut data.pool2(); + // let pool = &mut (&pool).into(); + let num_deleted = Post::delete(pool, data.inserted_post.id).await?; + Community::delete(pool, data.inserted_community.id).await?; + Person::delete(pool, data.local_user_view.person.id).await?; + Person::delete(pool, data.inserted_bot.id).await?; + Person::delete(pool, data.blocked_local_user_view.person.id).await?; + Instance::delete(pool, data.inserted_instance.id).await?; + assert_eq!(1, num_deleted); + + Ok(()) + } + } + impl AsyncTestContext for Data { + async fn setup() -> Self { + Data::setup().await.expect("setup failed") + } + async fn teardown(self) { + Data::teardown(self).await.expect("teardown failed") + } } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_with_person() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_with_person(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; let local_user_form = LocalUserUpdateForm { show_bot_accounts: Some(false), @@ -823,12 +945,14 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + // remove tags post + read_post_listing.remove(0); let post_listing_single_with_person = PostView::read( pool, @@ -838,7 +962,7 @@ mod tests { ) .await?; - let expected_post_listing_with_user = expected_post_view(&data, pool).await?; + let expected_post_listing_with_user = expected_post_view(data, pool).await?; // Should be only one person, IE the bot post, and blocked should be missing assert_eq!( @@ -864,17 +988,19 @@ mod tests { .list(&data.site, pool) .await?; // should include bot post which has "undetermined" language - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_with_bots)); - - cleanup(data, pool).await + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_with_bots) + ); + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_no_person() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_no_person(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let read_post_listing_multiple_no_person = PostQuery { community_id: Some(data.inserted_community.id), @@ -887,32 +1013,31 @@ mod tests { let read_post_listing_single_no_person = PostView::read(pool, data.inserted_post.id, None, false).await?; - let expected_post_listing_no_person = expected_post_view(&data, pool).await?; + let expected_post_listing_no_person = expected_post_view(data, pool).await?; // Should be 2 posts, with the bot post, and the blocked assert_eq!( - vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + vec![POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], names(&read_post_listing_multiple_no_person) ); assert_eq!( Some(&expected_post_listing_no_person), - read_post_listing_multiple_no_person.get(1) + read_post_listing_multiple_no_person.get(2) ); assert_eq!( expected_post_listing_no_person, read_post_listing_single_no_person ); - - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_title_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_title_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // A post which contains the search them 'Post' not in the title (but in the body) let new_post = PostInsertForm { @@ -950,6 +1075,7 @@ mod tests { assert_eq!( vec![ POST_WITH_ANOTHER_TITLE, + POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON @@ -959,19 +1085,19 @@ mod tests { // Should be 3 posts when we search for title only assert_eq!( - vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + vec![POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], names(&read_post_listing_by_title_only) ); Post::delete(pool, inserted_post.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_block_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_block_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let community_block = CommunityBlockForm { person_id: data.local_user_view.person.id, @@ -989,15 +1115,15 @@ mod tests { assert_eq!(read_post_listings_with_person_after_block, vec![]); CommunityBlock::unblock(pool, &community_block).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_like() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_like(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; let post_like_form = PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1); @@ -1020,7 +1146,7 @@ mod tests { ) .await?; - let mut expected_post_with_upvote = expected_post_view(&data, pool).await?; + let mut expected_post_with_upvote = expected_post_view(data, pool).await?; expected_post_with_upvote.my_vote = Some(1); expected_post_with_upvote.counts.score = 1; expected_post_with_upvote.counts.upvotes = 1; @@ -1033,26 +1159,27 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + read_post_listing.remove(0); assert_eq!(vec![expected_post_with_upvote], read_post_listing); let like_removed = PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id).await?; assert_eq!(uplete::Count::only_deleted(1), like_removed); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_liked_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_liked_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Like both the bot post, and your own // The liked_only should not show your own post @@ -1087,15 +1214,15 @@ mod tests { // Should be no posts assert_eq!(read_disliked_post_listing, vec![]); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_saved_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Save only the bot post // The saved_only should only show the bot post @@ -1115,15 +1242,15 @@ mod tests { // This should only include the bot post, not the one you created assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn creator_info() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn creator_info(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Make one of the inserted persons a moderator let person_id = data.local_user_view.person.id; @@ -1145,23 +1272,24 @@ mod tests { .collect::>(); let expected_post_listing = vec![ + ("tegan".to_owned(), true, true), ("mybot".to_owned(), false, false), ("tegan".to_owned(), true, true), ]; assert_eq!(expected_post_listing, post_listing); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_person_language() -> LemmyResult<()> { + async fn post_listing_person_language(data: &mut Data) -> LemmyResult<()> { const EL_POSTO: &str = "el posto"; - let pool = &build_db_pool()?; + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let spanish_id = Language::read_id_from_code(pool, "es").await?; @@ -1180,17 +1308,23 @@ mod tests { let post_listings_all = data.default_post_query().list(&data.site, pool).await?; // no language filters specified, all posts should be returned - assert_eq!(vec![EL_POSTO, POST_BY_BOT, POST], names(&post_listings_all)); + assert_eq!( + vec![EL_POSTO, POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_all) + ); LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id).await?; let post_listing_french = data.default_post_query().list(&data.site, pool).await?; // only one post in french and one undetermined should be returned - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listing_french)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listing_french) + ); assert_eq!( Some(french_id), - post_listing_french.get(1).map(|p| p.post.language_id) + post_listing_french.get(2).map(|p| p.post.language_id) ); LocalUserLanguage::update( @@ -1207,6 +1341,7 @@ mod tests { .map(|p| (p.post.name, p.post.language_id)) .collect::>(); let expected_post_listings_french_und = vec![ + (POST_WITH_TAGS.to_owned(), french_id), (POST_BY_BOT.to_owned(), UNDETERMINED_ID), (POST.to_owned(), french_id), ]; @@ -1214,15 +1349,15 @@ mod tests { // french post and undetermined language post should be returned assert_eq!(expected_post_listings_french_und, post_listings_french_und); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_removed() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_removed(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Remove the post Post::update( @@ -1237,7 +1372,7 @@ mod tests { // Make sure you don't see the removed post in the results let post_listings_no_admin = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_no_admin)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_no_admin)); // Removed bot post is shown to admins on its profile page data.local_user_view.local_user.admin = true; @@ -1249,15 +1384,15 @@ mod tests { .await?; assert_eq!(vec![POST_BY_BOT], names(&post_listings_is_admin)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_deleted() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_deleted(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Delete the post Post::update( @@ -1288,15 +1423,15 @@ mod tests { assert_eq!(expect_contains_deleted, contains_deleted); } - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hidden_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hidden_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; Community::update( pool, @@ -1324,17 +1459,17 @@ mod tests { let posts = data.default_post_query().list(&data.site, pool).await?; assert!(!posts.is_empty()); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_instance_block() -> LemmyResult<()> { + async fn post_listing_instance_block(data: &mut Data) -> LemmyResult<()> { const POST_FROM_BLOCKED_INSTANCE: &str = "post on blocked instance"; - let pool = &build_db_pool()?; + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let blocked_instance = Instance::read_or_create(pool, "another_domain.tld".to_string()).await?; @@ -1359,7 +1494,12 @@ mod tests { // no instance block, should return all posts let post_listings_all = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_all) ); @@ -1372,7 +1512,10 @@ mod tests { // now posts from communities on that instance should be hidden let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_blocked)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_blocked) + ); assert!(post_listings_blocked .iter() .all(|p| p.post.id != post_from_blocked_instance.id)); @@ -1381,20 +1524,25 @@ mod tests { InstanceBlock::unblock(pool, &block_form).await?; let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_blocked) ); Instance::delete(pool, blocked_instance.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn pagination_includes_each_post_once() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn pagination_includes_each_post_once(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let community_form = CommunityInsertForm::new( data.inserted_instance.id, @@ -1496,15 +1644,15 @@ mod tests { assert_eq!(inserted_post_ids, listed_post_ids); Community::delete(pool, inserted_community.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_read() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_read(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Make sure local user hides read posts let local_user_form = LocalUserUpdateForm { @@ -1520,7 +1668,7 @@ mod tests { // Make sure you don't see the read post in the results let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_read)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_hide_read)); // Test with the show_read override as true let post_listings_show_read_true = PostQuery { @@ -1530,7 +1678,7 @@ mod tests { .list(&data.site, pool) .await?; assert_eq!( - vec![POST_BY_BOT, POST], + vec![POST_WITH_TAGS, POST_BY_BOT, POST], names(&post_listings_show_read_true) ); @@ -1541,16 +1689,19 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST], names(&post_listings_show_read_false)); - cleanup(data, pool).await + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_show_read_false) + ); + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_hidden() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_hidden(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Mark a post as hidden PostHide::hide( @@ -1562,7 +1713,10 @@ mod tests { // Make sure you don't see the hidden post in the results let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_hide_hidden) + ); // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { @@ -1573,20 +1727,23 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_hidden) + ); // Make sure that hidden field is true. - assert!(&post_listings_show_hidden.first().is_some_and(|p| p.hidden)); + assert!(&post_listings_show_hidden.get(1).is_some_and(|p| p.hidden)); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_hide_nsfw() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_hide_nsfw(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Mark a post as nsfw let update_form = PostUpdateForm { @@ -1594,11 +1751,11 @@ mod tests { ..Default::default() }; - Post::update(pool, data.inserted_bot_post.id, &update_form).await?; + Post::update(pool, data.inserted_post_with_tags.id, &update_form).await?; // Make sure you don't see the nsfw post in the regular results let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_nsfw)); + assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_hide_nsfw)); // Make sure it does come back with the show_nsfw option let post_listings_show_nsfw = PostQuery { @@ -1609,22 +1766,19 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_nsfw) + ); // Make sure that nsfw field is true. - assert!(&post_listings_show_nsfw.first().is_some_and(|p| p.post.nsfw)); - - cleanup(data, pool).await - } - - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - let num_deleted = Post::delete(pool, data.inserted_post.id).await?; - Community::delete(pool, data.inserted_community.id).await?; - Person::delete(pool, data.local_user_view.person.id).await?; - Person::delete(pool, data.inserted_bot.id).await?; - Person::delete(pool, data.blocked_local_user_view.person.id).await?; - Instance::delete(pool, data.inserted_instance.id).await?; - assert_eq!(1, num_deleted); + assert!( + &post_listings_show_nsfw + .first() + .ok_or(LemmyErrorType::NotFound)? + .post + .nsfw + ); Ok(()) } @@ -1746,15 +1900,16 @@ mod tests { hidden: false, saved: false, creator_blocked: false, + tags: PostTags::default(), }) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn local_only_instance() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); + async fn local_only_instance(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; Community::update( pool, @@ -1779,7 +1934,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, authenticated_query.len()); + assert_eq!(3, authenticated_query.len()); let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await; assert!(unauthenticated_post.is_err()); @@ -1793,16 +1948,15 @@ mod tests { .await; assert!(authenticated_post.is_ok()); - cleanup(data, pool).await?; Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_local_user_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_local_user_banned_from_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Test that post view shows if local user is blocked from community let banned_from_comm_person = PersonInsertForm::test_form(data.inserted_instance.id, "jill"); @@ -1837,15 +1991,15 @@ mod tests { assert!(post_view.banned_from_community); Person::delete(pool, inserted_banned_from_comm_person.id).await?; - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_local_user_not_banned_from_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_local_user_not_banned_from_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; let post_view = PostView::read( pool, @@ -1857,15 +2011,15 @@ mod tests { assert!(!post_view.banned_from_community); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn speed_check() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn speed_check(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Make sure the post_view query is less than this time let duration_max = Duration::from_millis(80); @@ -1913,15 +2067,15 @@ mod tests { duration_max ); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listings_no_comments_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listings_no_comments_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let data = init_data(pool).await?; // Create a comment for a post let comment_form = CommentInsertForm::new( @@ -1941,17 +2095,20 @@ mod tests { .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT], names(&post_listings_no_comments)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT], + names(&post_listings_no_comments) + ); - cleanup(data, pool).await + Ok(()) } + #[test_context(Data)] #[tokio::test] #[serial] - async fn post_listing_private_community() -> LemmyResult<()> { - let pool = &build_db_pool()?; + async fn post_listing_private_community(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); let pool = &mut pool.into(); - let mut data = init_data(pool).await?; // Mark community as private Community::update( @@ -2003,7 +2160,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, read_post_listing.len()); + assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, data.inserted_post.id, @@ -2030,7 +2187,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, read_post_listing.len()); + assert_eq!(3, read_post_listing.len()); let post_view = PostView::read( pool, data.inserted_post.id, @@ -2040,6 +2197,33 @@ mod tests { .await; assert!(post_view.is_ok()); - cleanup(data, pool).await + Ok(()) + } + + #[test_context(Data)] + #[tokio::test] + #[serial] + async fn post_tags_present(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); + let pool = &mut pool.into(); + + let post_view = PostView::read( + pool, + data.inserted_post_with_tags.id, + Some(&data.local_user_view.local_user), + false, + ) + .await?; + + assert_eq!(2, post_view.tags.tags.len()); + assert_eq!(data.tag_1.name, post_view.tags.tags[0].name); + assert_eq!(data.tag_2.name, post_view.tags.tags[1].name); + + let all_posts = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(2, all_posts[0].tags.tags.len()); // post with tags + assert_eq!(0, all_posts[1].tags.tags.len()); // bot post + assert_eq!(0, all_posts[2].tags.tags.len()); // normal post + + Ok(()) } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 4586fbcac..a95376a1a 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -1,5 +1,7 @@ #[cfg(feature = "full")] use diesel::Queryable; +#[cfg(feature = "full")] +use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types}; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ @@ -20,6 +22,7 @@ use lemmy_db_schema::{ private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site, + tag::Tag, }, SubscribedType, }; @@ -151,6 +154,7 @@ pub struct PostView { #[cfg_attr(feature = "full", ts(optional))] pub my_vote: Option, pub unread_comments: i64, + pub tags: PostTags, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] @@ -237,3 +241,12 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] +#[serde(transparent)] +#[cfg_attr(feature = "full", diesel(sql_type = Nullable))] +/// we wrap this in a struct so we can implement FromSqlRow for it +pub struct PostTags { + pub tags: Vec, +} diff --git a/migrations/2024-12-17-144959_community-post-tags/down.sql b/migrations/2024-12-17-144959_community-post-tags/down.sql new file mode 100644 index 000000000..9e6e2299f --- /dev/null +++ b/migrations/2024-12-17-144959_community-post-tags/down.sql @@ -0,0 +1,4 @@ +DROP TABLE post_tag; + +DROP TABLE tag; + diff --git a/migrations/2024-12-17-144959_community-post-tags/up.sql b/migrations/2024-12-17-144959_community-post-tags/up.sql new file mode 100644 index 000000000..f0c596e09 --- /dev/null +++ b/migrations/2024-12-17-144959_community-post-tags/up.sql @@ -0,0 +1,23 @@ +-- a tag is a federatable object that gives additional context to another object, which can be displayed and filtered on +-- currently, we only have community post tags, which is a tag that is created by post authors as well as mods of a community, +-- to categorize a post. in the future we may add more tag types, depending on the requirements, +-- this will lead to either expansion of this table (community_id optional, addition of tag_type enum) +-- or split of this table / creation of new tables. +CREATE TABLE tag ( + id serial PRIMARY KEY, + ap_id text NOT NULL UNIQUE, + name text NOT NULL, + community_id int NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE, + published timestamptz NOT NULL DEFAULT now(), + updated timestamptz, + deleted boolean NOT NULL DEFAULT FALSE +); + +-- an association between a post and a tag. created/updated by the post author or mods of a community +CREATE TABLE post_tag ( + post_id int NOT NULL REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE, + tag_id int NOT NULL REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE, + published timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (post_id, tag_id) +); + From 8b78ddeb68daf7cde07d3204414b7bcdc5cdbef5 Mon Sep 17 00:00:00 2001 From: Integral Date: Fri, 20 Dec 2024 04:42:01 +0800 Subject: [PATCH 06/23] refactor: avoid using format! when String creation is unnecessary (#5268) --- crates/apub/src/fetcher/markdown_links.rs | 3 ++- crates/routes/src/feeds.rs | 4 ++-- crates/utils/src/utils/markdown/image_links.rs | 3 ++- src/scheduled_tasks.rs | 6 ++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs index d83aae515..a5e51caa7 100644 --- a/crates/apub/src/fetcher/markdown_links.rs +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -42,7 +42,8 @@ pub async fn markdown_rewrite_remote_links( let mut local_url = local_url.to_string(); // restore title if let Some(extra) = extra { - local_url = format!("{local_url} {extra}"); + local_url.push(' '); + local_url.push_str(extra); } src.replace_range(start..end, local_url.as_str()); } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 55e9cc7f3..cd1ca3e98 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -454,7 +454,6 @@ fn build_item( protocol_and_hostname: &str, ) -> LemmyResult { // TODO add images - let author_url = format!("{protocol_and_hostname}/u/{creator_name}"); let guid = Some(Guid { permalink: true, value: url.to_owned(), @@ -464,7 +463,8 @@ fn build_item( Ok(Item { title: Some(format!("Reply from {creator_name}")), author: Some(format!( - "/u/{creator_name} (link)" + "/u/{creator_name} (link)", + format_args!("{protocol_and_hostname}/u/{creator_name}") )), pub_date: Some(published.to_rfc2822()), comments: Some(url.to_owned()), diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index 0990b1bc7..7914452ff 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -24,7 +24,8 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { ); // restore custom emoji format if let Some(extra) = extra { - proxied = format!("{proxied} {extra}"); + proxied.push(' '); + proxied.push_str(extra); } src.replace_range(start..end, &proxied); } diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 3406bf694..53c0b888b 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -190,10 +190,8 @@ async fn process_ranks_in_batches( UPDATE {aggregates_table} a {set_clause} FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published; "#, - id_column = format!("{table_name}_id"), - aggregates_table = format!("{table_name}_aggregates"), - set_clause = set_clause, - where_clause = where_clause + id_column = format_args!("{table_name}_id"), + aggregates_table = format_args!("{table_name}_aggregates"), )) .bind::(previous_batch_last_published) .bind::(update_batch_size) From 9d3a0cef56ba0ab2412d05c2bda2bd2edea5a944 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Dec 2024 17:22:31 -0500 Subject: [PATCH 07/23] Adding `report_combined` table. (#5231) * Combined tables try 2 * Finishing up combined report table. * Fix ts optionals. * Adding tests, triggers, and history updates for report_combined. * Adding profile. * Add cursor pagination to report_combined view (#5244) * add pagination cursor * store timestamp instead of id in cursor (partial) * Revert "store timestamp instead of id in cursor (partial)" This reverts commit 89359dde4bc5fee39fdd2840828330f398444a36. * use paginated query builder * Fixing migration and paged API. * Using dullbananas trigger procedure * Removing pointless list routes, reorganizing tests. * Fixing column XOR check. * Forgot to remove list report actions. * Cleanup. * Use internal tagging. * Fixing api tests. * Adding a few indexes. * Fixing migration name. * Fixing unique constraints. * Addressing PR comments. * Fixing api tests. * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas * Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas * Fixing import and fmt. * Fixing null types in postgres. * Comment out err. * Addressing PR comments. * Removing serialization --------- Co-authored-by: dullbananas --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +- api_tests/src/comment.spec.ts | 37 +- api_tests/src/community.spec.ts | 5 +- api_tests/src/follow.spec.ts | 1 - api_tests/src/image.spec.ts | 1 - api_tests/src/post.spec.ts | 58 +- api_tests/src/shared.ts | 26 +- crates/api/src/comment_report/list.rs | 37 - crates/api/src/lib.rs | 4 +- crates/api/src/local_user/report_count.rs | 36 +- crates/api/src/post_report/list.rs | 37 - crates/api/src/private_message_report/list.rs | 35 - .../{ => reports}/comment_report/create.rs | 2 +- .../comment_report}/mod.rs | 1 - .../{ => reports}/comment_report/resolve.rs | 2 +- crates/api/src/reports/mod.rs | 4 + .../src/{ => reports}/post_report/create.rs | 2 +- .../post_report}/mod.rs | 1 - .../src/{ => reports}/post_report/resolve.rs | 2 +- .../private_message_report/create.rs | 2 +- .../private_message_report}/mod.rs | 1 - .../private_message_report/resolve.rs | 2 +- .../api/src/reports/report_combined/list.rs | 41 + crates/api/src/reports/report_combined/mod.rs | 1 + crates/api_common/src/comment.rs | 58 +- crates/api_common/src/lib.rs | 1 + crates/api_common/src/person.rs | 7 +- crates/api_common/src/post.rs | 61 +- crates/api_common/src/private_message.rs | 54 +- crates/api_common/src/reports/combined.rs | 32 + crates/api_common/src/reports/comment.rs | 31 + crates/api_common/src/reports/mod.rs | 4 + crates/api_common/src/reports/post.rs | 31 + .../api_common/src/reports/private_message.rs | 31 + .../db_schema/replaceable_schema/triggers.sql | 32 + crates/db_schema/src/newtypes.rs | 11 +- crates/db_schema/src/schema.rs | 14 + crates/db_schema/src/source/combined/mod.rs | 1 + .../db_schema/src/source/combined/report.rs | 23 + crates/db_schema/src/source/mod.rs | 1 + crates/db_views/src/comment_report_view.rs | 489 +-------- crates/db_views/src/lib.rs | 2 + crates/db_views/src/post_report_view.rs | 356 +------ .../src/private_message_report_view.rs | 195 +--- crates/db_views/src/report_combined_view.rs | 967 ++++++++++++++++++ crates/db_views/src/structs.rs | 52 + .../down.sql | 2 + .../up.sql | 42 + src/api_routes_v3.rs | 25 +- src/api_routes_v4.rs | 32 +- 51 files changed, 1501 insertions(+), 1403 deletions(-) delete mode 100644 crates/api/src/comment_report/list.rs delete mode 100644 crates/api/src/post_report/list.rs delete mode 100644 crates/api/src/private_message_report/list.rs rename crates/api/src/{ => reports}/comment_report/create.rs (97%) rename crates/api/src/{post_report => reports/comment_report}/mod.rs (70%) rename crates/api/src/{ => reports}/comment_report/resolve.rs (95%) create mode 100644 crates/api/src/reports/mod.rs rename crates/api/src/{ => reports}/post_report/create.rs (97%) rename crates/api/src/{private_message_report => reports/post_report}/mod.rs (70%) rename crates/api/src/{ => reports}/post_report/resolve.rs (96%) rename crates/api/src/{ => reports}/private_message_report/create.rs (96%) rename crates/api/src/{comment_report => reports/private_message_report}/mod.rs (70%) rename crates/api/src/{ => reports}/private_message_report/resolve.rs (93%) create mode 100644 crates/api/src/reports/report_combined/list.rs create mode 100644 crates/api/src/reports/report_combined/mod.rs create mode 100644 crates/api_common/src/reports/combined.rs create mode 100644 crates/api_common/src/reports/comment.rs create mode 100644 crates/api_common/src/reports/mod.rs create mode 100644 crates/api_common/src/reports/post.rs create mode 100644 crates/api_common/src/reports/private_message.rs create mode 100644 crates/db_schema/src/source/combined/mod.rs create mode 100644 crates/db_schema/src/source/combined/report.rs create mode 100644 crates/db_views/src/report_combined_view.rs create mode 100644 migrations/2024-12-02-181601_add_report_combined_table/down.sql create mode 100644 migrations/2024-12-02-181601_add_report_combined_table/up.sql diff --git a/api_tests/package.json b/api_tests/package.json index 7ea21d0ba..965388625 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -28,7 +28,7 @@ "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-api-v4.16", + "lemmy-js-client": "0.20.0-reports-combined.3", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 496606e6c..198062652 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^29.5.0 version: 29.7.0(@types/node@22.9.0) lemmy-js-client: - specifier: 0.20.0-api-v4.16 - version: 0.20.0-api-v4.16 + specifier: 0.20.0-reports-combined.3 + version: 0.20.0-reports-combined.3 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1167,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-api-v4.16: - resolution: {integrity: sha512-9Wn7b8YT2KnEA286+RV1B3mLmecAynvAERoC0ZZiccfSgkEvd3rG9A5X9ejiPqp+JzDZJeisO57+Ut4QHr5oTw==} + lemmy-js-client@0.20.0-reports-combined.3: + resolution: {integrity: sha512-0Z/9S41r6NM8f09Gkxerq9zYBE6UcywXfeWNxsYknkyh0ZnKbtNxjTkSxE6JpRbz7wokKFRSH9NpwgNloQY5uw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3077,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-api-v4.16: {} + lemmy-js-client@0.20.0-reports-combined.3: {} leven@3.1.0: {} diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 5cf94aa03..419e58769 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -22,7 +22,6 @@ import { createCommunity, registerUser, reportComment, - listCommentReports, randomString, unfollows, getComments, @@ -38,8 +37,15 @@ import { blockCommunity, delay, saveUserSettings, + listReports, } from "./shared"; -import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; +import { + CommentReportView, + CommentView, + CommunityView, + ReportCombinedView, + SaveUserSettings, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; let postOnAlphaRes: PostResponse; @@ -796,13 +802,17 @@ test("Report a comment", async () => { let alphaReport = (await reportComment(alpha, alphaComment.id, reason)) .comment_report_view.comment_report; - let betaReport = (await waitUntil( - () => - listCommentReports(beta).then(r => - r.comment_reports.find(rep => rep.comment_report.reason === reason), - ), - e => !!e, - ))!.comment_report; + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkCommentReportReason(r, reason); + }), + ), + e => !!e, + )!) as CommentReportView + ).comment_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_comment_text).toBe( @@ -877,3 +887,12 @@ test.skip("Fetch a deeply nested comment", async () => { expect(betaComment!.comment!.comment).toBeDefined(); expect(betaComment?.comment?.post).toBeDefined(); }); + +function checkCommentReportReason(rcv: ReportCombinedView, reason: string) { + switch (rcv.type_) { + case "Comment": + return rcv.comment_report.reason === reason; + default: + return false; + } +} diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 2bb092088..2d1570ea6 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -16,7 +16,6 @@ import { followCommunity, banPersonFromCommunity, resolvePerson, - getSite, createPost, getPost, resolvePost, @@ -36,7 +35,7 @@ import { userBlockInstance, } from "./shared"; import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; -import { EditCommunity, EditSite, GetPosts } from "lemmy-js-client"; +import { EditCommunity, GetPosts } from "lemmy-js-client"; beforeAll(setupLogins); afterAll(unfollows); @@ -573,7 +572,7 @@ test("Remote mods can edit communities", async () => { communityRes.community_view.community.id, ); - await expect(alphaCommunity.community_view.community.description).toBe( + expect(alphaCommunity.community_view.community.description).toBe( "Example description", ); }); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 936ce2606..c447e14cd 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -5,7 +5,6 @@ import { setupLogins, resolveBetaCommunity, followCommunity, - getSite, waitUntil, beta, betaUrl, diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index a3478081a..4d1abbdfd 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -18,7 +18,6 @@ import { epsilon, followCommunity, gamma, - getSite, imageFetchLimit, registerUser, resolveBetaCommunity, diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 4158bbdc7..52f86e8ef 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -27,10 +27,8 @@ import { followCommunity, banPersonFromCommunity, reportPost, - listPostReports, randomString, registerUser, - getSite, unfollows, resolveCommunity, waitUntil, @@ -38,11 +36,18 @@ import { alphaUrl, loginUser, createCommunity, + listReports, getMyUser, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; -import { EditSite, ResolveObject } from "lemmy-js-client"; +import { + EditSite, + PostReport, + PostReportView, + ReportCombinedView, + ResolveObject, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -688,16 +693,17 @@ test("Report a post", async () => { expect(gammaReport).toBeDefined(); // Report was federated to community instance - let betaReport = (await waitUntil( - () => - listPostReports(beta).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport); + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_post_name).toBe(gammaReport.original_post_name); @@ -707,16 +713,17 @@ test("Report a post", async () => { await unfollowRemotes(alpha); // Report was federated to poster's instance - let alphaReport = (await waitUntil( - () => - listPostReports(alpha).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let alphaReport = ( + (await waitUntil( + () => + listReports(alpha).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport); + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(alphaReport).toBeDefined(); expect(alphaReport.resolved).toBe(false); expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name); @@ -817,3 +824,12 @@ test("Rewrite markdown links", async () => { `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, ); }); + +function checkPostReportName(rcv: ReportCombinedView, report: PostReport) { + switch (rcv.type_) { + case "Post": + return rcv.post_report.original_post_name === report.original_post_name; + default: + return false; + } +} diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 1ed13d9cf..4cad739f4 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,5 +1,4 @@ import { - AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, @@ -16,6 +15,8 @@ import { LemmyHttp, ListCommunityPendingFollows, ListCommunityPendingFollowsResponse, + ListReports, + ListReportsResponse, MyUserInfo, PersonId, PostView, @@ -75,12 +76,8 @@ import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessa import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages"; import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse"; import { CreatePostReport } from "lemmy-js-client/dist/types/CreatePostReport"; -import { ListPostReportsResponse } from "lemmy-js-client/dist/types/ListPostReportsResponse"; -import { ListPostReports } from "lemmy-js-client/dist/types/ListPostReports"; import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportResponse"; import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport"; -import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse"; -import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports"; import { GetPostsResponse } from "lemmy-js-client/dist/types/GetPostsResponse"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse"; @@ -210,7 +207,9 @@ async function allowInstance(api: LemmyHttp, instance: string) { // Ignore errors from duplicate allows (because setup gets called for each test file) try { await api.adminAllowInstance(params); - } catch {} + } catch (error) { + // console.error(error); + } } export async function createPost( @@ -809,11 +808,11 @@ export async function reportPost( return api.createPostReport(form); } -export async function listPostReports( +export async function listReports( api: LemmyHttp, -): Promise { - let form: ListPostReports = {}; - return api.listPostReports(form); +): Promise { + let form: ListReports = {}; + return api.listReports(form); } export async function reportComment( @@ -840,13 +839,6 @@ export async function reportPrivateMessage( return api.createPrivateMessageReport(form); } -export async function listCommentReports( - api: LemmyHttp, -): Promise { - let form: ListCommentReports = {}; - return api.listCommentReports(form); -} - export function getPosts( api: LemmyHttp, listingType?: ListingType, diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs deleted file mode 100644 index d2f723819..000000000 --- a/crates/api/src/comment_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - comment::{ListCommentReports, ListCommentReportsResponse}, - context::LemmyContext, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists comment reports for a community if an id is supplied -/// or returns all comment reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_comment_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let comment_id = data.comment_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let comment_reports = CommentReportQuery { - community_id, - comment_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListCommentReportsResponse { comment_reports })) -} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 6a2c94332..aa6e37000 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -33,13 +33,11 @@ use std::io::Cursor; use totp_rs::{Secret, TOTP}; pub mod comment; -pub mod comment_report; pub mod community; pub mod local_user; pub mod post; -pub mod post_report; pub mod private_message; -pub mod private_message_report; +pub mod reports; pub mod site; pub mod sitemap; diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 32448dcaa..0d24a4de9 100644 --- a/crates/api/src/local_user/report_count.rs +++ b/crates/api/src/local_user/report_count.rs @@ -4,12 +4,7 @@ use lemmy_api_common::{ person::{GetReportCount, GetReportCountResponse}, utils::check_community_mod_of_any_or_admin_action, }; -use lemmy_db_views::structs::{ - CommentReportView, - LocalUserView, - PostReportView, - PrivateMessageReportView, -}; +use lemmy_db_views::structs::{LocalUserView, ReportCombinedViewInternal}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -18,29 +13,14 @@ pub async fn report_count( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let person_id = local_user_view.person.id; - let admin = local_user_view.local_user.admin; - let community_id = data.community_id; - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - let comment_reports = - CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id) - .await?; + let count = ReportCombinedViewInternal::get_report_count( + &mut context.pool(), + &local_user_view, + data.community_id, + ) + .await?; - let post_reports = - PostReportView::get_report_count(&mut context.pool(), person_id, admin, community_id).await?; - - let private_message_reports = if admin && community_id.is_none() { - Some(PrivateMessageReportView::get_report_count(&mut context.pool()).await?) - } else { - None - }; - - Ok(Json(GetReportCountResponse { - community_id, - comment_reports, - post_reports, - private_message_reports, - })) + Ok(Json(GetReportCountResponse { count })) } diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/post_report/list.rs deleted file mode 100644 index 7d1d50b0b..000000000 --- a/crates/api/src/post_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - post::{ListPostReports, ListPostReportsResponse}, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists post reports for a community if an id is supplied -/// or returns all post reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_post_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let post_id = data.post_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let post_reports = PostReportQuery { - community_id, - post_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListPostReportsResponse { post_reports })) -} diff --git a/crates/api/src/private_message_report/list.rs b/crates/api/src/private_message_report/list.rs deleted file mode 100644 index 79ef53e1c..000000000 --- a/crates/api/src/private_message_report/list.rs +++ /dev/null @@ -1,35 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, - utils::is_admin, -}; -use lemmy_db_views::{ - private_message_report_view::PrivateMessageReportQuery, - structs::LocalUserView, -}; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_pm_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - is_admin(&local_user_view)?; - - let unresolved_only = data.unresolved_only.unwrap_or_default(); - let page = data.page; - let limit = data.limit; - let private_message_reports = PrivateMessageReportQuery { - unresolved_only, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(ListPrivateMessageReportsResponse { - private_message_reports, - })) -} diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/reports/comment_report/create.rs similarity index 97% rename from crates/api/src/comment_report/create.rs rename to crates/api/src/reports/comment_report/create.rs index 48066cfe6..a456ded36 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/reports/comment_report/create.rs @@ -2,8 +2,8 @@ use crate::check_report_reason; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - comment::{CommentReportResponse, CreateCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, CreateCommentReport}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_comment_deleted_or_removed, diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/reports/comment_report/mod.rs similarity index 70% rename from crates/api/src/post_report/mod.rs rename to crates/api/src/reports/comment_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/post_report/mod.rs +++ b/crates/api/src/reports/comment_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/reports/comment_report/resolve.rs similarity index 95% rename from crates/api/src/comment_report/resolve.rs rename to crates/api/src/reports/comment_report/resolve.rs index 58d5041dc..5ab36054f 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/reports/comment_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ - comment::{CommentReportResponse, ResolveCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, ResolveCommentReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; diff --git a/crates/api/src/reports/mod.rs b/crates/api/src/reports/mod.rs new file mode 100644 index 000000000..f23d1d71f --- /dev/null +++ b/crates/api/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod comment_report; +pub mod post_report; +pub mod private_message_report; +pub mod report_combined; diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/reports/post_report/create.rs similarity index 97% rename from crates/api/src/post_report/create.rs rename to crates/api/src/reports/post_report/create.rs index b9edf35c5..bc85bdbe7 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/reports/post_report/create.rs @@ -3,7 +3,7 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - post::{CreatePostReport, PostReportResponse}, + reports::post::{CreatePostReport, PostReportResponse}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_community_user_action, diff --git a/crates/api/src/private_message_report/mod.rs b/crates/api/src/reports/post_report/mod.rs similarity index 70% rename from crates/api/src/private_message_report/mod.rs rename to crates/api/src/reports/post_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/private_message_report/mod.rs +++ b/crates/api/src/reports/post_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/reports/post_report/resolve.rs similarity index 96% rename from crates/api/src/post_report/resolve.rs rename to crates/api/src/reports/post_report/resolve.rs index 652327513..26b182a45 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/reports/post_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - post::{PostReportResponse, ResolvePostReport}, + reports::post::{PostReportResponse, ResolvePostReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/reports/private_message_report/create.rs similarity index 96% rename from crates/api/src/private_message_report/create.rs rename to crates/api/src/reports/private_message_report/create.rs index de8ca390f..17b5dceeb 100644 --- a/crates/api/src/private_message_report/create.rs +++ b/crates/api/src/reports/private_message_report/create.rs @@ -2,7 +2,7 @@ use crate::check_report_reason; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, + reports::private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, utils::send_new_report_email_to_admins, }; use lemmy_db_schema::{ diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/reports/private_message_report/mod.rs similarity index 70% rename from crates/api/src/comment_report/mod.rs rename to crates/api/src/reports/private_message_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/comment_report/mod.rs +++ b/crates/api/src/reports/private_message_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/reports/private_message_report/resolve.rs similarity index 93% rename from crates/api/src/private_message_report/resolve.rs rename to crates/api/src/reports/private_message_report/resolve.rs index 7d821a60c..3f812e4fe 100644 --- a/crates/api/src/private_message_report/resolve.rs +++ b/crates/api/src/reports/private_message_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, + reports::private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, utils::is_admin, }; use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable}; diff --git a/crates/api/src/reports/report_combined/list.rs b/crates/api/src/reports/report_combined/list.rs new file mode 100644 index 000000000..12548d189 --- /dev/null +++ b/crates/api/src/reports/report_combined/list.rs @@ -0,0 +1,41 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + reports::combined::{ListReports, ListReportsResponse}, + utils::check_community_mod_of_any_or_admin_action, +}; +use lemmy_db_views::{report_combined_view::ReportCombinedQuery, structs::LocalUserView}; +use lemmy_utils::error::LemmyResult; + +/// Lists reports for a community if an id is supplied +/// or returns all reports for communities a user moderates +#[tracing::instrument(skip(context))] +pub async fn list_reports( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let community_id = data.community_id; + let unresolved_only = data.unresolved_only; + + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let reports = ReportCombinedQuery { + community_id, + unresolved_only, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListReportsResponse { reports })) +} diff --git a/crates/api/src/reports/report_combined/mod.rs b/crates/api/src/reports/report_combined/mod.rs new file mode 100644 index 000000000..d17e233fb --- /dev/null +++ b/crates/api/src/reports/report_combined/mod.rs @@ -0,0 +1 @@ +pub mod list; diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index e08365789..bae9c4de4 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -1,9 +1,9 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommentReportId, CommunityId, LanguageId, LocalUserId, PostId}, + newtypes::{CommentId, CommunityId, LanguageId, LocalUserId, PostId}, CommentSortType, ListingType, }; -use lemmy_db_views::structs::{CommentReportView, CommentView, VoteView}; +use lemmy_db_views::structs::{CommentView, VoteView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -146,60 +146,6 @@ pub struct GetCommentsResponse { pub comments: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Report a comment. -pub struct CreateCommentReport { - pub comment_id: CommentId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report response. -pub struct CommentReportResponse { - pub comment_report_view: CommentReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a comment report (only doable by mods). -pub struct ResolveCommentReport { - pub report_id: CommentReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List comment reports. -pub struct ListCommentReports { - #[cfg_attr(feature = "full", ts(optional))] - pub comment_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report list response. -pub struct ListCommentReportsResponse { - pub comment_reports: Vec, -} - #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 6e09d904d..8af1dec25 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -11,6 +11,7 @@ pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; +pub mod reports; #[cfg(feature = "full")] pub mod request; #[cfg(feature = "full")] diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index b95cf5e77..797946d65 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -448,12 +448,7 @@ pub struct GetReportCount { #[cfg_attr(feature = "full", ts(export))] /// A response for the number of reports. pub struct GetReportCountResponse { - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - pub comment_reports: i64, - pub post_reports: i64, - #[cfg_attr(feature = "full", ts(optional))] - pub private_message_reports: Option, + pub count: i64, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index fb16c8aa8..db987d63c 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,10 +1,10 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId, TagId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, TagId}, ListingType, PostFeatureType, PostSortType, }; -use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; +use lemmy_db_views::structs::{PaginationCursor, PostView, VoteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -118,6 +118,8 @@ pub struct GetPosts { pub no_comments_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[skip_serializing_none] @@ -251,61 +253,6 @@ pub struct SavePost { pub save: bool, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a post report. -pub struct CreatePostReport { - pub post_id: PostId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post report response. -pub struct PostReportResponse { - pub post_report_view: PostReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a post report (mods only). -pub struct ResolvePostReport { - pub report_id: PostReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List post reports. -pub struct ListPostReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - // TODO make into tagged enum at some point - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub post_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post reports response. -pub struct ListPostReportsResponse { - pub post_reports: Vec, -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index 666fe3865..8bd417a8e 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -1,5 +1,5 @@ -use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}; -use lemmy_db_views::structs::{PrivateMessageReportView, PrivateMessageView}; +use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}; +use lemmy_db_views::structs::PrivateMessageView; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -72,53 +72,3 @@ pub struct PrivateMessagesResponse { pub struct PrivateMessageResponse { pub private_message_view: PrivateMessageView, } - -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a report for a private message. -pub struct CreatePrivateMessageReport { - pub private_message_id: PrivateMessageId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// A private message report response. -pub struct PrivateMessageReportResponse { - pub private_message_report_view: PrivateMessageReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a private message report. -pub struct ResolvePrivateMessageReport { - pub report_id: PrivateMessageReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List private message reports. -// TODO , perhaps GetReports should be a tagged enum list too. -pub struct ListPrivateMessageReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The response for list private message reports. -pub struct ListPrivateMessageReportsResponse { - pub private_message_reports: Vec, -} diff --git a/crates/api_common/src/reports/combined.rs b/crates/api_common/src/reports/combined.rs new file mode 100644 index 000000000..69d928830 --- /dev/null +++ b/crates/api_common/src/reports/combined.rs @@ -0,0 +1,32 @@ +use lemmy_db_schema::newtypes::CommunityId; +use lemmy_db_views::structs::{ReportCombinedPaginationCursor, ReportCombinedView}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List reports. +pub struct ListReports { + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, + /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] + pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post reports response. +pub struct ListReportsResponse { + pub reports: Vec, +} diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs new file mode 100644 index 000000000..d1a51a6a8 --- /dev/null +++ b/crates/api_common/src/reports/comment.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{CommentId, CommentReportId}; +use lemmy_db_views::structs::CommentReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Report a comment. +pub struct CreateCommentReport { + pub comment_id: CommentId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The comment report response. +pub struct CommentReportResponse { + pub comment_report_view: CommentReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a comment report (only doable by mods). +pub struct ResolveCommentReport { + pub report_id: CommentReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/reports/mod.rs b/crates/api_common/src/reports/mod.rs new file mode 100644 index 000000000..6584de1bc --- /dev/null +++ b/crates/api_common/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod combined; +pub mod comment; +pub mod post; +pub mod private_message; diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs new file mode 100644 index 000000000..a4d20d575 --- /dev/null +++ b/crates/api_common/src/reports/post.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PostId, PostReportId}; +use lemmy_db_views::structs::PostReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a post report. +pub struct CreatePostReport { + pub post_id: PostId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post report response. +pub struct PostReportResponse { + pub post_report_view: PostReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a post report (mods only). +pub struct ResolvePostReport { + pub report_id: PostReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs new file mode 100644 index 000000000..5fd401564 --- /dev/null +++ b/crates/api_common/src/reports/private_message.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PrivateMessageId, PrivateMessageReportId}; +use lemmy_db_views::structs::PrivateMessageReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a report for a private message. +pub struct CreatePrivateMessageReport { + pub private_message_id: PrivateMessageId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A private message report response. +pub struct PrivateMessageReportResponse { + pub private_message_report_view: PrivateMessageReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a private message report. +pub struct ResolvePrivateMessageReport { + pub report_id: PrivateMessageReportId, + pub resolved: bool, +} diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index e5b3e22d0..2d9b0df6e 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -653,3 +653,35 @@ CREATE TRIGGER change_values FOR EACH ROW EXECUTE FUNCTION r.private_message_change_values (); +-- Combined tables triggers +-- These insert (published, item_id) into X_combined tables +-- Reports (comment_report, post_report, private_message_report) +CREATE PROCEDURE r.create_report_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.report_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO report_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER report_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_report_combined_trigger ('post_report'); + +CALL r.create_report_combined_trigger ('comment_report'); + +CALL r.create_report_combined_trigger ('private_message_report'); + diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 963f847a5..4c4a9b66c 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -89,19 +89,19 @@ pub struct PersonMentionId(i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The comment report id. -pub struct CommentReportId(i32); +pub struct CommentReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The post report id. -pub struct PostReportId(i32); +pub struct PostReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The private message report id. -pub struct PrivateMessageReportId(i32); +pub struct PrivateMessageReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] @@ -179,6 +179,11 @@ pub struct LtreeDef(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// The report combined id +pub struct ReportCombinedId(i32); + impl DbUrl { pub fn inner(&self) -> &Url { &self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 77122f7cb..64aff118b 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -889,6 +889,16 @@ diesel::table! { } } +diesel::table! { + report_combined (id) { + id -> Int4, + published -> Timestamptz, + post_report_id -> Nullable, + comment_report_id -> Nullable, + private_message_report_id -> Nullable, + } +} + diesel::table! { secret (id) { id -> Int4, @@ -1057,6 +1067,9 @@ diesel::joinable!(post_tag -> tag (tag_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); +diesel::joinable!(report_combined -> comment_report (comment_report_id)); +diesel::joinable!(report_combined -> post_report (post_report_id)); +diesel::joinable!(report_combined -> private_message_report (private_message_report_id)); diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); @@ -1127,6 +1140,7 @@ diesel::allow_tables_to_appear_in_same_query!( received_activity, registration_application, remote_image, + report_combined, secret, sent_activity, site, diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs new file mode 100644 index 000000000..7352eef8e --- /dev/null +++ b/crates/db_schema/src/source/combined/mod.rs @@ -0,0 +1 @@ +pub mod report; diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs new file mode 100644 index 000000000..2902c5548 --- /dev/null +++ b/crates/db_schema/src/source/combined/report.rs @@ -0,0 +1,23 @@ +use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, ReportCombinedId}; +#[cfg(feature = "full")] +use crate::schema::report_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = report_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = report_combined_keys))] +/// A combined reports table. +pub struct ReportCombined { + pub id: ReportCombinedId, + pub published: DateTime, + pub post_report_id: Option, + pub comment_report_id: Option, + pub private_message_report_id: Option, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 6230d004d..2ac2692b4 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -5,6 +5,7 @@ use url::Url; pub mod activity; pub mod actor_language; pub mod captcha_answer; +pub mod combined; pub mod comment; pub mod comment_reply; pub mod comment_report; diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index b4a23a0da..6154b9b56 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -1,7 +1,6 @@ -use crate::structs::{CommentReportView, LocalUserView}; +use crate::structs::CommentReportView; use diesel::{ dsl::now, - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -12,7 +11,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, + newtypes::{CommentReportId, PersonId}, schema::{ comment, comment_actions, @@ -26,26 +25,21 @@ use lemmy_db_schema::{ post, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentReportView, (CommentReportId, PersonId)>, - impl ListFn<'a, CommentReportView, (CommentReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: comment_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl CommentReportView { + /// returns the CommentReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: CommentReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + comment_report::table + .find(report_id) .inner_join(comment::table) .inner_join(post::table.on(comment::post_id.eq(post::id))) .inner_join(community::table.on(post::community_id.eq(community::id))) @@ -117,456 +111,7 @@ fn queries<'a>() -> Queries< comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move { - all_joins( - comment_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, - (options, user): (CommentReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(comment_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(comment_id) = options.comment_id { - query = query.filter(comment_report::comment_id.eq(comment_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(comment_report::resolved.eq(false)) - .order_by(comment_report::published.asc()); - } else { - query = query.order_by(comment_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl CommentReportView { - /// returns the CommentReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: CommentReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// Returns the current unresolved comment report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - - let conn = &mut get_conn(pool).await?; - - let mut query = comment_report::table - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .filter(comment_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(comment_report::id)) - .first::(conn) - .await - } else { - query - .select(count(comment_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct CommentReportQuery { - pub community_id: Option, - pub comment_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl CommentReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - comment_report_view::{CommentReportQuery, CommentReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::CommentAggregates, - source::{ - comment::{Comment, CommentInsertForm}, - comment_report::{CommentReport, CommentReportForm}, - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::{build_db_pool_for_tests, RANK_DEFAULT}, - CommunityVisibility, - SubscribedType, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community crv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_timmy.id, - inserted_post.id, - "A test comment 32".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - // sara reports - let sara_report_form = CommentReportForm { - creator_id: inserted_sara.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from sara".into(), - }; - - let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?; - - // jessica reports - let jessica_report_form = CommentReportForm { - creator_id: inserted_jessica.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from jessica".into(), - }; - - let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - - let agg = CommentAggregates::read(pool, inserted_comment.id).await?; - - let read_jessica_report_view = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let expected_jessica_report_view = CommentReportView { - comment_report: inserted_jessica_report.clone(), - comment: inserted_comment.clone(), - post: inserted_post, - creator_is_moderator: true, - creator_is_admin: false, - creator_blocked: false, - subscribed: SubscribedType::NotSubscribed, - saved: false, - community: Community { - id: inserted_community.id, - name: inserted_community.name, - icon: None, - removed: false, - deleted: false, - nsfw: false, - actor_id: inserted_community.actor_id.clone(), - local: true, - title: inserted_community.title, - sidebar: None, - description: None, - updated: None, - banner: None, - hidden: false, - posting_restricted_to_mods: false, - published: inserted_community.published, - private_key: inserted_community.private_key, - public_key: inserted_community.public_key, - last_refreshed_at: inserted_community.last_refreshed_at, - followers_url: inserted_community.followers_url, - inbox_url: inserted_community.inbox_url, - moderators_url: inserted_community.moderators_url, - featured_url: inserted_community.featured_url, - instance_id: inserted_instance.id, - visibility: CommunityVisibility::Public, - }, - creator: Person { - id: inserted_jessica.id, - name: inserted_jessica.name, - display_name: None, - published: inserted_jessica.published, - avatar: None, - actor_id: inserted_jessica.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_jessica.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_jessica.private_key, - public_key: inserted_jessica.public_key, - last_refreshed_at: inserted_jessica.last_refreshed_at, - }, - comment_creator: Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - }, - creator_banned_from_community: false, - counts: CommentAggregates { - comment_id: inserted_comment.id, - score: 0, - upvotes: 0, - downvotes: 0, - published: agg.published, - child_count: 0, - hot_rank: RANK_DEFAULT, - controversy_rank: 0.0, - report_count: 2, - unresolved_report_count: 2, - }, - my_vote: None, - resolver: None, - }; - - assert_eq!(read_jessica_report_view, expected_jessica_report_view); - - let mut expected_sara_report_view = expected_jessica_report_view.clone(); - expected_sara_report_view.comment_report = inserted_sara_report; - expected_sara_report_view.creator = Person { - id: inserted_sara.id, - name: inserted_sara.name, - display_name: None, - published: inserted_sara.published, - avatar: None, - actor_id: inserted_sara.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_sara.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_sara.private_key, - public_key: inserted_sara.public_key, - last_refreshed_at: inserted_sara.last_refreshed_at, - }; - - // Do a batch read of timmys reports - let reports = CommentReportQuery::default() - .list(pool, &timmy_view) - .await?; - - assert_eq!( - reports, - [ - expected_jessica_report_view.clone(), - expected_sara_report_view.clone(), - ] - ); - - // Make sure the counts are correct - let report_count = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Try to resolve the report - CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let read_jessica_report_view_after_resolve = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; - expected_jessica_report_view_after_resolve - .comment_report - .resolved = true; - expected_jessica_report_view_after_resolve - .comment_report - .resolver_id = Some(inserted_timmy.id); - expected_jessica_report_view_after_resolve - .comment_report - .updated = read_jessica_report_view_after_resolve - .comment_report - .updated; - expected_jessica_report_view_after_resolve - .counts - .unresolved_report_count = 1; - expected_sara_report_view.counts.unresolved_report_count = 1; - expected_jessica_report_view_after_resolve.resolver = Some(Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - }); - - assert_eq!( - read_jessica_report_view_after_resolve, - expected_jessica_report_view_after_resolve - ); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = CommentReportQuery { - unresolved_only: (true), - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_eq!(reports_after_resolve[0], expected_sara_report_view); - assert_eq!(reports_after_resolve.len(), 1); - - // Make sure the counts are correct - let report_count_after_resolved = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 3c1fcd84a..6c9b21023 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -24,6 +24,8 @@ pub mod private_message_view; #[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] +pub mod report_combined_view; +#[cfg(feature = "full")] pub mod site_view; pub mod structs; #[cfg(feature = "full")] diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 9429c258f..4c7fd676c 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -1,6 +1,5 @@ -use crate::structs::{LocalUserView, PostReportView}; +use crate::structs::PostReportView; use diesel::{ - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -11,7 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommunityId, PersonId, PostId, PostReportId}, + newtypes::{PersonId, PostReportId}, schema::{ community, community_actions, @@ -24,26 +23,22 @@ use lemmy_db_schema::{ post_report, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PostReportView, (PostReportId, PersonId)>, - impl ListFn<'a, PostReportView, (PostReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: post_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl PostReportView { + /// returns the PostReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: PostReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + post_report::table + .find(report_id) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) @@ -104,322 +99,7 @@ fn queries<'a>() -> Queries< post_aggregates::all_columns, aliases::person2.fields(person::all_columns.nullable()), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (PostReportId, PersonId)| async move { - all_joins( - post_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, (options, user): (PostReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(post_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(post_id) = options.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(post_report::resolved.eq(false)) - .order_by(post_report::published.asc()); - } else { - query = query.order_by(post_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PostReportView { - /// returns the PostReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: PostReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// returns the current unresolved post report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - let mut query = post_report::table - .inner_join(post::table) - .filter(post_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(post_report::id)) - .first::(conn) - .await - } else { - query - .select(count(post_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct PostReportQuery { - pub community_id: Option, - pub post_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PostReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - post_report_view::{PostReportQuery, PostReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::PostAggregates, - assert_length, - source::{ - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - post_report::{PostReport, PostReportForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community prv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - // sara reports - let sara_report_form = PostReportForm { - creator_id: inserted_sara.id, - post_id: inserted_post.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from sara".into(), - }; - - PostReport::report(pool, &sara_report_form).await?; - - let new_post_2 = PostInsertForm::new( - "A test post crv 2".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post_2 = Post::create(pool, &new_post_2).await?; - - // jessica reports - let jessica_report_form = PostReportForm { - creator_id: inserted_jessica.id, - post_id: inserted_post_2.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from jessica".into(), - }; - - let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; - - let read_jessica_report_view = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - // Make sure the triggers are reading the aggregates correctly. - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - - assert_eq!( - read_jessica_report_view.post_report, - inserted_jessica_report - ); - assert_eq!(read_jessica_report_view.post, inserted_post_2); - assert_eq!(read_jessica_report_view.community.id, inserted_community.id); - assert_eq!(read_jessica_report_view.creator.id, inserted_jessica.id); - assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id); - assert_eq!(read_jessica_report_view.my_vote, None); - assert_eq!(read_jessica_report_view.resolver, None); - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 1); - - // Do a batch read of timmys reports - let reports = PostReportQuery::default().list(pool, &timmy_view).await?; - - assert_eq!(reports[1].creator.id, inserted_sara.id); - assert_eq!(reports[0].creator.id, inserted_jessica.id); - - // Make sure the counts are correct - let report_count = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Pretend the post was removed, and resolve all reports for that object. - // This is called manually in the API for post removals - PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) - .await?; - - let read_jessica_report_view_after_resolve = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - assert!(read_jessica_report_view_after_resolve.post_report.resolved); - assert_eq!( - read_jessica_report_view_after_resolve - .post_report - .resolver_id, - Some(inserted_timmy.id) - ); - assert_eq!( - read_jessica_report_view_after_resolve - .resolver - .map(|r| r.id), - Some(inserted_timmy.id) - ); - - // Make sure the unresolved_post report got decremented in the trigger - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 0); - - // Make sure the other unresolved report isn't changed - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = PostReportQuery { - unresolved_only: true, - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_length!(1, reports_after_resolve); - assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id); - - // Make sure the counts are correct - let report_count_after_resolved = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index e59d99608..956ccf0e1 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -1,76 +1,13 @@ use crate::structs::PrivateMessageReportView; -use diesel::{ - pg::Pg, - result::Error, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::PrivateMessageReportId, schema::{person, private_message, private_message_report}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PrivateMessageReportView, PrivateMessageReportId>, - impl ListFn<'a, PrivateMessageReportView, PrivateMessageReportQuery>, -> { - let all_joins = - |query: private_message_report::BoxedQuery<'a, Pg>| { - query - .inner_join(private_message::table) - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1 - .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), - ) - .left_join(aliases::person2.on( - private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), - )) - .select(( - private_message_report::all_columns, - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - aliases::person2.fields(person::all_columns).nullable(), - )) - }; - - let read = move |mut conn: DbConn<'a>, report_id: PrivateMessageReportId| async move { - all_joins(private_message_report::table.find(report_id).into_boxed()) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, options: PrivateMessageReportQuery| async move { - let mut query = all_joins(private_message_report::table.into_boxed()); - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(private_message_report::resolved.eq(false)) - .order_by(private_message_report::published.asc()); - } else { - query = query.order_by(private_message_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - impl PrivateMessageReportView { /// returns the PrivateMessageReportView for the provided report_id /// @@ -79,118 +16,28 @@ impl PrivateMessageReportView { pool: &mut DbPool<'_>, report_id: PrivateMessageReportId, ) -> Result { - queries().read(pool, report_id).await - } - - /// Returns the current unresolved post report count for the communities you mod - pub async fn get_report_count(pool: &mut DbPool<'_>) -> Result { - use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - private_message_report::table + .find(report_id) .inner_join(private_message::table) - .filter(private_message_report::resolved.eq(false)) - .into_boxed() - .select(count(private_message_report::id)) - .first::(conn) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + aliases::person1 + .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), + ) + .left_join( + aliases::person2.on( + private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), + ), + ) + .select(( + private_message_report::all_columns, + private_message::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + aliases::person2.fields(person::all_columns).nullable(), + )) + .first(conn) .await } } - -#[derive(Default)] -pub struct PrivateMessageReportQuery { - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PrivateMessageReportQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::private_message_report_view::PrivateMessageReportQuery; - use lemmy_db_schema::{ - assert_length, - source::{ - instance::Instance, - person::{Person, PersonInsertForm}, - private_message::{PrivateMessage, PrivateMessageInsertForm}, - private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, - }, - traits::{Crud, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); - let inserted_timmy = Person::create(pool, &new_person_1).await?; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); - let inserted_jessica = Person::create(pool, &new_person_2).await?; - - // timmy sends private message to jessica - let pm_form = PrivateMessageInsertForm::new( - inserted_timmy.id, - inserted_jessica.id, - "something offensive".to_string(), - ); - let pm = PrivateMessage::create(pool, &pm_form).await?; - - // jessica reports private message - let pm_report_form = PrivateMessageReportForm { - creator_id: inserted_jessica.id, - original_pm_text: pm.content.clone(), - private_message_id: pm.id, - reason: "its offensive".to_string(), - }; - let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - - let reports = PrivateMessageReportQuery::default().list(pool).await?; - assert_length!(1, reports); - assert!(!reports[0].private_message_report.resolved); - assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name); - assert_eq!(inserted_jessica.name, reports[0].creator.name); - assert_eq!(pm_report.reason, reports[0].private_message_report.reason); - assert_eq!(pm.content, reports[0].private_message.content); - - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); - let inserted_admin = Person::create(pool, &new_person_3).await?; - - // admin resolves the report (after taking appropriate action) - PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?; - - let reports = PrivateMessageReportQuery { - unresolved_only: (false), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(1, reports); - assert!(reports[0].private_message_report.resolved); - assert!(reports[0].resolver.is_some()); - assert_eq!( - Some(&inserted_admin.name), - reports[0].resolver.as_ref().map(|r| &r.name) - ); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs new file mode 100644 index 000000000..879634cf0 --- /dev/null +++ b/crates/db_views/src/report_combined_view.rs @@ -0,0 +1,967 @@ +use crate::structs::{ + CommentReportView, + LocalUserView, + PostReportView, + PrivateMessageReportView, + ReportCombinedPaginationCursor, + ReportCombinedView, + ReportCombinedViewInternal, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + PgExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::CommunityId, + schema::{ + comment, + comment_actions, + comment_aggregates, + comment_report, + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_aggregates, + post_report, + private_message, + private_message_report, + report_combined, + }, + source::{ + combined::report::{report_combined_keys as key, ReportCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, +}; +use lemmy_utils::error::LemmyResult; + +impl ReportCombinedViewInternal { + /// returns the current unresolved report count for the communities you mod + pub async fn get_report_count( + pool: &mut DbPool<'_>, + user: &LocalUserView, + community_id: Option, + ) -> Result { + use diesel::dsl::count; + + let conn = &mut get_conn(pool).await?; + let my_person_id = user.local_user.person_id; + + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + // Need to join to comment and post to get the community + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ), + ) + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .filter( + post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) + .into_boxed(); + + if let Some(community_id) = community_id { + query = query.filter(post::community_id.eq(community_id)) + } + + // If its not an admin, get only the ones you mod + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + query + .select(count(report_combined::id)) + .first::(conn) + .await + } +} + +impl ReportCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &ReportCombinedView) -> ReportCombinedPaginationCursor { + let (prefix, id) = match view { + ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0), + ReportCombinedView::Post(v) => ('P', v.post_report.id.0), + ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0), + }; + // hex encoding to prevent ossification + ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = report_combined::table + .select(ReportCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(report_combined::comment_report_id.eq(id)), + "P" => query.filter(report_combined::post_report_id.eq(id)), + "M" => query.filter(report_combined::private_message_report_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(ReportCombined); + +#[derive(Default)] +pub struct ReportCombinedQuery { + pub community_id: Option, + pub unresolved_only: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl ReportCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let my_person_id = user.local_user.person_id; + let item_creator = aliases::person1.field(person::id); + + let resolver = aliases::person2.field(person::id).nullable(); + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_report_id and comment_report_id are optional columns, + // many joins must use an OR condition. + // For example, the report creator must be the person table joined to either: + // - post_report.creator_id + // - comment_report.creator_id + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + // The report creator + .inner_join( + person::table.on( + post_report::creator_id + .eq(person::id) + .or(comment_report::creator_id.eq(person::id)) + .or(private_message_report::creator_id.eq(person::id)), + ), + ) + // The comment + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The private message + .left_join( + private_message::table + .on(private_message_report::private_message_id.eq(private_message::id)), + ) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator (`item_creator` is the id of this person) + .inner_join( + aliases::person1.on( + post::creator_id + .eq(item_creator) + .or(comment::creator_id.eq(item_creator)) + .or(private_message::creator_id.eq(item_creator)), + ), + ) + // The community + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .left_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), + ) + // The resolver + .left_join( + aliases::person2.on( + private_message_report::resolver_id + .eq(resolver) + .or(post_report::resolver_id.eq(resolver)) + .or(comment_report::resolver_id.eq(resolver)), + ), + ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) + .select(( + // Post-specific + post_report::all_columns.nullable(), + post::all_columns.nullable(), + post_aggregates::all_columns.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ) + .nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + // Comment-specific + comment_report::all_columns.nullable(), + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Private-message-specific + private_message_report::all_columns.nullable(), + private_message::all_columns.nullable(), + // Shared + person::all_columns, + aliases::person1.fields(person::all_columns), + community::all_columns.nullable(), + CommunityFollower::select_subscribed_type(), + aliases::person2.fields(person::all_columns.nullable()), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + )) + .into_boxed(); + + if let Some(community_id) = self.community_id { + query = query.filter(community::id.eq(community_id)); + } + + // If its not an admin, get only the ones you mod + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + if self.unresolved_only.unwrap_or_default() { + query = query + .filter( + post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) + // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, + // and remove the separate index; unless additional columns are added to this sort + .then_desc(ReverseTimestampKey(key::published)); + } else { + query = query.then_desc(key::published); + } + + // Tie breaker + query = query.then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(map_to_enum).collect(); + + Ok(out) + } +} + +/// Maps the combined DB row to an enum +fn map_to_enum(view: ReportCombinedViewInternal) -> Option { + // Use for a short alias + let v = view; + + if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + v.post_report, + v.post.clone(), + v.community.clone(), + v.post_unread_comments, + v.post_counts, + ) { + Some(ReportCombinedView::Post(PostReportView { + post_report, + post, + community, + unread_comments, + counts, + creator: v.report_creator, + post_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + resolver: v.resolver, + })) + } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_report, + v.comment, + v.comment_counts, + v.post.clone(), + v.community.clone(), + ) { + Some(ReportCombinedView::Comment(CommentReportView { + comment_report, + comment, + counts, + post, + community, + creator: v.report_creator, + comment_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + resolver: v.resolver, + })) + } else if let (Some(private_message_report), Some(private_message)) = + (v.private_message_report, v.private_message) + { + Some(ReportCombinedView::PrivateMessage( + PrivateMessageReportView { + private_message_report, + private_message, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) + } else { + None + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + report_combined_view::ReportCombinedQuery, + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + ReportCombinedView, + ReportCombinedViewInternal, + }, + }; + use lemmy_db_schema::{ + aggregates::structs::{CommentAggregates, PostAggregates}, + assert_length, + source::{ + comment::{Comment, CommentInsertForm}, + comment_report::{CommentReport, CommentReportForm}, + community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + post_report::{PostReport, PostReportForm}, + private_message::{PrivateMessage, PrivateMessageInsertForm}, + private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + }, + traits::{Crud, Joinable, Reportable}, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + jessica: Person, + timmy_view: LocalUserView, + admin_view: LocalUserView, + community: Community, + post: Post, + post_2: Post, + comment: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rcv"); + let inserted_timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_timmy.clone(), + counts: Default::default(), + }; + + // Make an admin, to be able to see private message reports. + let admin_form = PersonInsertForm::test_form(inserted_instance.id, "admin_rcv"); + let inserted_admin = Person::create(pool, &admin_form).await?; + let admin_local_user_form = LocalUserInsertForm::test_form_admin(inserted_admin.id); + let admin_local_user = LocalUser::create(pool, &admin_local_user_form, vec![]).await?; + let admin_view = LocalUserView { + local_user: admin_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_admin.clone(), + counts: Default::default(), + }; + + let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rcv"); + let inserted_sara = Person::create(pool, &sara_form).await?; + + let jessica_form = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); + let inserted_jessica = Person::create(pool, &jessica_form).await?; + + let community_form = CommunityInsertForm::new( + inserted_instance.id, + "test community crv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &community_form).await?; + + // Make timmy a mod + let timmy_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_timmy.id, + }; + CommunityModerator::join(pool, &timmy_moderator_form).await?; + + let post_form = PostInsertForm::new( + "A test post crv".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &post_form).await?; + + let new_post_2 = PostInsertForm::new( + "A test post crv 2".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &new_post_2).await?; + + // Timmy creates a comment + let comment_form = CommentInsertForm::new( + inserted_timmy.id, + inserted_post.id, + "A test comment rv".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + Ok(Data { + instance: inserted_instance, + timmy: inserted_timmy, + sara: inserted_sara, + jessica: inserted_jessica, + admin_view, + timmy_view, + community: inserted_community, + post: inserted_post, + post_2: inserted_post_2, + comment: inserted_comment, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports the post + let sara_report_post_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; + + // Sara reports the comment + let sara_report_comment_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "A test comment rv".into(), + reason: "from sara".into(), + }; + CommentReport::report(pool, &sara_report_comment_form).await?; + + // Timmy creates a private message report + let pm_form = PrivateMessageInsertForm::new( + data.timmy.id, + data.sara.id, + "something offensive crv".to_string(), + ); + let inserted_pm = PrivateMessage::create(pool, &pm_form).await?; + + // sara reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: data.sara.id, + original_pm_text: inserted_pm.content.clone(), + private_message_id: inserted_pm.id, + reason: "its offensive".to_string(), + }; + PrivateMessageReport::report(pool, &pm_report_form).await?; + + // Do a batch read of admins reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_eq!(3, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[2] { + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert_eq!(inserted_pm.id, v.private_message.id); + } else { + panic!("wrong type"); + } + + let report_count_admin = + ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; + assert_eq!(3, report_count_admin); + + // Timmy should only see 2 reports, since they're not an admin, + // but they do mod the community + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(2, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + + let report_count_timmy = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count_timmy); + + // Resolve the post report + PostReport::resolve(pool, inserted_post_report.id, data.timmy.id).await?; + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + assert_length!(1, reports_after_resolve); + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_private_message_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // timmy sends private message to jessica + let pm_form = PrivateMessageInsertForm::new( + data.timmy.id, + data.jessica.id, + "something offensive".to_string(), + ); + let pm = PrivateMessage::create(pool, &pm_form).await?; + + // jessica reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: data.jessica.id, + original_pm_text: pm.content.clone(), + private_message_id: pm.id, + reason: "its offensive".to_string(), + }; + let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(!v.private_message_report.resolved); + assert_eq!(data.timmy.name, v.private_message_creator.name); + assert_eq!(data.jessica.name, v.creator.name); + assert_eq!(pm_report.reason, v.private_message_report.reason); + assert_eq!(pm.content, v.private_message.content); + } else { + panic!("wrong type"); + } + + // admin resolves the report (after taking appropriate action) + PrivateMessageReport::resolve(pool, pm_report.id, data.admin_view.person.id).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(v.private_message_report.resolved); + assert!(v.resolver.is_some()); + assert_eq!( + Some(&data.admin_view.person.name), + v.resolver.as_ref().map(|r| &r.name) + ); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_post_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + + PostReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = PostReportForm { + creator_id: data.jessica.id, + post_id: data.post_2.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from jessica".into(), + }; + + let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; + + let read_jessica_report_view = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + // Make sure the triggers are reading the aggregates correctly. + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + + assert_eq!( + read_jessica_report_view.post_report, + inserted_jessica_report + ); + assert_eq!(read_jessica_report_view.post, data.post_2); + assert_eq!(read_jessica_report_view.community.id, data.community.id); + assert_eq!(read_jessica_report_view.creator.id, data.jessica.id); + assert_eq!(read_jessica_report_view.post_creator.id, data.timmy.id); + assert_eq!(read_jessica_report_view.my_vote, None); + assert_eq!(read_jessica_report_view.resolver, None); + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 1); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Post(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Pretend the post was removed, and resolve all reports for that object. + // This is called manually in the API for post removals + PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, data.timmy.id) + .await?; + + let read_jessica_report_view_after_resolve = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert!(read_jessica_report_view_after_resolve.post_report.resolved); + assert_eq!( + read_jessica_report_view_after_resolve + .post_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Make sure the unresolved_post report got decremented in the trigger + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 0); + + // Make sure the other unresolved report isn't changed + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_comment_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from sara".into(), + }; + + CommentReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = CommentReportForm { + creator_id: data.jessica.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from jessica".into(), + }; + + let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; + + let agg = CommentAggregates::read(pool, data.comment.id).await?; + assert_eq!(agg.report_count, 2); + + let read_jessica_report_view = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert_eq!(read_jessica_report_view.counts.unresolved_report_count, 2); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Resolve the report + CommentReport::resolve(pool, inserted_jessica_report.id, data.timmy.id).await?; + let read_jessica_report_view_after_resolve = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + assert!( + read_jessica_report_view_after_resolve + .comment_report + .resolved + ); + assert_eq!( + read_jessica_report_view_after_resolve + .comment_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a95376a1a..7fe529eb6 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -129,6 +129,12 @@ pub struct PostReportView { #[cfg_attr(feature = "full", ts(export))] pub struct PaginationCursor(pub String); +/// like PaginationCursor but for the report_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ReportCombinedPaginationCursor(pub String); + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] @@ -242,6 +248,52 @@ pub struct LocalImageView { pub person: Person, } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined report view +pub struct ReportCombinedViewInternal { + // Post-specific + pub post_report: Option, + pub post: Option, + pub post_counts: Option, + pub post_unread_comments: Option, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + // Comment-specific + pub comment_report: Option, + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + // Private-message-specific + pub private_message_report: Option, + pub private_message: Option, + // Shared + pub report_creator: Person, + pub item_creator: Person, + pub community: Option, + pub subscribed: SubscribedType, + pub resolver: Option, + pub item_creator_is_admin: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_is_moderator: bool, + pub item_creator_blocked: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum ReportCombinedView { + Post(PostReportView), + Comment(CommentReportView), + PrivateMessage(PrivateMessageReportView), +} + #[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] #[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] #[serde(transparent)] diff --git a/migrations/2024-12-02-181601_add_report_combined_table/down.sql b/migrations/2024-12-02-181601_add_report_combined_table/down.sql new file mode 100644 index 000000000..b27ba9bc4 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE report_combined; + diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql new file mode 100644 index 000000000..8efb2a074 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -0,0 +1,42 @@ +-- Creates combined tables for +-- Reports: (comment, post, and private_message) +CREATE TABLE report_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_report_id int UNIQUE REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, + comment_report_id int UNIQUE REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, + private_message_report_id int UNIQUE REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id) = 1) +); + +CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC); + +CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO report_combined (published, post_report_id, comment_report_id, private_message_report_id) +SELECT + published, + id, + NULL::int, + NULL::int +FROM + post_report +UNION ALL +SELECT + published, + NULL::int, + id, + NULL::int +FROM + comment_report +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + id +FROM + private_message_report; + diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index eefaf5b87..5e8fb741d 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -58,16 +53,11 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, }, site::{ federated_instances::get_federated_instances, @@ -222,7 +212,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -247,8 +236,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) // Private Message .service( @@ -260,8 +248,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), ) // User .service( diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index a9f71c9da..b03be60f2 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -65,16 +60,12 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, + report_combined::list::list_reports, }, site::{ admin_allow_instance::admin_allow_instance, @@ -235,7 +226,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -259,8 +249,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) // Private Message .service( @@ -271,8 +260,13 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), + ) + // Reports + .service( + scope("/report") + .wrap(rate_limit.message()) + .route("/list", get().to(list_reports)), ) // User .service( From 6015ef045d429a48171c564f19d94d1ae13d20d7 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 20 Dec 2024 17:20:16 -0500 Subject: [PATCH 08/23] Running cargo-features-manager prune. (#5274) - Context #5273 --- Cargo.lock | 73 +++++++++++----------------- Cargo.toml | 13 ++--- crates/api_common/Cargo.toml | 4 +- crates/db_schema/Cargo.toml | 6 +-- crates/db_views_actor/Cargo.toml | 4 +- crates/db_views_moderator/Cargo.toml | 4 +- crates/federate/Cargo.toml | 2 +- crates/utils/Cargo.toml | 7 ++- 8 files changed, 46 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7215d79f..10f629cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.12" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ "flate2", "futures-core", @@ -495,15 +495,15 @@ dependencies = [ [[package]] name = "atom_syndication" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3a5ed3201df5658d1aa45060c5a57dc9dba8a8ada20d696d67cb0c479ee043" +checksum = "3ee79fb83c725eae67b55218870813d2fc39fd85e4f1583848ef9f4f823cfe7c" dependencies = [ "chrono", "derive_builder", "diligent-date-parser", "never", - "quick-xml 0.36.1", + "quick-xml 0.37.1", ] [[package]] @@ -514,21 +514,20 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +checksum = "f47bb8cc16b669d267eeccf585aea077d0882f4777b1c1f740217885d6e6e5a3" dependencies = [ "aws-lc-sys", - "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.21.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" +checksum = "a2101df3813227bbaaaa0b04cd61c534c7954b22bd68d399b440be937dc63ff7" dependencies = [ "bindgen", "cc", @@ -618,9 +617,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.6.0", "cexpr", @@ -907,9 +906,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] @@ -1374,9 +1373,9 @@ dependencies = [ [[package]] name = "diligent-date-parser" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182" +checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9" dependencies = [ "chrono", ] @@ -1471,9 +1470,9 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" dependencies = [ "base64 0.22.1", "memchr", @@ -1599,9 +1598,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fdeflate" @@ -2897,12 +2896,12 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3219,12 +3218,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "mirai-annotations" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" - [[package]] name = "mockall" version = "0.13.0" @@ -3598,9 +3591,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" @@ -3725,9 +3718,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", "syn 2.0.87", @@ -3821,16 +3814,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" -dependencies = [ - "encoding_rs", - "memchr", -] - [[package]] name = "quick-xml" version = "0.37.1" @@ -4324,9 +4307,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" diff --git a/Cargo.toml b/Cargo.toml index b1553be5c..df10a78b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,21 +103,21 @@ diesel-async = "0.5.1" serde = { version = "1.0.215", features = ["derive"] } serde_with = "3.9.0" actix-web = { version = "4.9.0", default-features = false, features = [ - "macros", - "rustls-0_23", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", + "macros", + "rustls-0_23", ] } tracing = "0.1.40" tracing-actix-web = { version = "0.7.10", default-features = false } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } url = { version = "2.5.3", features = ["serde"] } reqwest = { version = "0.12.7", default-features = false, features = [ - "json", "blocking", "gzip", + "json", "rustls-tls", ] } reqwest-middleware = "0.3.3" @@ -126,17 +126,15 @@ clokwerk = "0.4.0" doku = { version = "0.21.1", features = ["url-2"] } bcrypt = "0.15.1" chrono = { version = "0.4.38", features = [ - "serde", "now", + "serde", ], default-features = false } serde_json = { version = "1.0.132", features = ["preserve_order"] } base64 = "0.22.1" uuid = { version = "1.11.0", features = ["serde"] } async-trait = "0.1.83" captcha = "0.0.9" -anyhow = { version = "1.0.93", features = [ - "backtrace", -] } # backtrace is on by default on nightly, but not stable rust +anyhow = { version = "1.0.93", features = ["backtrace"] } diesel_ltree = "0.3.1" serial_test = "3.2.0" tokio = { version = "1.41.1", features = ["full"] } @@ -149,7 +147,6 @@ futures = "0.3.31" http = "1.1" rosetta-i18n = "0.1.3" ts-rs = { version = "10.0.0", features = [ - "serde-compat", "chrono-impl", "no-serde-warnings", "url-impl", diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml index 74a0390ca..b9e8a5a76 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -67,9 +67,9 @@ urlencoding = { workspace = true } mime = { version = "0.3.17", optional = true } mime_guess = "2.0.5" infer = "0.16.0" -webpage = { version = "2.0", default-features = false, features = [ +webpage = { version = "2.0", default-features = false, optional = true, features = [ "serde", -], optional = true } +] } encoding_rs = { version = "0.8.35", optional = true } jsonwebtoken = { version = "9.3.0", optional = true } diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index eac9d6ddd..a511508f8 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -52,8 +52,8 @@ activitypub_federation = { workspace = true, optional = true } lemmy_utils = { workspace = true, optional = true } bcrypt = { workspace = true, optional = true } diesel = { workspace = true, features = [ - "postgres", "chrono", + "postgres", "serde_json", "uuid", ], optional = true } @@ -61,14 +61,14 @@ diesel-derive-newtype = { workspace = true, optional = true } diesel-derive-enum = { workspace = true, optional = true } diesel_migrations = { workspace = true, optional = true } diesel-async = { workspace = true, features = [ - "postgres", "deadpool", + "postgres", ], optional = true } regex = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } async-trait = { workspace = true } tracing = { workspace = true } -deadpool = { version = "0.12.1", features = ["rt_tokio_1"], optional = true } +deadpool = { version = "0.12.1", optional = true, features = ["rt_tokio_1"] } ts-rs = { workspace = true, optional = true } futures-util = { workspace = true } tokio = { workspace = true, optional = true } diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 18a79826b..00f8bdcaf 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -26,13 +26,13 @@ full = [ [dependencies] lemmy_db_schema = { workspace = true } diesel = { workspace = true, features = [ - "postgres", "chrono", + "postgres", "serde_json", ], optional = true } diesel-async = { workspace = true, features = [ - "postgres", "deadpool", + "postgres", ], optional = true } serde = { workspace = true } serde_with = { workspace = true } diff --git a/crates/db_views_moderator/Cargo.toml b/crates/db_views_moderator/Cargo.toml index df8ec1ea2..ae598fa32 100644 --- a/crates/db_views_moderator/Cargo.toml +++ b/crates/db_views_moderator/Cargo.toml @@ -20,13 +20,13 @@ full = ["lemmy_db_schema/full", "diesel", "diesel-async", "ts-rs"] [dependencies] lemmy_db_schema = { workspace = true } diesel = { workspace = true, features = [ - "postgres", "chrono", + "postgres", "serde_json", ], optional = true } diesel-async = { workspace = true, features = [ - "postgres", "deadpool", + "postgres", ], optional = true } serde = { workspace = true } serde_with = { workspace = true } diff --git a/crates/federate/Cargo.toml b/crates/federate/Cargo.toml index 5d7454276..bdfc00678 100644 --- a/crates/federate/Cargo.toml +++ b/crates/federate/Cargo.toml @@ -25,7 +25,7 @@ activitypub_federation.workspace = true anyhow.workspace = true futures.workspace = true chrono.workspace = true -diesel = { workspace = true, features = ["postgres", "chrono", "serde_json"] } +diesel = { workspace = true, features = ["chrono", "postgres", "serde_json"] } diesel-async = { workspace = true, features = ["deadpool", "postgres"] } reqwest.workspace = true serde_json.workspace = true diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 7ed4c0476..ded84132c 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -65,10 +65,10 @@ anyhow = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } strum = { workspace = true } futures = { workspace = true, optional = true } -diesel = { workspace = true, features = ["chrono"], optional = true } +diesel = { workspace = true, optional = true, features = ["chrono"] } http = { workspace = true, optional = true } doku = { workspace = true, features = ["url-2"], optional = true } -uuid = { workspace = true, features = ["serde", "v4"], optional = true } +uuid = { workspace = true, optional = true, features = ["v4"] } rosetta-i18n = { workspace = true, optional = true } tokio = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } @@ -77,9 +77,8 @@ deser-hjson = { version = "2.2.4", optional = true } smart-default = { version = "0.7.1", optional = true } lettre = { version = "0.11.10", default-features = false, features = [ "builder", - "tokio1", - "tokio1-rustls-tls", "smtp-transport", + "tokio1-rustls-tls", ], optional = true } markdown-it = { version = "0.6.1", optional = true } ts-rs = { workspace = true, optional = true } From 7585aac446a060e0d8685e08fa552547ae9750de Mon Sep 17 00:00:00 2001 From: Nutomic Date: Sat, 21 Dec 2024 00:21:09 +0000 Subject: [PATCH 09/23] Fix startup errors, add unit test for scheduled task errors (fixes #5209) (#5269) * Fix startup errors, add ci check (fixes #5209) * normal unit test * cleanup * shear * remove serial * migration --- Cargo.lock | 1 - Cargo.toml | 1 - crates/db_schema/replaceable_schema/utils.sql | 36 +- .../down.sql | 3 + .../up.sql | 3 + src/lib.rs | 15 + src/scheduled_tasks.rs | 530 ++++++++---------- src/session_middleware.rs | 34 +- 8 files changed, 279 insertions(+), 344 deletions(-) create mode 100644 migrations/2024-12-20-090225_update-replaceable-schema/down.sql create mode 100644 migrations/2024-12-20-090225_update-replaceable-schema/up.sql diff --git a/Cargo.lock b/Cargo.lock index 10f629cd9..62f0bcd83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2805,7 +2805,6 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "prometheus", - "reqwest 0.12.8", "reqwest-middleware", "reqwest-tracing", "rustls 0.23.16", diff --git a/Cargo.toml b/Cargo.toml index df10a78b8..b03a01715 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,6 @@ tracing = { workspace = true } tracing-actix-web = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true } -reqwest = { workspace = true } reqwest-middleware = { workspace = true } reqwest-tracing = { workspace = true } clokwerk = { workspace = true } diff --git a/crates/db_schema/replaceable_schema/utils.sql b/crates/db_schema/replaceable_schema/utils.sql index 0c7f42ff2..a9b32f3dd 100644 --- a/crates/db_schema/replaceable_schema/utils.sql +++ b/crates/db_schema/replaceable_schema/utils.sql @@ -186,26 +186,26 @@ BEGIN AND pe.bot_account = FALSE UNION SELECT - pl.person_id, + pa.person_id, p.community_id FROM - post_like pl - INNER JOIN post p ON pl.post_id = p.id - INNER JOIN person pe ON pl.person_id = pe.id + post_actions pa + INNER JOIN post p ON pa.post_id = p.id + INNER JOIN person pe ON pa.person_id = pe.id WHERE - pl.published > ('now'::timestamp - i::interval) + pa.liked > ('now'::timestamp - i::interval) AND pe.bot_account = FALSE UNION SELECT - cl.person_id, + ca.person_id, p.community_id FROM - comment_like cl - INNER JOIN comment c ON cl.comment_id = c.id + comment_actions ca + INNER JOIN comment c ON ca.comment_id = c.id INNER JOIN post p ON c.post_id = p.id - INNER JOIN person pe ON cl.person_id = pe.id + INNER JOIN person pe ON ca.person_id = pe.id WHERE - cl.published > ('now'::timestamp - i::interval) + ca.liked > ('now'::timestamp - i::interval) AND pe.bot_account = FALSE) a GROUP BY community_id; @@ -244,22 +244,22 @@ BEGIN AND pe.bot_account = FALSE UNION SELECT - pl.person_id + pa.person_id FROM - post_like pl - INNER JOIN person pe ON pl.person_id = pe.id + post_actions pa + INNER JOIN person pe ON pa.person_id = pe.id WHERE - pl.published > ('now'::timestamp - i::interval) + pa.liked > ('now'::timestamp - i::interval) AND pe.local = TRUE AND pe.bot_account = FALSE UNION SELECT - cl.person_id + ca.person_id FROM - comment_like cl - INNER JOIN person pe ON cl.person_id = pe.id + comment_actions ca + INNER JOIN person pe ON ca.person_id = pe.id WHERE - cl.published > ('now'::timestamp - i::interval) + ca.liked > ('now'::timestamp - i::interval) AND pe.local = TRUE AND pe.bot_account = FALSE) a; RETURN count_; diff --git a/migrations/2024-12-20-090225_update-replaceable-schema/down.sql b/migrations/2024-12-20-090225_update-replaceable-schema/down.sql new file mode 100644 index 000000000..deb75def2 --- /dev/null +++ b/migrations/2024-12-20-090225_update-replaceable-schema/down.sql @@ -0,0 +1,3 @@ +SELECT + 1; + diff --git a/migrations/2024-12-20-090225_update-replaceable-schema/up.sql b/migrations/2024-12-20-090225_update-replaceable-schema/up.sql new file mode 100644 index 000000000..deb75def2 --- /dev/null +++ b/migrations/2024-12-20-090225_update-replaceable-schema/up.sql @@ -0,0 +1,3 @@ +SELECT + 1; + diff --git a/src/lib.rs b/src/lib.rs index 5586b6159..f344f2927 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -370,3 +370,18 @@ fn cors_config(settings: &Settings) -> Cors { _ => cors_default, } } + +#[cfg(test)] +pub mod tests { + use activitypub_federation::config::Data; + use lemmy_api_common::context::LemmyContext; + use std::env::set_current_dir; + + pub async fn test_context() -> Data { + // hack, necessary so that config file can be loaded from hardcoded, relative path. + // Ignore errors as this gets called once for every test (so changing dir again would fail). + set_current_dir("crates/utils").ok(); + + LemmyContext::init_test_context().await + } +} diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 53c0b888b..b656ac36f 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -40,16 +40,19 @@ use lemmy_db_schema::{ utils::{find_action, functions::coalesce, get_conn, now, DbPool, DELETED_REPLACEMENT_TEXT}, }; use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; -use lemmy_utils::error::LemmyResult; +use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use reqwest_middleware::ClientWithMiddleware; use std::time::Duration; -use tracing::{error, info, warn}; +use tracing::{info, warn}; /// Schedules various cleanup tasks for lemmy in a background thread pub async fn setup(context: Data) -> LemmyResult<()> { // Setup the connections let mut scheduler = AsyncScheduler::new(); - startup_jobs(&mut context.pool()).await; + startup_jobs(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to run startup tasks: {e}")) + .ok(); let context_1 = context.clone(); // Update active counts expired bans and unpublished posts every hour @@ -57,9 +60,18 @@ pub async fn setup(context: Data) -> LemmyResult<()> { let context = context_1.clone(); async move { - active_counts(&mut context.pool()).await; - update_banned_when_expired(&mut context.pool()).await; - delete_instance_block_when_expired(&mut context.pool()).await; + active_counts(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to update active counts: {e}")) + .ok(); + update_banned_when_expired(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to update expired bans: {e}")) + .ok(); + delete_instance_block_when_expired(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to delete expired instance bans: {e}")) + .ok(); } }); @@ -69,9 +81,18 @@ pub async fn setup(context: Data) -> LemmyResult<()> { let context = context_1.reset_request_count(); async move { - update_hot_ranks(&mut context.pool()).await; - delete_expired_captcha_answers(&mut context.pool()).await; - publish_scheduled_posts(&context).await; + update_hot_ranks(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to update hot ranks: {e}")) + .ok(); + delete_expired_captcha_answers(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to delete expired captcha answers: {e}")) + .ok(); + publish_scheduled_posts(&context) + .await + .inspect_err(|e| warn!("Failed to publish scheduled posts: {e}")) + .ok(); } }); @@ -81,7 +102,10 @@ pub async fn setup(context: Data) -> LemmyResult<()> { let context = context_1.clone(); async move { - clear_old_activities(&mut context.pool()).await; + clear_old_activities(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to clear old activities: {e}")) + .ok(); } }); @@ -94,8 +118,14 @@ pub async fn setup(context: Data) -> LemmyResult<()> { let context = context_1.clone(); async move { - overwrite_deleted_posts_and_comments(&mut context.pool()).await; - delete_old_denied_users(&mut context.pool()).await; + overwrite_deleted_posts_and_comments(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to overwrite deleted posts/comments: {e}")) + .ok(); + delete_old_denied_users(&mut context.pool()) + .await + .inspect_err(|e| warn!("Failed to delete old denied users: {e}")) + .ok(); update_instance_software(&mut context.pool(), context.client()) .await .inspect_err(|e| warn!("Failed to update instance software: {e}")) @@ -111,49 +141,44 @@ pub async fn setup(context: Data) -> LemmyResult<()> { } /// Run these on server startup -async fn startup_jobs(pool: &mut DbPool<'_>) { - active_counts(pool).await; - update_hot_ranks(pool).await; - update_banned_when_expired(pool).await; - delete_instance_block_when_expired(pool).await; - clear_old_activities(pool).await; - overwrite_deleted_posts_and_comments(pool).await; - delete_old_denied_users(pool).await; +async fn startup_jobs(pool: &mut DbPool<'_>) -> LemmyResult<()> { + active_counts(pool).await?; + update_hot_ranks(pool).await?; + update_banned_when_expired(pool).await?; + delete_instance_block_when_expired(pool).await?; + clear_old_activities(pool).await?; + overwrite_deleted_posts_and_comments(pool).await?; + delete_old_denied_users(pool).await?; + Ok(()) } /// Update the hot_rank columns for the aggregates tables /// Runs in batches until all necessary rows are updated once -async fn update_hot_ranks(pool: &mut DbPool<'_>) { +async fn update_hot_ranks(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Updating hot ranks for all history..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - process_post_aggregates_ranks_in_batches(&mut conn).await; + process_post_aggregates_ranks_in_batches(&mut conn).await?; - process_ranks_in_batches( - &mut conn, - "comment", - "a.hot_rank != 0", - "SET hot_rank = r.hot_rank(a.score, a.published)", - ) - .await; + process_ranks_in_batches( + &mut conn, + "comment", + "a.hot_rank != 0", + "SET hot_rank = r.hot_rank(a.score, a.published)", + ) + .await?; - process_ranks_in_batches( - &mut conn, - "community", - "a.hot_rank != 0", - "SET hot_rank = r.hot_rank(a.subscribers, a.published)", - ) - .await; + process_ranks_in_batches( + &mut conn, + "community", + "a.hot_rank != 0", + "SET hot_rank = r.hot_rank(a.subscribers, a.published)", + ) + .await?; - info!("Finished hot ranks update!"); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + info!("Finished hot ranks update!"); + Ok(()) } #[derive(QueryableByName)] @@ -171,7 +196,7 @@ async fn process_ranks_in_batches( table_name: &str, where_clause: &str, set_clause: &str, -) { +) -> LemmyResult<()> { let process_start_time: DateTime = Utc.timestamp_opt(0, 0).single().unwrap_or_default(); let update_batch_size = 1000; // Bigger batches than this tend to cause seq scans @@ -180,7 +205,7 @@ async fn process_ranks_in_batches( while let Some(previous_batch_last_published) = previous_batch_result { // Raw `sql_query` is used as a performance optimization - Diesel does not support doing this // in a single query (neither as a CTE, nor using a subquery) - let result = sql_query(format!( + let updated_rows = sql_query(format!( r#"WITH batch AS (SELECT a.{id_column} FROM {aggregates_table} a WHERE a.published > $1 AND ({where_clause}) @@ -196,35 +221,31 @@ async fn process_ranks_in_batches( .bind::(previous_batch_last_published) .bind::(update_batch_size) .get_results::(conn) - .await; + .await + .map_err(|e| { + LemmyErrorType::Unknown(format!("Failed to update {} hot_ranks: {}", table_name, e)) + })?; - match result { - Ok(updated_rows) => { - processed_rows_count += updated_rows.len(); - previous_batch_result = updated_rows.last().map(|row| row.published); - } - Err(e) => { - error!("Failed to update {} hot_ranks: {}", table_name, e); - break; - } - } + processed_rows_count += updated_rows.len(); + previous_batch_result = updated_rows.last().map(|row| row.published); } info!( "Finished process_hot_ranks_in_batches execution for {} (processed {} rows)", table_name, processed_rows_count ); + Ok(()) } /// Post aggregates is a special case, since it needs to join to the community_aggregates /// table, to get the active monthly user counts. -async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) { +async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) -> LemmyResult<()> { let process_start_time: DateTime = Utc.timestamp_opt(0, 0).single().unwrap_or_default(); let update_batch_size = 1000; // Bigger batches than this tend to cause seq scans let mut processed_rows_count = 0; let mut previous_batch_result = Some(process_start_time); while let Some(previous_batch_last_published) = previous_batch_result { - let result = sql_query( + let updated_rows = sql_query( r#"WITH batch AS (SELECT pa.post_id FROM post_aggregates pa WHERE pa.published > $1 @@ -243,283 +264,190 @@ async fn process_post_aggregates_ranks_in_batches(conn: &mut AsyncPgConnection) .bind::(previous_batch_last_published) .bind::(update_batch_size) .get_results::(conn) - .await; + .await.map_err(|e| LemmyErrorType::Unknown(format!("Failed to update {} hot_ranks: {}", "post_aggregates", e)))?; - match result { - Ok(updated_rows) => { - processed_rows_count += updated_rows.len(); - previous_batch_result = updated_rows.last().map(|row| row.published); - } - Err(e) => { - error!("Failed to update {} hot_ranks: {}", "post_aggregates", e); - break; - } - } + processed_rows_count += updated_rows.len(); + previous_batch_result = updated_rows.last().map(|row| row.published); } info!( "Finished process_hot_ranks_in_batches execution for {} (processed {} rows)", "post_aggregates", processed_rows_count ); + Ok(()) } -async fn delete_expired_captcha_answers(pool: &mut DbPool<'_>) { - let conn = get_conn(pool).await; +async fn delete_expired_captcha_answers(pool: &mut DbPool<'_>) -> LemmyResult<()> { + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - diesel::delete( - captcha_answer::table - .filter(captcha_answer::published.lt(now() - IntervalDsl::minutes(10))), - ) - .execute(&mut conn) - .await - .map(|_| { - info!("Done."); - }) - .inspect_err(|e| error!("Failed to clear old captcha answers: {e}")) - .ok(); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + diesel::delete( + captcha_answer::table.filter(captcha_answer::published.lt(now() - IntervalDsl::minutes(10))), + ) + .execute(&mut conn) + .await?; + info!("Done."); + + Ok(()) } /// Clear old activities (this table gets very large) -async fn clear_old_activities(pool: &mut DbPool<'_>) { +async fn clear_old_activities(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Clearing old activities..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - diesel::delete( - sent_activity::table.filter(sent_activity::published.lt(now() - IntervalDsl::days(7))), - ) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to clear old sent activities: {e}")) - .ok(); + diesel::delete( + sent_activity::table.filter(sent_activity::published.lt(now() - IntervalDsl::days(7))), + ) + .execute(&mut conn) + .await?; - diesel::delete( - received_activity::table - .filter(received_activity::published.lt(now() - IntervalDsl::days(7))), - ) - .execute(&mut conn) - .await - .map(|_| info!("Done.")) - .inspect_err(|e| error!("Failed to clear old received activities: {e}")) - .ok(); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + diesel::delete( + received_activity::table.filter(received_activity::published.lt(now() - IntervalDsl::days(7))), + ) + .execute(&mut conn) + .await?; + info!("Done."); + Ok(()) } -async fn delete_old_denied_users(pool: &mut DbPool<'_>) { - LocalUser::delete_old_denied_local_users(pool) - .await - .map(|_| { - info!("Done."); - }) - .inspect_err(|e| error!("Failed to deleted old denied users: {e}")) - .ok(); +async fn delete_old_denied_users(pool: &mut DbPool<'_>) -> LemmyResult<()> { + LocalUser::delete_old_denied_local_users(pool).await?; + info!("Done."); + Ok(()) } /// overwrite posts and comments 30d after deletion -async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) { +async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Overwriting deleted posts..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - diesel::update( - post::table - .filter(post::deleted.eq(true)) - .filter(post::updated.lt(now().nullable() - 1.months())) - .filter(post::body.ne(DELETED_REPLACEMENT_TEXT)), - ) - .set(( - post::body.eq(DELETED_REPLACEMENT_TEXT), - post::name.eq(DELETED_REPLACEMENT_TEXT), - )) - .execute(&mut conn) - .await - .map(|_| { - info!("Done."); - }) - .inspect_err(|e| error!("Failed to overwrite deleted posts: {e}")) - .ok(); + diesel::update( + post::table + .filter(post::deleted.eq(true)) + .filter(post::updated.lt(now().nullable() - 1.months())) + .filter(post::body.ne(DELETED_REPLACEMENT_TEXT)), + ) + .set(( + post::body.eq(DELETED_REPLACEMENT_TEXT), + post::name.eq(DELETED_REPLACEMENT_TEXT), + )) + .execute(&mut conn) + .await?; - info!("Overwriting deleted comments..."); - diesel::update( - comment::table - .filter(comment::deleted.eq(true)) - .filter(comment::updated.lt(now().nullable() - 1.months())) - .filter(comment::content.ne(DELETED_REPLACEMENT_TEXT)), - ) - .set(comment::content.eq(DELETED_REPLACEMENT_TEXT)) - .execute(&mut conn) - .await - .map(|_| { - info!("Done."); - }) - .inspect_err(|e| error!("Failed to overwrite deleted comments: {e}")) - .ok(); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + info!("Overwriting deleted comments..."); + diesel::update( + comment::table + .filter(comment::deleted.eq(true)) + .filter(comment::updated.lt(now().nullable() - 1.months())) + .filter(comment::content.ne(DELETED_REPLACEMENT_TEXT)), + ) + .set(comment::content.eq(DELETED_REPLACEMENT_TEXT)) + .execute(&mut conn) + .await?; + info!("Done."); + Ok(()) } /// Re-calculate the site and community active counts every 12 hours -async fn active_counts(pool: &mut DbPool<'_>) { +async fn active_counts(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Updating active site and community aggregates ..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - let intervals = vec![ - ("1 day", "day"), - ("1 week", "week"), - ("1 month", "month"), - ("6 months", "half_year"), - ]; + let intervals = vec![ + ("1 day", "day"), + ("1 week", "week"), + ("1 month", "month"), + ("6 months", "half_year"), + ]; - for (full_form, abbr) in &intervals { - let update_site_stmt = format!( + for (full_form, abbr) in &intervals { + let update_site_stmt = format!( "update site_aggregates set users_active_{} = (select * from r.site_aggregates_activity('{}')) where site_id = 1", abbr, full_form ); - sql_query(update_site_stmt) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to update site stats: {e}")) - .ok(); + sql_query(update_site_stmt).execute(&mut conn).await?; - let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from r.community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); - sql_query(update_community_stmt) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to update community stats: {e}")) - .ok(); - } - - info!("Done."); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } + let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from r.community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", abbr, full_form); + sql_query(update_community_stmt).execute(&mut conn).await?; } + + info!("Done."); + Ok(()) } /// Set banned to false after ban expires -async fn update_banned_when_expired(pool: &mut DbPool<'_>) { +async fn update_banned_when_expired(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Updating banned column if it expires ..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - diesel::update( - person::table - .filter(person::banned.eq(true)) - .filter(person::ban_expires.lt(now().nullable())), - ) - .set(person::banned.eq(false)) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to update person.banned when expires: {e}")) - .ok(); + diesel::update( + person::table + .filter(person::banned.eq(true)) + .filter(person::ban_expires.lt(now().nullable())), + ) + .set(person::banned.eq(false)) + .execute(&mut conn) + .await?; - diesel::delete( - community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())), - ) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}")) - .ok(); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + diesel::delete( + community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await?; + Ok(()) } /// Set banned to false after ban expires -async fn delete_instance_block_when_expired(pool: &mut DbPool<'_>) { +async fn delete_instance_block_when_expired(pool: &mut DbPool<'_>) -> LemmyResult<()> { info!("Delete instance blocks when expired ..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - diesel::delete( - federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), - ) - .execute(&mut conn) - .await - .inspect_err(|e| error!("Failed to remove federation_blocklist expired rows: {e}")) - .ok(); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } - } + diesel::delete( + federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await?; + Ok(()) } /// Find all unpublished posts with scheduled date in the future, and publish them. -async fn publish_scheduled_posts(context: &Data) { +async fn publish_scheduled_posts(context: &Data) -> LemmyResult<()> { let pool = &mut context.pool(); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - let scheduled_posts: Vec<_> = post::table - .inner_join(community::table) - .inner_join(person::table) - // find all posts which have scheduled_publish_time that is in the past - .filter(post::scheduled_publish_time.is_not_null()) - .filter(coalesce(post::scheduled_publish_time, now()).lt(now())) - // make sure the post, person and community are still around - .filter(not(post::deleted.or(post::removed))) - .filter(not(person::banned.or(person::deleted))) - .filter(not(community::removed.or(community::deleted))) - // ensure that user isnt banned from community - .filter(not(exists(find_action( - community_actions::received_ban, - (person::id, community::id), - )))) - .select((post::all_columns, community::all_columns)) - .get_results::<(Post, Community)>(&mut conn) - .await - .inspect_err(|e| error!("Failed to read unpublished posts: {e}")) - .ok() - .unwrap_or_default(); + let scheduled_posts: Vec<_> = post::table + .inner_join(community::table) + .inner_join(person::table) + // find all posts which have scheduled_publish_time that is in the past + .filter(post::scheduled_publish_time.is_not_null()) + .filter(coalesce(post::scheduled_publish_time, now()).lt(now())) + // make sure the post, person and community are still around + .filter(not(post::deleted.or(post::removed))) + .filter(not(person::banned.or(person::deleted))) + .filter(not(community::removed.or(community::deleted))) + // ensure that user isnt banned from community + .filter(not(exists(find_action( + community_actions::received_ban, + (person::id, community::id), + )))) + .select((post::all_columns, community::all_columns)) + .get_results::<(Post, Community)>(&mut conn) + .await?; - for (post, community) in scheduled_posts { - // mark post as published in db - let form = PostUpdateForm { - scheduled_publish_time: Some(None), - ..Default::default() - }; - Post::update(&mut context.pool(), post.id, &form) - .await - .inspect_err(|e| error!("Failed update scheduled post: {e}")) - .ok(); + for (post, community) in scheduled_posts { + // mark post as published in db + let form = PostUpdateForm { + scheduled_publish_time: Some(None), + ..Default::default() + }; + Post::update(&mut context.pool(), post.id, &form).await?; - // send out post via federation and webmention - let send_activity = SendActivityData::CreatePost(post.clone()); - ActivityChannel::submit_activity(send_activity, context) - .inspect_err(|e| error!("Failed federate scheduled post: {e}")) - .ok(); - send_webmention(post, community); - } - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); - } + // send out post via federation and webmention + let send_activity = SendActivityData::CreatePost(post.clone()); + ActivityChannel::submit_activity(send_activity, context)?; + send_webmention(post, community); } + Ok(()) } /// Updates the instance software and version. @@ -533,23 +461,16 @@ async fn update_instance_software( client: &ClientWithMiddleware, ) -> LemmyResult<()> { info!("Updating instances software and versions..."); - let conn = get_conn(pool).await; + let mut conn = get_conn(pool).await?; - match conn { - Ok(mut conn) => { - let instances = instance::table.get_results::(&mut conn).await?; + let instances = instance::table.get_results::(&mut conn).await?; - for instance in instances { - if let Some(form) = build_update_instance_form(&instance.domain, client).await { - Instance::update(pool, instance.id, form).await?; - } - } - info!("Finished updating instances software and versions..."); - } - Err(e) => { - error!("Failed to get connection from pool: {e}"); + for instance in instances { + if let Some(form) = build_update_instance_form(&instance.domain, client).await { + Instance::update(pool, instance.id, form).await?; } } + info!("Finished updating instances software and versions..."); Ok(()) } @@ -621,7 +542,8 @@ async fn build_update_instance_form( #[cfg(test)] mod tests { - use crate::scheduled_tasks::build_update_instance_form; + use super::*; + use crate::{scheduled_tasks::build_update_instance_form, tests::test_context}; use lemmy_api_common::request::client_builder; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, @@ -632,7 +554,6 @@ mod tests { use serial_test::serial; #[tokio::test] - #[serial] async fn test_nodeinfo_lemmy_ml() -> LemmyResult<()> { let client = ClientBuilder::new(client_builder(&Settings::default()).build()?).build(); let form = build_update_instance_form("lemmy.ml", &client) @@ -643,7 +564,6 @@ mod tests { } #[tokio::test] - #[serial] async fn test_nodeinfo_mastodon_social() -> LemmyResult<()> { let client = ClientBuilder::new(client_builder(&Settings::default()).build()?).build(); let form = build_update_instance_form("mastodon.social", &client) @@ -652,4 +572,16 @@ mod tests { assert_eq!(form.software.ok_or(LemmyErrorType::NotFound)?, "mastodon"); Ok(()) } + + #[tokio::test] + #[serial] + async fn test_scheduled_tasks_no_errors() -> LemmyResult<()> { + let context = test_context().await; + + startup_jobs(&mut context.pool()).await?; + update_instance_software(&mut context.pool(), context.client()).await?; + delete_expired_captcha_answers(&mut context.pool()).await?; + publish_scheduled_posts(&context).await?; + Ok(()) + } } diff --git a/src/session_middleware.rs b/src/session_middleware.rs index b495bdbb9..7e0f38a4e 100644 --- a/src/session_middleware.rs +++ b/src/session_middleware.rs @@ -99,7 +99,7 @@ where #[cfg(test)] mod tests { - use super::*; + use crate::tests::test_context; use actix_web::test::TestRequest; use lemmy_api_common::claims::Claims; use lemmy_db_schema::{ @@ -107,45 +107,29 @@ mod tests { instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, person::{Person, PersonInsertForm}, - secret::Secret, }, traits::Crud, - utils::build_db_pool_for_tests, }; - use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell}; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; - use reqwest::Client; - use reqwest_middleware::ClientBuilder; use serial_test::serial; - use std::env::set_current_dir; #[tokio::test] #[serial] async fn test_session_auth() -> LemmyResult<()> { - // hack, necessary so that config file can be loaded from hardcoded, relative path - set_current_dir("crates/utils")?; + let context = test_context().await; - let pool_ = build_db_pool_for_tests(); - let pool = &mut (&pool_).into(); - - let secret = Secret::init(pool).await?; - - let context = LemmyContext::create( - pool_.clone(), - ClientBuilder::new(Client::default()).build(), - secret, - RateLimitCell::with_test_config(), - ); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + let inserted_instance = + Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?; let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812"); - let inserted_person = Person::create(pool, &new_person).await?; + let inserted_person = Person::create(&mut context.pool(), &new_person).await?; let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); - let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; + let inserted_local_user = + LocalUser::create(&mut context.pool(), &local_user_form, vec![]).await?; let req = TestRequest::default().to_http_request(); let jwt = Claims::generate(inserted_local_user.id, req, &context).await?; @@ -153,7 +137,7 @@ mod tests { let valid = Claims::validate(&jwt, &context).await; assert!(valid.is_ok()); - let num_deleted = Person::delete(pool, inserted_person.id).await?; + let num_deleted = Person::delete(&mut context.pool(), inserted_person.id).await?; assert_eq!(1, num_deleted); Ok(()) From cc190aefdfffd917e274f2239b9f994d78f96f3d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 24 Dec 2024 14:24:12 -0500 Subject: [PATCH 10/23] Use codeberg pictrs mirror to fix CI. (#5286) --- api_tests/prepare-drone-federation-test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index e5a4bc604..c5151b7f5 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -15,7 +15,9 @@ export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queu # pictrs setup if [ ! -f "api_tests/pict-rs" ]; then - curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs + # This one sometimes goes down + # curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs + curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.6/pict-rs-linux-amd64" -o api_tests/pict-rs chmod +x api_tests/pict-rs fi ./api_tests/pict-rs \ From 76034f058d322bf7891c05894994e24b29d2d3a3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 27 Dec 2024 12:52:05 -0500 Subject: [PATCH 11/23] Cleaning up oauth providers conversion. (#5280) * Cleaning up oauth providers conversion. * Use into_iter --- crates/db_schema/src/impls/oauth_provider.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/db_schema/src/impls/oauth_provider.rs b/crates/db_schema/src/impls/oauth_provider.rs index 9d7e791e7..7665ba050 100644 --- a/crates/db_schema/src/impls/oauth_provider.rs +++ b/crates/db_schema/src/impls/oauth_provider.rs @@ -55,13 +55,11 @@ impl OAuthProvider { pub fn convert_providers_to_public( oauth_providers: Vec, ) -> Vec { - let mut result = Vec::::new(); - for oauth_provider in &oauth_providers { - if oauth_provider.enabled { - result.push(PublicOAuthProvider(oauth_provider.clone())); - } - } - result + oauth_providers + .into_iter() + .filter(|x| x.enabled) + .map(PublicOAuthProvider) + .collect() } pub async fn get_all_public(pool: &mut DbPool<'_>) -> Result, Error> { From f76322e3f5e6792df569b01d501238a5efc683f0 Mon Sep 17 00:00:00 2001 From: Richard Schwab Date: Fri, 27 Dec 2024 18:53:23 +0100 Subject: [PATCH 12/23] Reduce false positives in URL blocklist to reduce scunthorpe problem by matching at word boundaries. (#5282) This addresses an issue brought up on matrix where blocking rt.com resulted in links to deviantart.com getting blocked. --- crates/api_common/src/utils.rs | 4 +- crates/api_crud/src/site/update.rs | 2 + crates/utils/src/utils/markdown/mod.rs | 62 +++++++++++++++++--------- 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 80f559edb..ee232c93a 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -552,7 +552,9 @@ pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; // The urls are already validated on saving, so just escape them. - let regexes = urls.iter().map(|url| escape(&url.url)); + // If this regex creation changes it must be synced with + // lemmy_utils::utils::markdown::create_url_blocklist_test_regex_set. + let regexes = urls.iter().map(|url| format!(r"\b{}\b", escape(&url.url))); let set = RegexSet::new(regexes)?; Ok(set) diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index d2585ea43..8b0dfe0c5 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -151,6 +151,8 @@ pub async fn update_site( .ok(); if let Some(url_blocklist) = data.blocked_urls.clone() { + // If this validation changes it must be synced with + // lemmy_utils::utils::markdown::create_url_blocklist_test_regex_set. let parsed_urls = check_urls_are_valid(&url_blocklist)?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; } diff --git a/crates/utils/src/utils/markdown/mod.rs b/crates/utils/src/utils/markdown/mod.rs index ba509596e..25ac0ffd6 100644 --- a/crates/utils/src/utils/markdown/mod.rs +++ b/crates/utils/src/utils/markdown/mod.rs @@ -47,8 +47,10 @@ pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> Lemm mod tests { use super::*; + use crate::utils::validation::check_urls_are_valid; use image_links::markdown_rewrite_image_links; use pretty_assertions::assert_eq; + use regex::escape; #[test] fn test_basic_markdown() { @@ -191,9 +193,20 @@ mod tests { }); } + // This replicates the logic when saving url blocklist patterns and querying them. + // Refer to lemmy_api_crud::site::update::update_site and + // lemmy_api_common::utils::get_url_blocklist(). + fn create_url_blocklist_test_regex_set(patterns: Vec<&str>) -> LemmyResult { + let url_blocklist = patterns.iter().map(|&s| s.to_string()).collect(); + let valid_urls = check_urls_are_valid(&url_blocklist)?; + let regexes = valid_urls.iter().map(|p| format!(r"\b{}\b", escape(p))); + let set = RegexSet::new(regexes)?; + Ok(set) + } + #[test] fn test_url_blocking() -> LemmyResult<()> { - let set = RegexSet::new(vec![r"(https://)?example\.com/?"])?; + let set = create_url_blocklist_test_regex_set(vec!["example.com/"])?; assert!( markdown_check_for_blocked_urls(&String::from("[](https://example.com)"), &set).is_err() @@ -221,37 +234,42 @@ mod tests { ) .is_err()); - let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"])?; - assert!(markdown_check_for_blocked_urls( - &String::from("![](https://example.com/spam.jpg)"), - &set - ) - .is_err()); + let set = create_url_blocklist_test_regex_set(vec!["example.com/spam.jpg"])?; + assert!(markdown_check_for_blocked_urls("![](https://example.com/spam.jpg)", &set).is_err()); + assert!(markdown_check_for_blocked_urls("![](https://example.com/spam.jpg1)", &set).is_ok()); + // TODO: the following should not be matched, scunthorpe problem. + assert!( + markdown_check_for_blocked_urls("![](https://example.com/spam.jpg.html)", &set).is_err() + ); - let set = RegexSet::new(vec![ - r"(https://)?quo\.example\.com/?", - r"(https://)?foo\.example\.com/?", - r"(https://)?bar\.example\.com/?", + let set = create_url_blocklist_test_regex_set(vec![ + r"quo.example.com/", + r"foo.example.com/", + r"bar.example.com/", ])?; - assert!( - markdown_check_for_blocked_urls(&String::from("https://baz.example.com"), &set).is_ok() - ); + assert!(markdown_check_for_blocked_urls("https://baz.example.com", &set).is_ok()); - assert!( - markdown_check_for_blocked_urls(&String::from("https://bar.example.com"), &set).is_err() - ); + assert!(markdown_check_for_blocked_urls("https://bar.example.com", &set).is_err()); - let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"])?; + let set = create_url_blocklist_test_regex_set(vec!["example.com/banned_page"])?; - assert!( - markdown_check_for_blocked_urls(&String::from("https://example.com/page"), &set).is_ok() - ); + assert!(markdown_check_for_blocked_urls("https://example.com/page", &set).is_ok()); - let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"])?; + let set = create_url_blocklist_test_regex_set(vec!["ex.mple.com/"])?; assert!(markdown_check_for_blocked_urls("example.com", &set).is_ok()); + let set = create_url_blocklist_test_regex_set(vec!["rt.com/"])?; + + assert!(markdown_check_for_blocked_urls("deviantart.com", &set).is_ok()); + assert!(markdown_check_for_blocked_urls("art.com.example.com", &set).is_ok()); + assert!(markdown_check_for_blocked_urls("https://rt.com/abc", &set).is_err()); + assert!(markdown_check_for_blocked_urls("go to rt.com.", &set).is_err()); + assert!(markdown_check_for_blocked_urls("check out rt.computer", &set).is_ok()); + // TODO: the following should not be matched, scunthorpe problem. + assert!(markdown_check_for_blocked_urls("rt.com.example.com", &set).is_err()); + Ok(()) } From 83bf60571ef152671db8f638976a756ad3a294af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 20:38:21 -0500 Subject: [PATCH 13/23] chore(deps): update docker (#5293) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .woodpecker.yml | 2 +- docker/Dockerfile | 2 +- docker/docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 8bbae613e..2eb7d277e 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,7 @@ variables: # as well. Otherwise release builds can fail if Lemmy or dependencies rely on new Rust # features. In particular the ARM builder image needs to be updated manually in the repo below: # https://github.com/raskyld/lemmy-cross-toolchains - - &rust_image "rust:1.81" + - &rust_image "rust:1.83" - &rust_nightly_image "rustlang/rust:nightly" - &install_pnpm "corepack enable pnpm" - &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin" diff --git a/docker/Dockerfile b/docker/Dockerfile index 93f17bb95..5bb39555a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.10 +# syntax=docker/dockerfile:1.12 ARG RUST_VERSION=1.81 ARG CARGO_BUILD_FEATURES=default ARG RUST_RELEASE_MODE=debug diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index cb438af3a..dc978244e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -53,7 +53,7 @@ services: lemmy-ui: # use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build". - image: dessalines/lemmy-ui:0.19.6 + image: dessalines/lemmy-ui:0.19.8 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy ui image for development. make sure to comment out "image". # run: docker compose up --build From 34672ab21b193a8f774d6d3a5e88a58a1ba5b185 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 21:16:18 -0500 Subject: [PATCH 14/23] chore(deps): update rust crate anyhow to v1.0.95 (#5294) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62f0bcd83..b651f82cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" dependencies = [ "backtrace", ] From 39aace0c8f2a3d1510f664ce3276d2ec4aa3a9df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 23:35:13 -0500 Subject: [PATCH 15/23] chore(deps): update rust crate clap to v4.5.23 (#5296) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b651f82cc..009566cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -853,9 +853,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -877,9 +877,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clearurls" From ffc3493abed7cc2f901f228a2c66cb740214e0c3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 1 Jan 2025 12:47:21 -0500 Subject: [PATCH 16/23] Applying some clippy fixes. (#5292) --- crates/api_crud/src/post/create.rs | 2 +- crates/apub/src/activities/block/block_user.rs | 2 +- crates/apub/src/activities/block/undo_block_user.rs | 2 +- crates/apub/src/activities/community/update.rs | 4 ++-- crates/apub/src/objects/comment.rs | 4 ++-- crates/apub/src/objects/person.rs | 4 ++-- crates/apub/src/objects/post.rs | 4 ++-- crates/apub/src/objects/private_message.rs | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 948a7617e..452144faa 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -107,7 +107,7 @@ pub async fn create_post( let scheduled_publish_time = convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?; let post_form = PostInsertForm { - url: url.map(Into::into), + url, body, alt_text: data.alt_text.clone(), nsfw: data.nsfw, diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 64c402482..14b9f9adc 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -152,7 +152,7 @@ impl ActivityHandler for BlockUser { #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; - let expires = self.end_time.map(Into::into); + let expires = self.end_time; let mod_person = self.actor.dereference(context).await?; let blocked_person = self.object.dereference(context).await?; let target = self.target.dereference(context).await?; diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 122eae429..55715fd30 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -100,7 +100,7 @@ impl ActivityHandler for UndoBlockUser { #[tracing::instrument(skip_all)] async fn receive(self, context: &Data) -> LemmyResult<()> { insert_received_activity(&self.id, context).await?; - let expires = self.object.end_time.map(Into::into); + let expires = self.object.end_time; let mod_person = self.actor.dereference(context).await?; let blocked_person = self.object.object.dereference(context).await?; match self.object.target.dereference(context).await? { diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index fadf918bd..b6bc50ca0 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -98,8 +98,8 @@ impl ActivityHandler for UpdateCommunity { &None, &self.object.source, )), - published: self.object.published.map(Into::into), - updated: Some(self.object.updated.map(Into::into)), + published: self.object.published, + updated: Some(self.object.updated), nsfw: Some(self.object.sensitive.unwrap_or(false)), actor_id: Some(self.object.id.into()), public_key: Some(self.object.public_key.public_key_pem), diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 2c8ed9f9d..ed9a9e1a2 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -194,8 +194,8 @@ impl Object for ApubComment { post_id: post.id, content, removed: None, - published: note.published.map(Into::into), - updated: note.updated.map(Into::into), + published: note.published, + updated: note.updated, deleted: Some(false), ap_id: Some(note.id.into()), distinguished: note.distinguished, diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 97b83c194..50f8e8563 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -167,8 +167,8 @@ impl Object for ApubPerson { deleted: Some(false), avatar, banner, - published: person.published.map(Into::into), - updated: person.updated.map(Into::into), + published: person.published, + updated: person.updated, actor_id: Some(person.id.into()), bio, local: Some(false), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index bcd1dbf8e..0dd9201c2 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -249,8 +249,8 @@ impl Object for ApubPost { url: url.map(Into::into), body, alt_text, - published: page.published.map(Into::into), - updated: page.updated.map(Into::into), + published: page.published, + updated: page.updated, deleted: Some(false), nsfw: page.sensitive, ap_id: Some(page.id.clone().into()), diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index ec3e16fac..521419c82 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -166,8 +166,8 @@ impl Object for ApubPrivateMessage { creator_id: creator.id, recipient_id: recipient.id, content, - published: note.published.map(Into::into), - updated: note.updated.map(Into::into), + published: note.published, + updated: note.updated, deleted: Some(false), read: None, ap_id: Some(note.id.into()), From b8556fa5cf9211131b2198d623c300f8fee3ea77 Mon Sep 17 00:00:00 2001 From: dullbananas Date: Wed, 1 Jan 2025 10:52:20 -0700 Subject: [PATCH 17/23] Shorten "options" variable name (#5285) * comment view, post view * do rename everywhere else * fix person_view --------- Co-authored-by: Dessalines --- crates/db_views/src/comment_view.rs | 55 +++++++------- crates/db_views/src/post_view.rs | 75 ++++++++----------- crates/db_views/src/private_message_view.rs | 11 ++- .../src/registration_application_view.rs | 8 +- .../db_views_actor/src/comment_reply_view.rs | 14 ++-- crates/db_views_actor/src/community_view.rs | 20 ++--- .../db_views_actor/src/person_mention_view.rs | 14 ++-- crates/db_views_actor/src/person_view.rs | 10 +-- 8 files changed, 96 insertions(+), 111 deletions(-) diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 2cf751f9f..504f3cc4e 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -141,28 +141,25 @@ fn queries<'a>() -> Queries< query.first(&mut conn).await }; - let list = move |mut conn: DbConn<'a>, (options, site): (CommentQuery<'a>, &'a Site)| async move { + let list = move |mut conn: DbConn<'a>, (o, site): (CommentQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let local_user_id_join = options - .local_user - .local_user_id() - .unwrap_or(LocalUserId(-1)); + let local_user_id_join = o.local_user.local_user_id().unwrap_or(LocalUserId(-1)); - let mut query = all_joins(comment::table.into_boxed(), options.local_user.person_id()); + let mut query = all_joins(comment::table.into_boxed(), o.local_user.person_id()); - if let Some(creator_id) = options.creator_id { + if let Some(creator_id) = o.creator_id { query = query.filter(comment::creator_id.eq(creator_id)); }; - if let Some(post_id) = options.post_id { + if let Some(post_id) = o.post_id { query = query.filter(comment::post_id.eq(post_id)); }; - if let Some(parent_path) = options.parent_path.as_ref() { + if let Some(parent_path) = o.parent_path.as_ref() { query = query.filter(comment::path.contained_by(parent_path)); }; //filtering out removed and deleted comments from search - if let Some(search_term) = options.search_term { + if let Some(search_term) = o.search_term { query = query.filter( comment::content .ilike(fuzzy_search(&search_term)) @@ -170,13 +167,13 @@ fn queries<'a>() -> Queries< ); }; - if let Some(community_id) = options.community_id { + if let Some(community_id) = o.community_id { query = query.filter(post::community_id.eq(community_id)); } let is_subscribed = community_actions::followed.is_not_null(); - match options.listing_type.unwrap_or_default() { + match o.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */ ListingType::Local => { query = query @@ -190,32 +187,30 @@ fn queries<'a>() -> Queries< } // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { + if o.saved_only.unwrap_or_default() { query = query .filter(comment_actions::saved.is_not_null()) .then_order_by(comment_actions::saved.desc()); } - if let Some(my_id) = options.local_user.person_id() { + if let Some(my_id) = o.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); - if options.liked_only.unwrap_or_default() { + if o.liked_only.unwrap_or_default() { query = query .filter(not_creator_filter) .filter(comment_actions::like_score.eq(1)); - } else if options.disliked_only.unwrap_or_default() { + } else if o.disliked_only.unwrap_or_default() { query = query .filter(not_creator_filter) .filter(comment_actions::like_score.eq(-1)); } } - if !options.local_user.show_bot_accounts() { + if !o.local_user.show_bot_accounts() { query = query.filter(person::bot_account.eq(false)); }; - if options.local_user.is_some() - && options.listing_type.unwrap_or_default() != ListingType::ModeratorView - { + if o.local_user.is_some() && o.listing_type.unwrap_or_default() != ListingType::ModeratorView { // Filter out the rows with missing languages query = query.filter(exists( local_user_language::table.filter( @@ -232,15 +227,15 @@ fn queries<'a>() -> Queries< .filter(person_actions::blocked.is_null()); }; - if !options.local_user.show_nsfw(site) { + if !o.local_user.show_nsfw(site) { query = query .filter(post::nsfw.eq(false)) .filter(community::nsfw.eq(false)); }; - query = options.local_user.visible_communities_only(query); + query = o.local_user.visible_communities_only(query); - if !options.local_user.is_admin() { + if !o.local_user.is_admin() { query = query.filter( community::visibility .ne(CommunityVisibility::Private) @@ -249,8 +244,8 @@ fn queries<'a>() -> Queries< } // A Max depth given means its a tree fetch - let (limit, offset) = if let Some(max_depth) = options.max_depth { - let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() { + let (limit, offset) = if let Some(max_depth) = o.max_depth { + let depth_limit = if let Some(parent_path) = o.parent_path.as_ref() { parent_path.0.split('.').count() as i32 + max_depth // Add one because of root "0" } else { @@ -261,7 +256,7 @@ fn queries<'a>() -> Queries< // only order if filtering by a post id, or parent_path. DOS potential otherwise and max_depth // + !post_id isn't used anyways (afaik) - if options.post_id.is_some() || options.parent_path.is_some() { + if o.post_id.is_some() || o.parent_path.is_some() { // Always order by the parent path first query = query.then_order_by(subpath(comment::path, 0, -1)); } @@ -278,16 +273,16 @@ fn queries<'a>() -> Queries< // (i64::MAX, 0) (300, 0) } else { - // limit_and_offset_unlimited(options.page, options.limit) - limit_and_offset(options.page, options.limit)? + // limit_and_offset_unlimited(o.page, o.limit) + limit_and_offset(o.page, o.limit)? }; // distinguished comments should go first when viewing post - if options.post_id.is_some() || options.parent_path.is_some() { + if o.post_id.is_some() || o.parent_path.is_some() { query = query.then_order_by(comment::distinguished.desc()); } - query = match options.sort.unwrap_or(CommentSortType::Hot) { + query = match o.sort.unwrap_or(CommentSortType::Hot) { CommentSortType::Hot => query .then_order_by(comment_aggregates::hot_rank.desc()) .then_order_by(comment_aggregates::score.desc()), diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 6ed89e364..741df57ff 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -226,23 +226,20 @@ fn queries<'a>() -> Queries< .await }; - let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { + let list = move |mut conn: DbConn<'a>, (o, site): (PostQuery<'a>, &'a Site)| async move { // The left join below will return None in this case - let local_user_id_join = options - .local_user - .local_user_id() - .unwrap_or(LocalUserId(-1)); + let local_user_id_join = o.local_user.local_user_id().unwrap_or(LocalUserId(-1)); let mut query = all_joins( post_aggregates::table.into_boxed(), - options.local_user.person_id(), + o.local_user.person_id(), ); // hide posts from deleted communities query = query.filter(community::deleted.eq(false)); // only creator can see deleted posts and unpublished scheduled posts - if let Some(person_id) = options.local_user.person_id() { + if let Some(person_id) = o.local_user.person_id() { query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id))); query = query.filter( post::scheduled_publish_time @@ -256,21 +253,21 @@ fn queries<'a>() -> Queries< } // only show removed posts to admin when viewing user profile - if !(options.creator_id.is_some() && options.local_user.is_admin()) { + if !(o.creator_id.is_some() && o.local_user.is_admin()) { query = query .filter(community::removed.eq(false)) .filter(post::removed.eq(false)); } - if let Some(community_id) = options.community_id { + if let Some(community_id) = o.community_id { query = query.filter(post_aggregates::community_id.eq(community_id)); } - if let Some(creator_id) = options.creator_id { + if let Some(creator_id) = o.creator_id { query = query.filter(post_aggregates::creator_id.eq(creator_id)); } let is_subscribed = community_actions::followed.is_not_null(); - match options.listing_type.unwrap_or_default() { + match o.listing_type.unwrap_or_default() { ListingType::Subscribed => query = query.filter(is_subscribed), ListingType::Local => { query = query @@ -283,14 +280,14 @@ fn queries<'a>() -> Queries< } } - if let Some(search_term) = &options.search_term { - if options.url_only.unwrap_or_default() { + if let Some(search_term) = &o.search_term { + if o.url_only.unwrap_or_default() { query = query.filter(post::url.eq(search_term)); } else { let searcher = fuzzy_search(search_term); let name_filter = post::name.ilike(searcher.clone()); let body_filter = post::body.ilike(searcher.clone()); - query = if options.title_only.unwrap_or_default() { + query = if o.title_only.unwrap_or_default() { query.filter(name_filter) } else { query.filter(name_filter.or(body_filter)) @@ -299,64 +296,58 @@ fn queries<'a>() -> Queries< } } - if !options - .show_nsfw - .unwrap_or(options.local_user.show_nsfw(site)) - { + if !o.show_nsfw.unwrap_or(o.local_user.show_nsfw(site)) { query = query .filter(post::nsfw.eq(false)) .filter(community::nsfw.eq(false)); }; - if !options.local_user.show_bot_accounts() { + if !o.local_user.show_bot_accounts() { query = query.filter(person::bot_account.eq(false)); }; // Filter to show only posts with no comments - if options.no_comments_only.unwrap_or_default() { + if o.no_comments_only.unwrap_or_default() { query = query.filter(post_aggregates::comments.eq(0)); }; // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { + if o.saved_only.unwrap_or_default() { query = query .filter(post_actions::saved.is_not_null()) .then_order_by(post_actions::saved.desc()); } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. - else if !options - .show_read - .unwrap_or(options.local_user.show_read_posts()) - { + else if !o.show_read.unwrap_or(o.local_user.show_read_posts()) { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views - if options.creator_id.is_none() { + if o.creator_id.is_none() { query = query.filter(post_actions::read.is_null()); } } // If a creator id isn't given (IE its on home or community pages), hide the hidden posts - if !options.show_hidden.unwrap_or_default() && options.creator_id.is_none() { + if !o.show_hidden.unwrap_or_default() && o.creator_id.is_none() { query = query.filter(post_actions::hidden.is_null()); } - if let Some(my_id) = options.local_user.person_id() { + if let Some(my_id) = o.local_user.person_id() { let not_creator_filter = post_aggregates::creator_id.ne(my_id); - if options.liked_only.unwrap_or_default() { + if o.liked_only.unwrap_or_default() { query = query .filter(not_creator_filter) .filter(post_actions::like_score.eq(1)); - } else if options.disliked_only.unwrap_or_default() { + } else if o.disliked_only.unwrap_or_default() { query = query .filter(not_creator_filter) .filter(post_actions::like_score.eq(-1)); } }; - query = options.local_user.visible_communities_only(query); + query = o.local_user.visible_communities_only(query); - if !options.local_user.is_admin() { + if !o.local_user.is_admin() { query = query.filter( community::visibility .ne(CommunityVisibility::Private) @@ -365,9 +356,9 @@ fn queries<'a>() -> Queries< } // Dont filter blocks or missing languages for moderator view type - if options.listing_type.unwrap_or_default() != ListingType::ModeratorView { + if o.listing_type.unwrap_or_default() != ListingType::ModeratorView { // Filter out the rows with missing languages if user is logged in - if options.local_user.is_some() { + if o.local_user.is_some() { query = query.filter(exists( local_user_language::table.filter( post::language_id @@ -383,15 +374,15 @@ fn queries<'a>() -> Queries< query = query.filter(person_actions::blocked.is_null()); } - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query = query.limit(limit).offset(offset); let mut query = PaginatedQueryBuilder::new(query); - let page_after = options.page_after.map(|c| c.0); - let page_before_or_equal = options.page_before_or_equal.map(|c| c.0); + let page_after = o.page_after.map(|c| c.0); + let page_before_or_equal = o.page_before_or_equal.map(|c| c.0); - if options.page_back.unwrap_or_default() { + if o.page_back.unwrap_or_default() { query = query .before(page_after) .after_or_equal(page_before_or_equal) @@ -403,7 +394,7 @@ fn queries<'a>() -> Queries< } // featured posts first - query = if options.community_id.is_none() || options.community_id_just_for_prefetch { + query = if o.community_id.is_none() || o.community_id_just_for_prefetch { query.then_desc(key::featured_local) } else { query.then_desc(key::featured_community) @@ -412,7 +403,7 @@ fn queries<'a>() -> Queries< let time = |interval| post_aggregates::published.gt(now() - interval); // then use the main sort - query = match options.sort.unwrap_or(Hot) { + query = match o.sort.unwrap_or(Hot) { Active => query.then_desc(key::hot_rank_active), Hot => query.then_desc(key::hot_rank), Scaled => query.then_desc(key::scaled_rank), @@ -436,7 +427,7 @@ fn queries<'a>() -> Queries< // use publish as fallback. especially useful for hot rank which reaches zero after some days. // necessary because old posts can be fetched over federation and inserted with high post id - query = match options.sort.unwrap_or(Hot) { + query = match o.sort.unwrap_or(Hot) { // A second time-based sort would not be very useful New | Old | NewComments => query, _ => query.then_desc(key::published), @@ -454,7 +445,7 @@ fn queries<'a>() -> Queries< .text("PostQuery::list") .text_if( "getting upper bound for next query", - options.community_id_just_for_prefetch, + o.community_id_just_for_prefetch, ) .load::(&mut conn) .await diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index 2286b7dc6..346dab49a 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -53,8 +53,7 @@ fn queries<'a>() -> Queries< .await }; - let list = move |mut conn: DbConn<'a>, - (options, recipient_id): (PrivateMessageQuery, PersonId)| async move { + let list = move |mut conn: DbConn<'a>, (o, recipient_id): (PrivateMessageQuery, PersonId)| async move { let mut query = all_joins(private_message::table.into_boxed()) .select(selection) // Dont show replies from blocked users @@ -63,9 +62,9 @@ fn queries<'a>() -> Queries< .filter(instance_actions::blocked.is_null()); // If its unread, I only want the ones to me - if options.unread_only { + if o.unread_only { query = query.filter(private_message::read.eq(false)); - if let Some(i) = options.creator_id { + if let Some(i) = o.creator_id { query = query.filter(private_message::creator_id.eq(i)) } query = query.filter(private_message::recipient_id.eq(recipient_id)); @@ -77,7 +76,7 @@ fn queries<'a>() -> Queries< .eq(recipient_id) .or(private_message::creator_id.eq(recipient_id)), ); - if let Some(i) = options.creator_id { + if let Some(i) = o.creator_id { query = query.filter( private_message::creator_id .eq(i) @@ -86,7 +85,7 @@ fn queries<'a>() -> Queries< } } - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query = query .filter(private_message::deleted.eq(false)) diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 0fa0a5d7e..72329b978 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -53,12 +53,12 @@ fn queries<'a>() -> Queries< query.first(&mut conn).await }; - let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move { + let list = move |mut conn: DbConn<'a>, o: RegistrationApplicationQuery| async move { let mut query = all_joins(registration_application::table.into_boxed()); // If viewing all applications, order by newest, but if viewing unresolved only, show the oldest // first (FIFO) - if options.unread_only { + if o.unread_only { query = query .filter(registration_application::admin_id.is_null()) .order_by(registration_application::published.asc()); @@ -66,11 +66,11 @@ fn queries<'a>() -> Queries< query = query.order_by(registration_application::published.desc()); } - if options.verified_email_only { + if o.verified_email_only { query = query.filter(local_user::email_verified.eq(true)) } - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query = query.limit(limit).offset(offset); diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 6c5442e6a..75f8ed4e2 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -113,24 +113,24 @@ fn queries<'a>() -> Queries< .await }; - let list = move |mut conn: DbConn<'a>, options: CommentReplyQuery| async move { + let list = move |mut conn: DbConn<'a>, o: CommentReplyQuery| async move { // These filters need to be kept in sync with the filters in // CommentReplyView::get_unread_replies() - let mut query = all_joins(comment_reply::table.into_boxed(), options.my_person_id); + let mut query = all_joins(comment_reply::table.into_boxed(), o.my_person_id); - if let Some(recipient_id) = options.recipient_id { + if let Some(recipient_id) = o.recipient_id { query = query.filter(comment_reply::recipient_id.eq(recipient_id)); } - if options.unread_only { + if o.unread_only { query = query.filter(comment_reply::read.eq(false)); } - if !options.show_bot_accounts { + if !o.show_bot_accounts { query = query.filter(not(person::bot_account)); }; - query = match options.sort.unwrap_or(CommentSortType::New) { + query = match o.sort.unwrap_or(CommentSortType::New) { CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), CommentSortType::Controversial => { query.then_order_by(comment_aggregates::controversy_rank.desc()) @@ -143,7 +143,7 @@ fn queries<'a>() -> Queries< // Don't show replies from blocked persons query = query.filter(person_actions::blocked.is_null()); - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query .limit(limit) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 8bcf50ba3..1a8e3c4cd 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -90,17 +90,17 @@ fn queries<'a>() -> Queries< query.first(&mut conn).await }; - let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { + let list = move |mut conn: DbConn<'a>, (o, site): (CommunityQuery<'a>, &'a Site)| async move { use CommunitySortType::*; - let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection); + let mut query = all_joins(community::table.into_boxed(), o.local_user).select(selection); - if let Some(search_term) = options.search_term { + if let Some(search_term) = o.search_term { let searcher = fuzzy_search(&search_term); let name_filter = community::name.ilike(searcher.clone()); let title_filter = community::title.ilike(searcher.clone()); let description_filter = community::description.ilike(searcher.clone()); - query = if options.title_only.unwrap_or_default() { + query = if o.title_only.unwrap_or_default() { query.filter(name_filter.or(title_filter)) } else { query.filter(name_filter.or(title_filter.or(description_filter))) @@ -108,7 +108,7 @@ fn queries<'a>() -> Queries< } // Hide deleted and removed for non-admins or mods - if !options.is_mod_or_admin { + if !o.is_mod_or_admin { query = query.filter(not_removed_or_deleted).filter( community::hidden .eq(false) @@ -116,7 +116,7 @@ fn queries<'a>() -> Queries< ); } - match options.sort.unwrap_or(Hot) { + match o.sort.unwrap_or(Hot) { Hot | Active | Scaled => query = query.order_by(community_aggregates::hot_rank.desc()), NewComments | TopDay | TopTwelveHour | TopSixHour | TopHour => { query = query.order_by(community_aggregates::users_active_day.desc()) @@ -137,7 +137,7 @@ fn queries<'a>() -> Queries< NameDesc => query = query.order_by(lower(community::name).desc()), }; - if let Some(listing_type) = options.listing_type { + if let Some(listing_type) = o.listing_type { query = match listing_type { ListingType::Subscribed => { query.filter(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted))) @@ -151,13 +151,13 @@ fn queries<'a>() -> Queries< // also hidden (based on profile setting) query = query.filter(instance_actions::blocked.is_null()); query = query.filter(community_actions::blocked.is_null()); - if !(options.local_user.show_nsfw(site) || options.show_nsfw) { + if !(o.local_user.show_nsfw(site) || o.show_nsfw) { query = query.filter(community::nsfw.eq(false)); } - query = options.local_user.visible_communities_only(query); + query = o.local_user.visible_communities_only(query); - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query .limit(limit) .offset(offset) diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs index 08be67a82..b3d6235d4 100644 --- a/crates/db_views_actor/src/person_mention_view.rs +++ b/crates/db_views_actor/src/person_mention_view.rs @@ -113,24 +113,24 @@ fn queries<'a>() -> Queries< .await }; - let list = move |mut conn: DbConn<'a>, options: PersonMentionQuery| async move { + let list = move |mut conn: DbConn<'a>, o: PersonMentionQuery| async move { // These filters need to be kept in sync with the filters in // PersonMentionView::get_unread_mentions() - let mut query = all_joins(person_mention::table.into_boxed(), options.my_person_id); + let mut query = all_joins(person_mention::table.into_boxed(), o.my_person_id); - if let Some(recipient_id) = options.recipient_id { + if let Some(recipient_id) = o.recipient_id { query = query.filter(person_mention::recipient_id.eq(recipient_id)); } - if options.unread_only { + if o.unread_only { query = query.filter(person_mention::read.eq(false)); } - if !options.show_bot_accounts { + if !o.show_bot_accounts { query = query.filter(not(person::bot_account)); }; - query = match options.sort.unwrap_or(CommentSortType::Hot) { + query = match o.sort.unwrap_or(CommentSortType::Hot) { CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), CommentSortType::Controversial => { query.then_order_by(comment_aggregates::controversy_rank.desc()) @@ -143,7 +143,7 @@ fn queries<'a>() -> Queries< // Don't show mentions from blocked persons query = query.filter(person_actions::blocked.is_null()); - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query .limit(limit) diff --git a/crates/db_views_actor/src/person_view.rs b/crates/db_views_actor/src/person_view.rs index b90ab7811..bc12e6559 100644 --- a/crates/db_views_actor/src/person_view.rs +++ b/crates/db_views_actor/src/person_view.rs @@ -99,15 +99,15 @@ fn queries<'a>( ) .filter(person::deleted.eq(false)); } - ListMode::Query(options) => { - if let Some(search_term) = options.search_term { + ListMode::Query(o) => { + if let Some(search_term) = o.search_term { let searcher = fuzzy_search(&search_term); query = query .filter(person::name.ilike(searcher.clone())) .or_filter(person::display_name.ilike(searcher)); } - let sort = options.sort.map(post_to_person_sort_type); + let sort = o.sort.map(post_to_person_sort_type); query = match sort.unwrap_or(PersonSortType::CommentScore) { PersonSortType::New => query.order_by(person::published.desc()), PersonSortType::Old => query.order_by(person::published.asc()), @@ -117,10 +117,10 @@ fn queries<'a>( PersonSortType::PostCount => query.order_by(person_aggregates::post_count.desc()), }; - let (limit, offset) = limit_and_offset(options.page, options.limit)?; + let (limit, offset) = limit_and_offset(o.page, o.limit)?; query = query.limit(limit).offset(offset); - if let Some(listing_type) = options.listing_type { + if let Some(listing_type) = o.listing_type { query = match listing_type { // return nothing as its not possible to follow users ListingType::Subscribed => query.limit(0), From 9c473e82989bf3a90f26a36b86be0b6577446833 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:28:26 -0500 Subject: [PATCH 18/23] chore(deps): update rust crate chrono to v0.4.39 (#5295) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 009566cf2..3226ea2e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,9 +797,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", From b91790e29ec01bbf5927478944982188a4148db7 Mon Sep 17 00:00:00 2001 From: dullbananas Date: Thu, 2 Jan 2025 10:35:56 -0700 Subject: [PATCH 19/23] Fix incorrect comment in report_combined_view test (#5297) --- crates/db_views/src/report_combined_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 879634cf0..999681fe0 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -571,7 +571,7 @@ mod tests { }; CommentReport::report(pool, &sara_report_comment_form).await?; - // Timmy creates a private message report + // Timmy creates a private message let pm_form = PrivateMessageInsertForm::new( data.timmy.id, data.sara.id, From c0342292951c237ec5f575f2165758e4f0712e6f Mon Sep 17 00:00:00 2001 From: anhcuky <42137630+anhcuky@users.noreply.github.com> Date: Fri, 3 Jan 2025 00:37:13 +0700 Subject: [PATCH 20/23] reset_password API to always return success (#5284) --- crates/api/src/local_user/reset_password.rs | 30 +++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index e0f63d2e6..20707950c 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -6,23 +6,31 @@ use lemmy_api_common::{ SuccessResponse, }; use lemmy_db_views::structs::{LocalUserView, SiteView}; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::LemmyResult; +use tracing::error; #[tracing::instrument(skip(context))] pub async fn reset_password( data: Json, context: Data, ) -> LemmyResult> { - // Fetch that email let email = data.email.to_lowercase(); - let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) - .await - .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; - - let site_view = SiteView::read_local(&mut context.pool()).await?; - check_email_verified(&local_user_view, &site_view)?; - - // Email the pure token to the user. - send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?; + // For security, errors are not returned. + // https://github.com/LemmyNet/lemmy/issues/5277 + let _ = try_reset_password(&email, &context).await; Ok(Json(SuccessResponse::default())) } + +async fn try_reset_password(email: &str, context: &LemmyContext) -> LemmyResult<()> { + let local_user_view = LocalUserView::find_by_email(&mut context.pool(), email).await?; + let site_view = SiteView::read_local(&mut context.pool()).await?; + + check_email_verified(&local_user_view, &site_view)?; + if let Err(e) = + send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await + { + error!("Failed to send password reset email: {}", e); + } + + Ok(()) +} From c656465e280172b9acd6344c385398ed91cb84b2 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 3 Jan 2025 05:02:20 -0500 Subject: [PATCH 21/23] Fix contradicting filters. (#5281) --- crates/apub/src/api/list_comments.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 3e7a2f4eb..05195b3af 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -9,7 +9,7 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ comment::{GetComments, GetCommentsResponse}, context::LemmyContext, - utils::check_private_instance, + utils::{check_conflicting_like_filters, check_private_instance}, }; use lemmy_db_schema::{ source::{comment::Comment, community::Community}, @@ -19,7 +19,7 @@ use lemmy_db_views::{ comment_view::CommentQuery, structs::{LocalUserView, SiteView}, }; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn list_comments( @@ -50,9 +50,7 @@ pub async fn list_comments( let liked_only = data.liked_only; let disliked_only = data.disliked_only; - if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { - return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); - } + check_conflicting_like_filters(liked_only, disliked_only)?; let page = data.page; let limit = data.limit; From ba779b978f06ee5a8c7f40562a1b41ebaded92f1 Mon Sep 17 00:00:00 2001 From: leoseg <70430884+leoseg@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:08:00 +0100 Subject: [PATCH 22/23] New parameter `read_only` for /api/v3/post/list (#5264) * added option to get only read only posts with unittests * formatted code * added index on (person_id, read) on post actions where read is not null * formatted sql * Update migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql Co-authored-by: dullbananas * Fixxed error in down.sql for migration of index on (person_id,read_only,post_id) on post_actions * Fixxed error in unittests * Update crates/db_views/src/post_view.rs Co-authored-by: dullbananas --------- Co-authored-by: dullbananas --- crates/api_common/src/post.rs | 2 ++ crates/apub/src/api/list_posts.rs | 2 ++ crates/db_views/src/post_view.rs | 35 +++++++++++++++++++ .../down.sql | 2 ++ .../up.sql | 4 +++ 5 files changed, 45 insertions(+) create mode 100644 migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql create mode 100644 migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index db987d63c..543e39495 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -99,6 +99,8 @@ pub struct GetPosts { #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, #[cfg_attr(feature = "full", ts(optional))] + pub read_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 63e737fdd..20e25f9a5 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -42,6 +42,7 @@ pub async fn list_posts( data.community_id }; let saved_only = data.saved_only; + let read_only = data.read_only; let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; @@ -78,6 +79,7 @@ pub async fn list_posts( sort, community_id, saved_only, + read_only, liked_only, disliked_only, page, diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 741df57ff..534b87e1e 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -317,6 +317,12 @@ fn queries<'a>() -> Queries< .filter(post_actions::saved.is_not_null()) .then_order_by(post_actions::saved.desc()); } + + if options.read_only.unwrap_or_default() { + query = query + .filter(post_actions::read.is_not_null()) + .then_order_by(post_actions::read.desc()) + } // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read // setting wont be able to see saved posts. else if !o.show_read.unwrap_or(o.local_user.show_read_posts()) { @@ -510,6 +516,7 @@ pub struct PostQuery<'a> { pub search_term: Option, pub url_only: Option, pub saved_only: Option, + pub read_only: Option, pub liked_only: Option, pub disliked_only: Option, pub title_only: Option, @@ -1236,6 +1243,34 @@ mod tests { Ok(()) } + #[test_context(Data)] + #[tokio::test] + #[serial] + async fn post_listing_read_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); + let pool = &mut pool.into(); + + // Only mark the bot post as read + // The read_only should only show the bot post + let post_read_form = + PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); + PostRead::mark_as_read(pool, &post_read_form).await?; + + // Only read the post marked as read + let read_read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + read_only: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + // This should only include the bot post, not the one you created + assert_eq!(vec![POST_BY_BOT], names(&read_read_post_listing)); + + Ok(()) + } + #[test_context(Data)] #[tokio::test] #[serial] diff --git a/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql new file mode 100644 index 000000000..08750942c --- /dev/null +++ b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql @@ -0,0 +1,2 @@ +DROP INDEX idx_post_actions_on_read_read_not_null; + diff --git a/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql new file mode 100644 index 000000000..03f9e4008 --- /dev/null +++ b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql @@ -0,0 +1,4 @@ +CREATE INDEX idx_post_actions_on_read_read_not_null ON post_actions (person_id, read, post_id) +WHERE + read IS NOT NULL; + From e9d27f28404da8a0a609f13979e43aae09caaaff Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 3 Jan 2025 05:16:25 -0500 Subject: [PATCH 23/23] Don't send out new user email verifies to admins, if already verified. (#5288) - Fixes #5272 --- crates/api/src/local_user/verify_email.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs index 4b6a8c928..813b68364 100644 --- a/crates/api/src/local_user/verify_email.rs +++ b/crates/api/src/local_user/verify_email.rs @@ -19,6 +19,11 @@ pub async fn verify_email( let site_view = SiteView::read_local(&mut context.pool()).await?; let token = data.token.clone(); let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?; + let local_user_id = verification.local_user_id; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + + // Check if their email has already been verified once, before this + let email_already_verified = local_user_view.local_user.email_verified; let form = LocalUserUpdateForm { // necessary in case this is a new signup @@ -27,18 +32,16 @@ pub async fn verify_email( email: Some(Some(verification.email)), ..Default::default() }; - let local_user_id = verification.local_user_id; LocalUser::update(&mut context.pool(), local_user_id, &form).await?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; - // send out notification about registration application to admins if enabled - if site_view.local_site.application_email_admins { - let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?; - + // Send out notification about registration application to admins if enabled, and the user hasn't + // already been verified. + if site_view.local_site.application_email_admins && !email_already_verified { send_new_applicant_email_to_admins( - &local_user.person.name, + &local_user_view.person.name, &mut context.pool(), context.settings(), )