]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
Reduce amount of columns selected (#3755)
[lemmy.git] / crates / db_views / src / post_view.rs
1 use crate::structs::{LocalUserView, PostView};
2 use diesel::{
3   debug_query,
4   dsl::{now, IntervalDsl},
5   pg::Pg,
6   result::Error,
7   sql_function,
8   sql_types,
9   BoolExpressionMethods,
10   ExpressionMethods,
11   JoinOnDsl,
12   NullableExpressionMethods,
13   PgTextExpressionMethods,
14   QueryDsl,
15 };
16 use diesel_async::RunQueryDsl;
17 use lemmy_db_schema::{
18   aggregates::structs::PostAggregates,
19   newtypes::{CommunityId, LocalUserId, PersonId, PostId},
20   schema::{
21     community,
22     community_block,
23     community_follower,
24     community_moderator,
25     community_person_ban,
26     local_user_language,
27     person,
28     person_block,
29     person_post_aggregates,
30     post,
31     post_aggregates,
32     post_like,
33     post_read,
34     post_saved,
35   },
36   source::{
37     community::{Community, CommunityFollower},
38     person::Person,
39     post::Post,
40   },
41   traits::JoinView,
42   utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
43   ListingType,
44   SortType,
45   SubscribedType,
46 };
47 use tracing::debug;
48
49 type PostViewTuple = (
50   Post,
51   Person,
52   Community,
53   bool,
54   PostAggregates,
55   SubscribedType,
56   bool,
57   bool,
58   bool,
59   Option<i16>,
60   i64,
61 );
62
63 sql_function!(fn coalesce(x: sql_types::Nullable<sql_types::BigInt>, y: sql_types::BigInt) -> sql_types::BigInt);
64
65 fn queries<'a>() -> Queries<
66   impl ReadFn<'a, PostView, (PostId, Option<PersonId>, Option<bool>)>,
67   impl ListFn<'a, PostView, PostQuery<'a>>,
68 > {
69   let all_joins = |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| {
70     // The left join below will return None in this case
71     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
72
73     query
74       .inner_join(person::table)
75       .inner_join(community::table)
76       .left_join(
77         community_person_ban::table.on(
78           post_aggregates::community_id
79             .eq(community_person_ban::community_id)
80             .and(community_person_ban::person_id.eq(post_aggregates::creator_id)),
81         ),
82       )
83       .inner_join(post::table)
84       .left_join(
85         community_follower::table.on(
86           post_aggregates::community_id
87             .eq(community_follower::community_id)
88             .and(community_follower::person_id.eq(person_id_join)),
89         ),
90       )
91       .left_join(
92         community_moderator::table.on(
93           post::community_id
94             .eq(community_moderator::community_id)
95             .and(community_moderator::person_id.eq(person_id_join)),
96         ),
97       )
98       .left_join(
99         post_saved::table.on(
100           post_aggregates::post_id
101             .eq(post_saved::post_id)
102             .and(post_saved::person_id.eq(person_id_join)),
103         ),
104       )
105       .left_join(
106         post_read::table.on(
107           post_aggregates::post_id
108             .eq(post_read::post_id)
109             .and(post_read::person_id.eq(person_id_join)),
110         ),
111       )
112       .left_join(
113         person_block::table.on(
114           post_aggregates::creator_id
115             .eq(person_block::target_id)
116             .and(person_block::person_id.eq(person_id_join)),
117         ),
118       )
119       .left_join(
120         post_like::table.on(
121           post_aggregates::post_id
122             .eq(post_like::post_id)
123             .and(post_like::person_id.eq(person_id_join)),
124         ),
125       )
126       .left_join(
127         person_post_aggregates::table.on(
128           post_aggregates::post_id
129             .eq(person_post_aggregates::post_id)
130             .and(person_post_aggregates::person_id.eq(person_id_join)),
131         ),
132       )
133   };
134
135   let selection = (
136     post::all_columns,
137     person::all_columns,
138     community::all_columns,
139     community_person_ban::id.nullable().is_not_null(),
140     post_aggregates::all_columns,
141     CommunityFollower::select_subscribed_type(),
142     post_saved::id.nullable().is_not_null(),
143     post_read::id.nullable().is_not_null(),
144     person_block::id.nullable().is_not_null(),
145     post_like::score.nullable(),
146     coalesce(
147       post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(),
148       post_aggregates::comments,
149     ),
150   );
151
152   let read = move |mut conn: DbConn<'a>,
153                    (post_id, my_person_id, is_mod_or_admin): (
154     PostId,
155     Option<PersonId>,
156     Option<bool>,
157   )| async move {
158     // The left join below will return None in this case
159     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
160
161     let mut query = all_joins(
162       post_aggregates::table
163         .filter(post_aggregates::post_id.eq(post_id))
164         .into_boxed(),
165       my_person_id,
166     )
167     .select(selection);
168
169     // Hide deleted and removed for non-admins or mods
170     if !is_mod_or_admin.unwrap_or(false) {
171       query = query
172         .filter(community::removed.eq(false))
173         .filter(post::removed.eq(false))
174         // users can see their own deleted posts
175         .filter(
176           community::deleted
177             .eq(false)
178             .or(post::creator_id.eq(person_id_join)),
179         )
180         .filter(
181           post::deleted
182             .eq(false)
183             .or(post::creator_id.eq(person_id_join)),
184         );
185     }
186
187     query.first::<PostViewTuple>(&mut conn).await
188   };
189
190   let list = move |mut conn: DbConn<'a>, options: PostQuery<'a>| async move {
191     let person_id = options.local_user.map(|l| l.person.id);
192     let local_user_id = options.local_user.map(|l| l.local_user.id);
193
194     // The left join below will return None in this case
195     let person_id_join = person_id.unwrap_or(PersonId(-1));
196     let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
197
198     let mut query = all_joins(post_aggregates::table.into_boxed(), person_id)
199       .left_join(
200         community_block::table.on(
201           post_aggregates::community_id
202             .eq(community_block::community_id)
203             .and(community_block::person_id.eq(person_id_join)),
204         ),
205       )
206       .left_join(
207         local_user_language::table.on(
208           post::language_id
209             .eq(local_user_language::language_id)
210             .and(local_user_language::local_user_id.eq(local_user_id_join)),
211         ),
212       )
213       .select(selection);
214
215     let is_creator = options.creator_id == options.local_user.map(|l| l.person.id);
216     // only show deleted posts to creator
217     if is_creator {
218       query = query
219         .filter(community::deleted.eq(false))
220         .filter(post::deleted.eq(false));
221     }
222
223     let is_admin = options.local_user.map(|l| l.person.admin).unwrap_or(false);
224     // only show removed posts to admin when viewing user profile
225     if !(options.is_profile_view && is_admin) {
226       query = query
227         .filter(community::removed.eq(false))
228         .filter(post::removed.eq(false));
229     }
230
231     if options.community_id.is_none() {
232       query = query.then_order_by(post_aggregates::featured_local.desc());
233     } else if let Some(community_id) = options.community_id {
234       query = query
235         .filter(post_aggregates::community_id.eq(community_id))
236         .then_order_by(post_aggregates::featured_community.desc());
237     }
238
239     if let Some(creator_id) = options.creator_id {
240       query = query.filter(post_aggregates::creator_id.eq(creator_id));
241     }
242
243     if let Some(listing_type) = options.listing_type {
244       match listing_type {
245         ListingType::Subscribed => query = query.filter(community_follower::pending.is_not_null()),
246         ListingType::Local => {
247           query = query.filter(community::local.eq(true)).filter(
248             community::hidden
249               .eq(false)
250               .or(community_follower::person_id.eq(person_id_join)),
251           );
252         }
253         ListingType::All => {
254           query = query.filter(
255             community::hidden
256               .eq(false)
257               .or(community_follower::person_id.eq(person_id_join)),
258           )
259         }
260       }
261     }
262
263     if let Some(url_search) = options.url_search {
264       query = query.filter(post::url.eq(url_search));
265     }
266
267     if let Some(search_term) = options.search_term {
268       let searcher = fuzzy_search(&search_term);
269       query = query.filter(
270         post::name
271           .ilike(searcher.clone())
272           .or(post::body.ilike(searcher)),
273       );
274     }
275
276     if !options
277       .local_user
278       .map(|l| l.local_user.show_nsfw)
279       .unwrap_or(false)
280     {
281       query = query
282         .filter(post::nsfw.eq(false))
283         .filter(community::nsfw.eq(false));
284     };
285
286     if !options
287       .local_user
288       .map(|l| l.local_user.show_bot_accounts)
289       .unwrap_or(true)
290     {
291       query = query.filter(person::bot_account.eq(false));
292     };
293
294     if options.saved_only.unwrap_or(false) {
295       query = query.filter(post_saved::id.is_not_null());
296     }
297
298     if options.moderator_view.unwrap_or(false) {
299       query = query.filter(community_moderator::person_id.is_not_null());
300     }
301     // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read
302     // setting wont be able to see saved posts.
303     else if !options
304       .local_user
305       .map(|l| l.local_user.show_read_posts)
306       .unwrap_or(true)
307     {
308       // Do not hide read posts when it is a user profile view
309       if !options.is_profile_view {
310         query = query.filter(post_read::post_id.is_null());
311       }
312     }
313
314     if options.liked_only.unwrap_or_default() {
315       query = query.filter(post_like::score.eq(1));
316     } else if options.disliked_only.unwrap_or_default() {
317       query = query.filter(post_like::score.eq(-1));
318     }
319
320     if options.local_user.is_some() {
321       // Filter out the rows with missing languages
322       query = query.filter(local_user_language::language_id.is_not_null());
323
324       // Don't show blocked communities or persons
325       query = query.filter(community_block::person_id.is_null());
326       if !options.moderator_view.unwrap_or(false) {
327         query = query.filter(person_block::person_id.is_null());
328       }
329     }
330
331     query = match options.sort.unwrap_or(SortType::Hot) {
332       SortType::Active => query
333         .then_order_by(post_aggregates::hot_rank_active.desc())
334         .then_order_by(post_aggregates::published.desc()),
335       SortType::Hot => query
336         .then_order_by(post_aggregates::hot_rank.desc())
337         .then_order_by(post_aggregates::published.desc()),
338       SortType::Controversial => query.then_order_by(post_aggregates::controversy_rank.desc()),
339       SortType::New => query.then_order_by(post_aggregates::published.desc()),
340       SortType::Old => query.then_order_by(post_aggregates::published.asc()),
341       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
342       SortType::MostComments => query
343         .then_order_by(post_aggregates::comments.desc())
344         .then_order_by(post_aggregates::published.desc()),
345       SortType::TopAll => query
346         .then_order_by(post_aggregates::score.desc())
347         .then_order_by(post_aggregates::published.desc()),
348       SortType::TopYear => query
349         .filter(post_aggregates::published.gt(now - 1.years()))
350         .then_order_by(post_aggregates::score.desc())
351         .then_order_by(post_aggregates::published.desc()),
352       SortType::TopMonth => query
353         .filter(post_aggregates::published.gt(now - 1.months()))
354         .then_order_by(post_aggregates::score.desc())
355         .then_order_by(post_aggregates::published.desc()),
356       SortType::TopWeek => query
357         .filter(post_aggregates::published.gt(now - 1.weeks()))
358         .then_order_by(post_aggregates::score.desc())
359         .then_order_by(post_aggregates::published.desc()),
360       SortType::TopDay => query
361         .filter(post_aggregates::published.gt(now - 1.days()))
362         .then_order_by(post_aggregates::score.desc())
363         .then_order_by(post_aggregates::published.desc()),
364       SortType::TopHour => query
365         .filter(post_aggregates::published.gt(now - 1.hours()))
366         .then_order_by(post_aggregates::score.desc())
367         .then_order_by(post_aggregates::published.desc()),
368       SortType::TopSixHour => query
369         .filter(post_aggregates::published.gt(now - 6.hours()))
370         .then_order_by(post_aggregates::score.desc())
371         .then_order_by(post_aggregates::published.desc()),
372       SortType::TopTwelveHour => query
373         .filter(post_aggregates::published.gt(now - 12.hours()))
374         .then_order_by(post_aggregates::score.desc())
375         .then_order_by(post_aggregates::published.desc()),
376       SortType::TopThreeMonths => query
377         .filter(post_aggregates::published.gt(now - 3.months()))
378         .then_order_by(post_aggregates::score.desc())
379         .then_order_by(post_aggregates::published.desc()),
380       SortType::TopSixMonths => query
381         .filter(post_aggregates::published.gt(now - 6.months()))
382         .then_order_by(post_aggregates::score.desc())
383         .then_order_by(post_aggregates::published.desc()),
384       SortType::TopNineMonths => query
385         .filter(post_aggregates::published.gt(now - 9.months()))
386         .then_order_by(post_aggregates::score.desc())
387         .then_order_by(post_aggregates::published.desc()),
388     };
389
390     let (limit, offset) = limit_and_offset(options.page, options.limit)?;
391
392     query = query.limit(limit).offset(offset);
393
394     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
395
396     query.load::<PostViewTuple>(&mut conn).await
397   };
398
399   Queries::new(read, list)
400 }
401
402 impl PostView {
403   pub async fn read(
404     pool: &mut DbPool<'_>,
405     post_id: PostId,
406     my_person_id: Option<PersonId>,
407     is_mod_or_admin: Option<bool>,
408   ) -> Result<Self, Error> {
409     let mut res = queries()
410       .read(pool, (post_id, my_person_id, is_mod_or_admin))
411       .await?;
412
413     // If a person is given, then my_vote, if None, should be 0, not null
414     // Necessary to differentiate between other person's votes
415     if my_person_id.is_some() && res.my_vote.is_none() {
416       res.my_vote = Some(0)
417     };
418
419     Ok(res)
420   }
421 }
422
423 #[derive(Default)]
424 pub struct PostQuery<'a> {
425   pub listing_type: Option<ListingType>,
426   pub sort: Option<SortType>,
427   pub creator_id: Option<PersonId>,
428   pub community_id: Option<CommunityId>,
429   pub local_user: Option<&'a LocalUserView>,
430   pub search_term: Option<String>,
431   pub url_search: Option<String>,
432   pub saved_only: Option<bool>,
433   pub liked_only: Option<bool>,
434   pub disliked_only: Option<bool>,
435   pub moderator_view: Option<bool>,
436   pub is_profile_view: bool,
437   pub page: Option<i64>,
438   pub limit: Option<i64>,
439 }
440
441 impl<'a> PostQuery<'a> {
442   pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PostView>, Error> {
443     queries().list(pool, self).await
444   }
445 }
446
447 impl JoinView for PostView {
448   type JoinTuple = PostViewTuple;
449   fn from_tuple(a: Self::JoinTuple) -> Self {
450     Self {
451       post: a.0,
452       creator: a.1,
453       community: a.2,
454       creator_banned_from_community: a.3,
455       counts: a.4,
456       subscribed: a.5,
457       saved: a.6,
458       read: a.7,
459       creator_blocked: a.8,
460       my_vote: a.9,
461       unread_comments: a.10,
462     }
463   }
464 }
465
466 #[cfg(test)]
467 mod tests {
468   #![allow(clippy::unwrap_used)]
469   #![allow(clippy::indexing_slicing)]
470
471   use crate::{
472     post_view::{PostQuery, PostView},
473     structs::LocalUserView,
474   };
475   use lemmy_db_schema::{
476     aggregates::structs::PostAggregates,
477     impls::actor_language::UNDETERMINED_ID,
478     newtypes::LanguageId,
479     source::{
480       actor_language::LocalUserLanguage,
481       community::{Community, CommunityInsertForm},
482       community_block::{CommunityBlock, CommunityBlockForm},
483       instance::Instance,
484       language::Language,
485       local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
486       person::{Person, PersonInsertForm},
487       person_block::{PersonBlock, PersonBlockForm},
488       post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
489     },
490     traits::{Blockable, Crud, Likeable},
491     utils::{build_db_pool_for_tests, DbPool},
492     SortType,
493     SubscribedType,
494   };
495   use serial_test::serial;
496
497   struct Data {
498     inserted_instance: Instance,
499     local_user_view: LocalUserView,
500     inserted_blocked_person: Person,
501     inserted_bot: Person,
502     inserted_community: Community,
503     inserted_post: Post,
504   }
505
506   async fn init_data(pool: &mut DbPool<'_>) -> Data {
507     let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
508       .await
509       .unwrap();
510
511     let person_name = "tegan".to_string();
512
513     let new_person = PersonInsertForm::builder()
514       .name(person_name.clone())
515       .public_key("pubkey".to_string())
516       .instance_id(inserted_instance.id)
517       .build();
518
519     let inserted_person = Person::create(pool, &new_person).await.unwrap();
520
521     let local_user_form = LocalUserInsertForm::builder()
522       .person_id(inserted_person.id)
523       .password_encrypted(String::new())
524       .build();
525     let inserted_local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
526
527     let new_bot = PersonInsertForm::builder()
528       .name("mybot".to_string())
529       .bot_account(Some(true))
530       .public_key("pubkey".to_string())
531       .instance_id(inserted_instance.id)
532       .build();
533
534     let inserted_bot = Person::create(pool, &new_bot).await.unwrap();
535
536     let new_community = CommunityInsertForm::builder()
537       .name("test_community_3".to_string())
538       .title("nada".to_owned())
539       .public_key("pubkey".to_string())
540       .instance_id(inserted_instance.id)
541       .build();
542
543     let inserted_community = Community::create(pool, &new_community).await.unwrap();
544
545     // Test a person block, make sure the post query doesn't include their post
546     let blocked_person = PersonInsertForm::builder()
547       .name(person_name)
548       .public_key("pubkey".to_string())
549       .instance_id(inserted_instance.id)
550       .build();
551
552     let inserted_blocked_person = Person::create(pool, &blocked_person).await.unwrap();
553
554     let post_from_blocked_person = PostInsertForm::builder()
555       .name("blocked_person_post".to_string())
556       .creator_id(inserted_blocked_person.id)
557       .community_id(inserted_community.id)
558       .language_id(Some(LanguageId(1)))
559       .build();
560
561     Post::create(pool, &post_from_blocked_person).await.unwrap();
562
563     // block that person
564     let person_block = PersonBlockForm {
565       person_id: inserted_person.id,
566       target_id: inserted_blocked_person.id,
567     };
568
569     PersonBlock::block(pool, &person_block).await.unwrap();
570
571     // A sample post
572     let new_post = PostInsertForm::builder()
573       .name("test post 3".to_string())
574       .creator_id(inserted_person.id)
575       .community_id(inserted_community.id)
576       .language_id(Some(LanguageId(47)))
577       .build();
578
579     let inserted_post = Post::create(pool, &new_post).await.unwrap();
580
581     let new_bot_post = PostInsertForm::builder()
582       .name("test bot post".to_string())
583       .creator_id(inserted_bot.id)
584       .community_id(inserted_community.id)
585       .build();
586
587     let _inserted_bot_post = Post::create(pool, &new_bot_post).await.unwrap();
588     let local_user_view = LocalUserView {
589       local_user: inserted_local_user,
590       person: inserted_person,
591       counts: Default::default(),
592     };
593
594     Data {
595       inserted_instance,
596       local_user_view,
597       inserted_blocked_person,
598       inserted_bot,
599       inserted_community,
600       inserted_post,
601     }
602   }
603
604   #[tokio::test]
605   #[serial]
606   async fn post_listing_with_person() {
607     let pool = &build_db_pool_for_tests().await;
608     let pool = &mut pool.into();
609     let mut data = init_data(pool).await;
610
611     let local_user_form = LocalUserUpdateForm::builder()
612       .show_bot_accounts(Some(false))
613       .build();
614     let inserted_local_user =
615       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
616         .await
617         .unwrap();
618     data.local_user_view.local_user = inserted_local_user;
619
620     let read_post_listing = PostQuery {
621       sort: (Some(SortType::New)),
622       community_id: (Some(data.inserted_community.id)),
623       local_user: (Some(&data.local_user_view)),
624       ..Default::default()
625     }
626     .list(pool)
627     .await
628     .unwrap();
629
630     let post_listing_single_with_person = PostView::read(
631       pool,
632       data.inserted_post.id,
633       Some(data.local_user_view.person.id),
634       None,
635     )
636     .await
637     .unwrap();
638
639     let mut expected_post_listing_with_user = expected_post_view(&data, pool).await;
640
641     // Should be only one person, IE the bot post, and blocked should be missing
642     assert_eq!(1, read_post_listing.len());
643
644     assert_eq!(expected_post_listing_with_user, read_post_listing[0]);
645     expected_post_listing_with_user.my_vote = Some(0);
646     assert_eq!(
647       expected_post_listing_with_user,
648       post_listing_single_with_person
649     );
650
651     let local_user_form = LocalUserUpdateForm::builder()
652       .show_bot_accounts(Some(true))
653       .build();
654     let inserted_local_user =
655       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
656         .await
657         .unwrap();
658     data.local_user_view.local_user = inserted_local_user;
659
660     let post_listings_with_bots = PostQuery {
661       sort: (Some(SortType::New)),
662       community_id: (Some(data.inserted_community.id)),
663       local_user: (Some(&data.local_user_view)),
664       ..Default::default()
665     }
666     .list(pool)
667     .await
668     .unwrap();
669     // should include bot post which has "undetermined" language
670     assert_eq!(2, post_listings_with_bots.len());
671
672     cleanup(data, pool).await;
673   }
674
675   #[tokio::test]
676   #[serial]
677   async fn post_listing_no_person() {
678     let pool = &build_db_pool_for_tests().await;
679     let pool = &mut pool.into();
680     let data = init_data(pool).await;
681
682     let read_post_listing_multiple_no_person = PostQuery {
683       sort: (Some(SortType::New)),
684       community_id: (Some(data.inserted_community.id)),
685       ..Default::default()
686     }
687     .list(pool)
688     .await
689     .unwrap();
690
691     let read_post_listing_single_no_person =
692       PostView::read(pool, data.inserted_post.id, None, None)
693         .await
694         .unwrap();
695
696     let expected_post_listing_no_person = expected_post_view(&data, pool).await;
697
698     // Should be 2 posts, with the bot post, and the blocked
699     assert_eq!(3, read_post_listing_multiple_no_person.len());
700
701     assert_eq!(
702       expected_post_listing_no_person,
703       read_post_listing_multiple_no_person[1]
704     );
705     assert_eq!(
706       expected_post_listing_no_person,
707       read_post_listing_single_no_person
708     );
709
710     cleanup(data, pool).await;
711   }
712
713   #[tokio::test]
714   #[serial]
715   async fn post_listing_block_community() {
716     let pool = &build_db_pool_for_tests().await;
717     let pool = &mut pool.into();
718     let data = init_data(pool).await;
719
720     let community_block = CommunityBlockForm {
721       person_id: data.local_user_view.person.id,
722       community_id: data.inserted_community.id,
723     };
724     CommunityBlock::block(pool, &community_block).await.unwrap();
725
726     let read_post_listings_with_person_after_block = PostQuery {
727       sort: (Some(SortType::New)),
728       community_id: (Some(data.inserted_community.id)),
729       local_user: (Some(&data.local_user_view)),
730       ..Default::default()
731     }
732     .list(pool)
733     .await
734     .unwrap();
735     // Should be 0 posts after the community block
736     assert_eq!(0, read_post_listings_with_person_after_block.len());
737
738     CommunityBlock::unblock(pool, &community_block)
739       .await
740       .unwrap();
741     cleanup(data, pool).await;
742   }
743
744   #[tokio::test]
745   #[serial]
746   async fn post_listing_like() {
747     let pool = &build_db_pool_for_tests().await;
748     let pool = &mut pool.into();
749     let mut data = init_data(pool).await;
750
751     let post_like_form = PostLikeForm {
752       post_id: data.inserted_post.id,
753       person_id: data.local_user_view.person.id,
754       score: 1,
755     };
756
757     let inserted_post_like = PostLike::like(pool, &post_like_form).await.unwrap();
758
759     let expected_post_like = PostLike {
760       id: inserted_post_like.id,
761       post_id: data.inserted_post.id,
762       person_id: data.local_user_view.person.id,
763       published: inserted_post_like.published,
764       score: 1,
765     };
766     assert_eq!(expected_post_like, inserted_post_like);
767
768     let post_listing_single_with_person = PostView::read(
769       pool,
770       data.inserted_post.id,
771       Some(data.local_user_view.person.id),
772       None,
773     )
774     .await
775     .unwrap();
776
777     let mut expected_post_with_upvote = expected_post_view(&data, pool).await;
778     expected_post_with_upvote.my_vote = Some(1);
779     expected_post_with_upvote.counts.score = 1;
780     expected_post_with_upvote.counts.upvotes = 1;
781     assert_eq!(expected_post_with_upvote, post_listing_single_with_person);
782
783     let local_user_form = LocalUserUpdateForm::builder()
784       .show_bot_accounts(Some(false))
785       .build();
786     let inserted_local_user =
787       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
788         .await
789         .unwrap();
790     data.local_user_view.local_user = inserted_local_user;
791
792     let read_post_listing = PostQuery {
793       sort: (Some(SortType::New)),
794       community_id: (Some(data.inserted_community.id)),
795       local_user: (Some(&data.local_user_view)),
796       ..Default::default()
797     }
798     .list(pool)
799     .await
800     .unwrap();
801     assert_eq!(1, read_post_listing.len());
802
803     assert_eq!(expected_post_with_upvote, read_post_listing[0]);
804
805     let read_liked_post_listing = PostQuery {
806       community_id: (Some(data.inserted_community.id)),
807       local_user: (Some(&data.local_user_view)),
808       liked_only: (Some(true)),
809       ..Default::default()
810     }
811     .list(pool)
812     .await
813     .unwrap();
814     assert_eq!(read_post_listing, read_liked_post_listing);
815
816     let read_disliked_post_listing = PostQuery {
817       community_id: (Some(data.inserted_community.id)),
818       local_user: (Some(&data.local_user_view)),
819       disliked_only: (Some(true)),
820       ..Default::default()
821     }
822     .list(pool)
823     .await
824     .unwrap();
825     assert!(read_disliked_post_listing.is_empty());
826
827     let like_removed =
828       PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id)
829         .await
830         .unwrap();
831     assert_eq!(1, like_removed);
832     cleanup(data, pool).await;
833   }
834
835   #[tokio::test]
836   #[serial]
837   async fn post_listing_person_language() {
838     let pool = &build_db_pool_for_tests().await;
839     let pool = &mut pool.into();
840     let data = init_data(pool).await;
841
842     let spanish_id = Language::read_id_from_code(pool, Some("es"))
843       .await
844       .unwrap()
845       .unwrap();
846     let post_spanish = PostInsertForm::builder()
847       .name("asffgdsc".to_string())
848       .creator_id(data.local_user_view.person.id)
849       .community_id(data.inserted_community.id)
850       .language_id(Some(spanish_id))
851       .build();
852
853     Post::create(pool, &post_spanish).await.unwrap();
854
855     let post_listings_all = PostQuery {
856       sort: (Some(SortType::New)),
857       local_user: (Some(&data.local_user_view)),
858       ..Default::default()
859     }
860     .list(pool)
861     .await
862     .unwrap();
863
864     // no language filters specified, all posts should be returned
865     assert_eq!(3, post_listings_all.len());
866
867     let french_id = Language::read_id_from_code(pool, Some("fr"))
868       .await
869       .unwrap()
870       .unwrap();
871     LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id)
872       .await
873       .unwrap();
874
875     let post_listing_french = PostQuery {
876       sort: (Some(SortType::New)),
877       local_user: (Some(&data.local_user_view)),
878       ..Default::default()
879     }
880     .list(pool)
881     .await
882     .unwrap();
883
884     // only one post in french and one undetermined should be returned
885     assert_eq!(2, post_listing_french.len());
886     assert!(post_listing_french
887       .iter()
888       .any(|p| p.post.language_id == french_id));
889
890     LocalUserLanguage::update(
891       pool,
892       vec![french_id, UNDETERMINED_ID],
893       data.local_user_view.local_user.id,
894     )
895     .await
896     .unwrap();
897     let post_listings_french_und = PostQuery {
898       sort: (Some(SortType::New)),
899       local_user: (Some(&data.local_user_view)),
900       ..Default::default()
901     }
902     .list(pool)
903     .await
904     .unwrap();
905
906     // french post and undetermined language post should be returned
907     assert_eq!(2, post_listings_french_und.len());
908     assert_eq!(
909       UNDETERMINED_ID,
910       post_listings_french_und[0].post.language_id
911     );
912     assert_eq!(french_id, post_listings_french_und[1].post.language_id);
913
914     cleanup(data, pool).await;
915   }
916
917   #[tokio::test]
918   #[serial]
919   async fn post_listings_removed() {
920     let pool = &build_db_pool_for_tests().await;
921     let pool = &mut pool.into();
922     let mut data = init_data(pool).await;
923
924     // Remove the post
925     Post::update(
926       pool,
927       data.inserted_post.id,
928       &PostUpdateForm::builder().removed(Some(true)).build(),
929     )
930     .await
931     .unwrap();
932
933     // Make sure you don't see the removed post in the results
934     let post_listings_no_admin = PostQuery {
935       sort: Some(SortType::New),
936       local_user: Some(&data.local_user_view),
937       ..Default::default()
938     }
939     .list(pool)
940     .await
941     .unwrap();
942     assert_eq!(1, post_listings_no_admin.len());
943
944     // Removed post is shown to admins on profile page
945     data.local_user_view.person.admin = true;
946     let post_listings_is_admin = PostQuery {
947       sort: Some(SortType::New),
948       local_user: Some(&data.local_user_view),
949       is_profile_view: true,
950       ..Default::default()
951     }
952     .list(pool)
953     .await
954     .unwrap();
955     assert_eq!(2, post_listings_is_admin.len());
956
957     cleanup(data, pool).await;
958   }
959
960   #[tokio::test]
961   #[serial]
962   async fn post_listings_deleted() {
963     let pool = &build_db_pool_for_tests().await;
964     let pool = &mut pool.into();
965     let data = init_data(pool).await;
966
967     // Delete the post
968     Post::update(
969       pool,
970       data.inserted_post.id,
971       &PostUpdateForm::builder().deleted(Some(true)).build(),
972     )
973     .await
974     .unwrap();
975
976     // Make sure you don't see the deleted post in the results
977     let post_listings_no_creator = PostQuery {
978       sort: Some(SortType::New),
979       ..Default::default()
980     }
981     .list(pool)
982     .await
983     .unwrap();
984     let not_contains_deleted = post_listings_no_creator
985       .iter()
986       .map(|p| p.post.id)
987       .all(|p| p != data.inserted_post.id);
988     assert!(not_contains_deleted);
989
990     // Deleted post is shown to creator
991     let post_listings_is_creator = PostQuery {
992       sort: Some(SortType::New),
993       local_user: Some(&data.local_user_view),
994       ..Default::default()
995     }
996     .list(pool)
997     .await
998     .unwrap();
999     let contains_deleted = post_listings_is_creator
1000       .iter()
1001       .map(|p| p.post.id)
1002       .any(|p| p == data.inserted_post.id);
1003     assert!(contains_deleted);
1004
1005     cleanup(data, pool).await;
1006   }
1007
1008   async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
1009     let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
1010     Community::delete(pool, data.inserted_community.id)
1011       .await
1012       .unwrap();
1013     Person::delete(pool, data.local_user_view.person.id)
1014       .await
1015       .unwrap();
1016     Person::delete(pool, data.inserted_bot.id).await.unwrap();
1017     Person::delete(pool, data.inserted_blocked_person.id)
1018       .await
1019       .unwrap();
1020     Instance::delete(pool, data.inserted_instance.id)
1021       .await
1022       .unwrap();
1023     assert_eq!(1, num_deleted);
1024   }
1025
1026   async fn expected_post_view(data: &Data, pool: &mut DbPool<'_>) -> PostView {
1027     let (inserted_person, inserted_community, inserted_post) = (
1028       &data.local_user_view.person,
1029       &data.inserted_community,
1030       &data.inserted_post,
1031     );
1032     let agg = PostAggregates::read(pool, inserted_post.id).await.unwrap();
1033
1034     PostView {
1035       post: Post {
1036         id: inserted_post.id,
1037         name: inserted_post.name.clone(),
1038         creator_id: inserted_person.id,
1039         url: None,
1040         body: None,
1041         published: inserted_post.published,
1042         updated: None,
1043         community_id: inserted_community.id,
1044         removed: false,
1045         deleted: false,
1046         locked: false,
1047         nsfw: false,
1048         embed_title: None,
1049         embed_description: None,
1050         embed_video_url: None,
1051         thumbnail_url: None,
1052         ap_id: inserted_post.ap_id.clone(),
1053         local: true,
1054         language_id: LanguageId(47),
1055         featured_community: false,
1056         featured_local: false,
1057       },
1058       my_vote: None,
1059       unread_comments: 0,
1060       creator: Person {
1061         id: inserted_person.id,
1062         name: inserted_person.name.clone(),
1063         display_name: None,
1064         published: inserted_person.published,
1065         avatar: None,
1066         actor_id: inserted_person.actor_id.clone(),
1067         local: true,
1068         admin: false,
1069         bot_account: false,
1070         banned: false,
1071         deleted: false,
1072         bio: None,
1073         banner: None,
1074         updated: None,
1075         inbox_url: inserted_person.inbox_url.clone(),
1076         shared_inbox_url: None,
1077         matrix_user_id: None,
1078         ban_expires: None,
1079         instance_id: data.inserted_instance.id,
1080         private_key: inserted_person.private_key.clone(),
1081         public_key: inserted_person.public_key.clone(),
1082         last_refreshed_at: inserted_person.last_refreshed_at,
1083       },
1084       creator_banned_from_community: false,
1085       community: Community {
1086         id: inserted_community.id,
1087         name: inserted_community.name.clone(),
1088         icon: None,
1089         removed: false,
1090         deleted: false,
1091         nsfw: false,
1092         actor_id: inserted_community.actor_id.clone(),
1093         local: true,
1094         title: "nada".to_owned(),
1095         description: None,
1096         updated: None,
1097         banner: None,
1098         hidden: false,
1099         posting_restricted_to_mods: false,
1100         published: inserted_community.published,
1101         instance_id: data.inserted_instance.id,
1102         private_key: inserted_community.private_key.clone(),
1103         public_key: inserted_community.public_key.clone(),
1104         last_refreshed_at: inserted_community.last_refreshed_at,
1105         followers_url: inserted_community.followers_url.clone(),
1106         inbox_url: inserted_community.inbox_url.clone(),
1107         shared_inbox_url: inserted_community.shared_inbox_url.clone(),
1108         moderators_url: inserted_community.moderators_url.clone(),
1109         featured_url: inserted_community.featured_url.clone(),
1110       },
1111       counts: PostAggregates {
1112         id: agg.id,
1113         post_id: inserted_post.id,
1114         comments: 0,
1115         score: 0,
1116         upvotes: 0,
1117         downvotes: 0,
1118         published: agg.published,
1119         newest_comment_time_necro: inserted_post.published,
1120         newest_comment_time: inserted_post.published,
1121         featured_community: false,
1122         featured_local: false,
1123         hot_rank: 1728,
1124         hot_rank_active: 1728,
1125         controversy_rank: 0.0,
1126         community_id: inserted_post.community_id,
1127         creator_id: inserted_post.creator_id,
1128       },
1129       subscribed: SubscribedType::NotSubscribed,
1130       read: false,
1131       saved: false,
1132       creator_blocked: false,
1133     }
1134   }
1135 }