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/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/config/defaults.hjson b/config/defaults.hjson index 21a76d7a5..9e24407cd 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -1,11 +1,7 @@ { # settings related to the postgresql database database: { - # Configure the database by specifying a URI - # - # This is the preferred method to specify database connection details since - # it is the most flexible. - # Connection URI pointing to a postgres instance + # Configure the database by specifying URI pointing to a postgres instance # # This example uses peer authentication to obviate the need for creating, # configuring, and managing passwords. @@ -14,25 +10,7 @@ # PostgreSQL's documentation. # # [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 - uri: "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql" - - # or - - # Configure the database by specifying parts of a URI - # - # Note that specifying the `uri` field should be preferred since it provides - # greater control over how the connection is made. This merely exists for - # backwards-compatibility. - # Username to connect to postgres - user: "string" - # Password to connect to postgres - password: "string" - # Host where postgres is running - host: "string" - # Port where postgres can be accessed - port: 123 - # Name of the postgres database for lemmy - database: "string" + connection: "postgres://lemmy:password@localhost:5432/lemmy" # Maximum number of active sql connections pool_size: 30 } 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/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/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()) 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/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; 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_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 91c91bef1..09ac02978 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -827,6 +827,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, @@ -952,6 +960,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, @@ -1033,6 +1053,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)); @@ -1040,6 +1062,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, @@ -1099,6 +1122,7 @@ diesel::allow_tables_to_appear_in_same_query!( post_actions, post_aggregates, post_report, + post_tag, private_message, private_message_report, received_activity, @@ -1109,5 +1133,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 fce2f7d45..787c6e2f2 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -550,6 +550,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 + } + define_sql_function!(#[sql_name = "coalesce"] fn coalesce_2_nullable(x: diesel::sql_types::Nullable, y: diesel::sql_types::Nullable) -> diesel::sql_types::Nullable); } 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 005fc1903..7d2e11999 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(()) } @@ -1747,15 +1901,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, @@ -1780,7 +1935,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()); @@ -1794,16 +1949,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"); @@ -1838,15 +1992,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, @@ -1858,15 +2012,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); @@ -1914,15 +2068,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( @@ -1942,17 +2096,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( @@ -2004,7 +2161,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, @@ -2031,7 +2188,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, @@ -2041,6 +2198,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/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(()) } diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index d6f11c09b..72d986d2d 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -3,13 +3,11 @@ use anyhow::{anyhow, Context}; use deser_hjson::from_str; use regex::Regex; use std::{env, fs, io::Error, sync::LazyLock}; +use structs::{PictrsConfig, PictrsImageMode, Settings}; use url::Url; -use urlencoding::encode; pub mod structs; -use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings}; - const DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; #[allow(clippy::expect_used)] @@ -51,20 +49,9 @@ impl Settings { pub fn get_database_url(&self) -> String { if let Ok(url) = env::var("LEMMY_DATABASE_URL") { - return url; - } - match &self.database.connection { - DatabaseConnection::Uri { uri } => uri.clone(), - DatabaseConnection::Parts(parts) => { - format!( - "postgres://{}:{}@{}:{}/{}", - encode(&parts.user), - encode(&parts.password), - parts.host, - parts.port, - encode(&parts.database), - ) - } + url + } else { + self.database.connection.clone() } } diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index cbbbcbfe5..1aef9f79b 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -132,64 +132,24 @@ pub enum PictrsImageMode { #[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] #[serde(default)] pub struct DatabaseConfig { - #[serde(flatten, default)] - pub(crate) connection: DatabaseConnection, + /// Configure the database by specifying URI pointing to a postgres instance + /// + /// This example uses peer authentication to obviate the need for creating, + /// configuring, and managing passwords. + /// + /// For an explanation of how to use connection URIs, see [here][0] in + /// PostgreSQL's documentation. + /// + /// [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 + #[default("postgres://lemmy:password@localhost:5432/lemmy")] + #[doku(example = "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql")] + pub(crate) connection: String, /// Maximum number of active sql connections #[default(30)] pub pool_size: usize, } -#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(untagged)] -pub enum DatabaseConnection { - /// Configure the database by specifying a URI - /// - /// This is the preferred method to specify database connection details since - /// it is the most flexible. - Uri { - /// Connection URI pointing to a postgres instance - /// - /// This example uses peer authentication to obviate the need for creating, - /// configuring, and managing passwords. - /// - /// For an explanation of how to use connection URIs, see [here][0] in - /// PostgreSQL's documentation. - /// - /// [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6 - #[doku(example = "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql")] - uri: String, - }, - - /// Configure the database by specifying parts of a URI - /// - /// Note that specifying the `uri` field should be preferred since it provides - /// greater control over how the connection is made. This merely exists for - /// backwards-compatibility. - #[default] - Parts(DatabaseConnectionParts), -} - -#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] -#[serde(default)] -pub struct DatabaseConnectionParts { - /// Username to connect to postgres - #[default("lemmy")] - pub(super) user: String, - /// Password to connect to postgres - #[default("password")] - pub(super) password: String, - #[default("localhost")] - /// Host where postgres is running - pub(super) host: String, - /// Port where postgres can be accessed - #[default(5432)] - pub(super) port: i32, - /// Name of the postgres database for lemmy - #[default("lemmy")] - pub(super) database: String, -} - #[derive(Debug, Deserialize, Serialize, Clone, Document, SmartDefault)] #[serde(deny_unknown_fields)] pub struct EmailConfig { diff --git a/docker/federation/lemmy_alpha.hjson b/docker/federation/lemmy_alpha.hjson index 84ef1a16e..a3bf2bb21 100644 --- a/docker/federation/lemmy_alpha.hjson +++ b/docker/federation/lemmy_alpha.hjson @@ -8,7 +8,7 @@ site_name: lemmy-alpha } database: { - host: postgres_alpha + connection: "postgres://lemmy:password@postgres_alpha:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_beta.hjson b/docker/federation/lemmy_beta.hjson index 1b4508a43..c026b2f71 100644 --- a/docker/federation/lemmy_beta.hjson +++ b/docker/federation/lemmy_beta.hjson @@ -8,7 +8,7 @@ site_name: lemmy-beta } database: { - host: postgres_beta + connection: "postgres://lemmy:password@postgres_beta:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_delta.hjson b/docker/federation/lemmy_delta.hjson index d05e4121f..acfddc304 100644 --- a/docker/federation/lemmy_delta.hjson +++ b/docker/federation/lemmy_delta.hjson @@ -8,6 +8,6 @@ site_name: lemmy-delta } database: { - host: postgres_delta + connection: "postgres://lemmy:password@postgres_delta:5432/lemmy" } } diff --git a/docker/federation/lemmy_epsilon.hjson b/docker/federation/lemmy_epsilon.hjson index c24baa9f8..a607353a6 100644 --- a/docker/federation/lemmy_epsilon.hjson +++ b/docker/federation/lemmy_epsilon.hjson @@ -8,7 +8,7 @@ site_name: lemmy-epsilon } database: { - host: postgres_epsilon + connection: "postgres://lemmy:password@postgres_epsilon:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/federation/lemmy_gamma.hjson b/docker/federation/lemmy_gamma.hjson index d7e5b6065..7db9a5065 100644 --- a/docker/federation/lemmy_gamma.hjson +++ b/docker/federation/lemmy_gamma.hjson @@ -8,7 +8,7 @@ site_name: lemmy-gamma } database: { - host: postgres_gamma + connection: "postgres://lemmy:password@postgres_gamma:5432/lemmy" } pictrs: { api_key: "my-pictrs-key" diff --git a/docker/lemmy.hjson b/docker/lemmy.hjson index 83adf9c0c..e28a49b6d 100644 --- a/docker/lemmy.hjson +++ b/docker/lemmy.hjson @@ -11,7 +11,7 @@ site_name: "lemmy-dev" } database: { - host: postgres + connection: "postgres://lemmy:password@postgres:5432/lemmy" } hostname: "localhost" 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) +); + 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