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