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