]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
7d461d14d99bb6e2db73e3f6e68e7ea82699f9d6
[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 {
612       show_bot_accounts: Some(false),
613       ..Default::default()
614     };
615     let inserted_local_user =
616       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
617         .await
618         .unwrap();
619     data.local_user_view.local_user = inserted_local_user;
620
621     let read_post_listing = PostQuery {
622       sort: (Some(SortType::New)),
623       community_id: (Some(data.inserted_community.id)),
624       local_user: (Some(&data.local_user_view)),
625       ..Default::default()
626     }
627     .list(pool)
628     .await
629     .unwrap();
630
631     let post_listing_single_with_person = PostView::read(
632       pool,
633       data.inserted_post.id,
634       Some(data.local_user_view.person.id),
635       None,
636     )
637     .await
638     .unwrap();
639
640     let mut expected_post_listing_with_user = expected_post_view(&data, pool).await;
641
642     // Should be only one person, IE the bot post, and blocked should be missing
643     assert_eq!(1, read_post_listing.len());
644
645     assert_eq!(expected_post_listing_with_user, read_post_listing[0]);
646     expected_post_listing_with_user.my_vote = Some(0);
647     assert_eq!(
648       expected_post_listing_with_user,
649       post_listing_single_with_person
650     );
651
652     let local_user_form = LocalUserUpdateForm {
653       show_bot_accounts: Some(true),
654       ..Default::default()
655     };
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 {
786       show_bot_accounts: Some(false),
787       ..Default::default()
788     };
789     let inserted_local_user =
790       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
791         .await
792         .unwrap();
793     data.local_user_view.local_user = inserted_local_user;
794
795     let read_post_listing = PostQuery {
796       sort: (Some(SortType::New)),
797       community_id: (Some(data.inserted_community.id)),
798       local_user: (Some(&data.local_user_view)),
799       ..Default::default()
800     }
801     .list(pool)
802     .await
803     .unwrap();
804     assert_eq!(1, read_post_listing.len());
805
806     assert_eq!(expected_post_with_upvote, read_post_listing[0]);
807
808     let read_liked_post_listing = PostQuery {
809       community_id: (Some(data.inserted_community.id)),
810       local_user: (Some(&data.local_user_view)),
811       liked_only: (Some(true)),
812       ..Default::default()
813     }
814     .list(pool)
815     .await
816     .unwrap();
817     assert_eq!(read_post_listing, read_liked_post_listing);
818
819     let read_disliked_post_listing = PostQuery {
820       community_id: (Some(data.inserted_community.id)),
821       local_user: (Some(&data.local_user_view)),
822       disliked_only: (Some(true)),
823       ..Default::default()
824     }
825     .list(pool)
826     .await
827     .unwrap();
828     assert!(read_disliked_post_listing.is_empty());
829
830     let like_removed =
831       PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id)
832         .await
833         .unwrap();
834     assert_eq!(1, like_removed);
835     cleanup(data, pool).await;
836   }
837
838   #[tokio::test]
839   #[serial]
840   async fn post_listing_person_language() {
841     let pool = &build_db_pool_for_tests().await;
842     let pool = &mut pool.into();
843     let data = init_data(pool).await;
844
845     let spanish_id = Language::read_id_from_code(pool, Some("es"))
846       .await
847       .unwrap()
848       .unwrap();
849     let post_spanish = PostInsertForm::builder()
850       .name("asffgdsc".to_string())
851       .creator_id(data.local_user_view.person.id)
852       .community_id(data.inserted_community.id)
853       .language_id(Some(spanish_id))
854       .build();
855
856     Post::create(pool, &post_spanish).await.unwrap();
857
858     let post_listings_all = PostQuery {
859       sort: (Some(SortType::New)),
860       local_user: (Some(&data.local_user_view)),
861       ..Default::default()
862     }
863     .list(pool)
864     .await
865     .unwrap();
866
867     // no language filters specified, all posts should be returned
868     assert_eq!(3, post_listings_all.len());
869
870     let french_id = Language::read_id_from_code(pool, Some("fr"))
871       .await
872       .unwrap()
873       .unwrap();
874     LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id)
875       .await
876       .unwrap();
877
878     let post_listing_french = PostQuery {
879       sort: (Some(SortType::New)),
880       local_user: (Some(&data.local_user_view)),
881       ..Default::default()
882     }
883     .list(pool)
884     .await
885     .unwrap();
886
887     // only one post in french and one undetermined should be returned
888     assert_eq!(2, post_listing_french.len());
889     assert!(post_listing_french
890       .iter()
891       .any(|p| p.post.language_id == french_id));
892
893     LocalUserLanguage::update(
894       pool,
895       vec![french_id, UNDETERMINED_ID],
896       data.local_user_view.local_user.id,
897     )
898     .await
899     .unwrap();
900     let post_listings_french_und = PostQuery {
901       sort: (Some(SortType::New)),
902       local_user: (Some(&data.local_user_view)),
903       ..Default::default()
904     }
905     .list(pool)
906     .await
907     .unwrap();
908
909     // french post and undetermined language post should be returned
910     assert_eq!(2, post_listings_french_und.len());
911     assert_eq!(
912       UNDETERMINED_ID,
913       post_listings_french_und[0].post.language_id
914     );
915     assert_eq!(french_id, post_listings_french_und[1].post.language_id);
916
917     cleanup(data, pool).await;
918   }
919
920   #[tokio::test]
921   #[serial]
922   async fn post_listings_removed() {
923     let pool = &build_db_pool_for_tests().await;
924     let pool = &mut pool.into();
925     let mut data = init_data(pool).await;
926
927     // Remove the post
928     Post::update(
929       pool,
930       data.inserted_post.id,
931       &PostUpdateForm {
932         removed: Some(true),
933         ..Default::default()
934       },
935     )
936     .await
937     .unwrap();
938
939     // Make sure you don't see the removed post in the results
940     let post_listings_no_admin = PostQuery {
941       sort: Some(SortType::New),
942       local_user: Some(&data.local_user_view),
943       ..Default::default()
944     }
945     .list(pool)
946     .await
947     .unwrap();
948     assert_eq!(1, post_listings_no_admin.len());
949
950     // Removed post is shown to admins on profile page
951     data.local_user_view.person.admin = true;
952     let post_listings_is_admin = PostQuery {
953       sort: Some(SortType::New),
954       local_user: Some(&data.local_user_view),
955       is_profile_view: true,
956       ..Default::default()
957     }
958     .list(pool)
959     .await
960     .unwrap();
961     assert_eq!(2, post_listings_is_admin.len());
962
963     cleanup(data, pool).await;
964   }
965
966   #[tokio::test]
967   #[serial]
968   async fn post_listings_deleted() {
969     let pool = &build_db_pool_for_tests().await;
970     let pool = &mut pool.into();
971     let data = init_data(pool).await;
972
973     // Delete the post
974     Post::update(
975       pool,
976       data.inserted_post.id,
977       &PostUpdateForm {
978         deleted: Some(true),
979         ..Default::default()
980       },
981     )
982     .await
983     .unwrap();
984
985     // Make sure you don't see the deleted post in the results
986     let post_listings_no_creator = PostQuery {
987       sort: Some(SortType::New),
988       ..Default::default()
989     }
990     .list(pool)
991     .await
992     .unwrap();
993     let not_contains_deleted = post_listings_no_creator
994       .iter()
995       .map(|p| p.post.id)
996       .all(|p| p != data.inserted_post.id);
997     assert!(not_contains_deleted);
998
999     // Deleted post is shown to creator
1000     let post_listings_is_creator = PostQuery {
1001       sort: Some(SortType::New),
1002       local_user: Some(&data.local_user_view),
1003       ..Default::default()
1004     }
1005     .list(pool)
1006     .await
1007     .unwrap();
1008     let contains_deleted = post_listings_is_creator
1009       .iter()
1010       .map(|p| p.post.id)
1011       .any(|p| p == data.inserted_post.id);
1012     assert!(contains_deleted);
1013
1014     cleanup(data, pool).await;
1015   }
1016
1017   async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
1018     let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
1019     Community::delete(pool, data.inserted_community.id)
1020       .await
1021       .unwrap();
1022     Person::delete(pool, data.local_user_view.person.id)
1023       .await
1024       .unwrap();
1025     Person::delete(pool, data.inserted_bot.id).await.unwrap();
1026     Person::delete(pool, data.inserted_blocked_person.id)
1027       .await
1028       .unwrap();
1029     Instance::delete(pool, data.inserted_instance.id)
1030       .await
1031       .unwrap();
1032     assert_eq!(1, num_deleted);
1033   }
1034
1035   async fn expected_post_view(data: &Data, pool: &mut DbPool<'_>) -> PostView {
1036     let (inserted_person, inserted_community, inserted_post) = (
1037       &data.local_user_view.person,
1038       &data.inserted_community,
1039       &data.inserted_post,
1040     );
1041     let agg = PostAggregates::read(pool, inserted_post.id).await.unwrap();
1042
1043     PostView {
1044       post: Post {
1045         id: inserted_post.id,
1046         name: inserted_post.name.clone(),
1047         creator_id: inserted_person.id,
1048         url: None,
1049         body: None,
1050         published: inserted_post.published,
1051         updated: None,
1052         community_id: inserted_community.id,
1053         removed: false,
1054         deleted: false,
1055         locked: false,
1056         nsfw: false,
1057         embed_title: None,
1058         embed_description: None,
1059         embed_video_url: None,
1060         thumbnail_url: None,
1061         ap_id: inserted_post.ap_id.clone(),
1062         local: true,
1063         language_id: LanguageId(47),
1064         featured_community: false,
1065         featured_local: false,
1066       },
1067       my_vote: None,
1068       unread_comments: 0,
1069       creator: Person {
1070         id: inserted_person.id,
1071         name: inserted_person.name.clone(),
1072         display_name: None,
1073         published: inserted_person.published,
1074         avatar: None,
1075         actor_id: inserted_person.actor_id.clone(),
1076         local: true,
1077         admin: false,
1078         bot_account: false,
1079         banned: false,
1080         deleted: false,
1081         bio: None,
1082         banner: None,
1083         updated: None,
1084         inbox_url: inserted_person.inbox_url.clone(),
1085         shared_inbox_url: None,
1086         matrix_user_id: None,
1087         ban_expires: None,
1088         instance_id: data.inserted_instance.id,
1089         private_key: inserted_person.private_key.clone(),
1090         public_key: inserted_person.public_key.clone(),
1091         last_refreshed_at: inserted_person.last_refreshed_at,
1092       },
1093       creator_banned_from_community: false,
1094       community: Community {
1095         id: inserted_community.id,
1096         name: inserted_community.name.clone(),
1097         icon: None,
1098         removed: false,
1099         deleted: false,
1100         nsfw: false,
1101         actor_id: inserted_community.actor_id.clone(),
1102         local: true,
1103         title: "nada".to_owned(),
1104         description: None,
1105         updated: None,
1106         banner: None,
1107         hidden: false,
1108         posting_restricted_to_mods: false,
1109         published: inserted_community.published,
1110         instance_id: data.inserted_instance.id,
1111         private_key: inserted_community.private_key.clone(),
1112         public_key: inserted_community.public_key.clone(),
1113         last_refreshed_at: inserted_community.last_refreshed_at,
1114         followers_url: inserted_community.followers_url.clone(),
1115         inbox_url: inserted_community.inbox_url.clone(),
1116         shared_inbox_url: inserted_community.shared_inbox_url.clone(),
1117         moderators_url: inserted_community.moderators_url.clone(),
1118         featured_url: inserted_community.featured_url.clone(),
1119       },
1120       counts: PostAggregates {
1121         id: agg.id,
1122         post_id: inserted_post.id,
1123         comments: 0,
1124         score: 0,
1125         upvotes: 0,
1126         downvotes: 0,
1127         published: agg.published,
1128         newest_comment_time_necro: inserted_post.published,
1129         newest_comment_time: inserted_post.published,
1130         featured_community: false,
1131         featured_local: false,
1132         hot_rank: 1728,
1133         hot_rank_active: 1728,
1134         controversy_rank: 0.0,
1135         community_id: inserted_post.community_id,
1136         creator_id: inserted_post.creator_id,
1137       },
1138       subscribed: SubscribedType::NotSubscribed,
1139       read: false,
1140       saved: false,
1141       creator_blocked: false,
1142     }
1143   }
1144 }