]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
Replace Option<bool> with bool for PostQuery and CommentQuery (#3819) (#3857)
[lemmy.git] / crates / db_views / src / comment_view.rs
1 use crate::structs::{CommentView, LocalUserView};
2 use diesel::{
3   pg::Pg,
4   result::Error,
5   BoolExpressionMethods,
6   ExpressionMethods,
7   JoinOnDsl,
8   NullableExpressionMethods,
9   PgTextExpressionMethods,
10   QueryDsl,
11 };
12 use diesel_async::RunQueryDsl;
13 use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
14 use lemmy_db_schema::{
15   aggregates::structs::CommentAggregates,
16   newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId},
17   schema::{
18     comment,
19     comment_aggregates,
20     comment_like,
21     comment_saved,
22     community,
23     community_block,
24     community_follower,
25     community_person_ban,
26     local_user_language,
27     person,
28     person_block,
29     post,
30   },
31   source::{
32     comment::Comment,
33     community::{Community, CommunityFollower},
34     person::Person,
35     post::Post,
36   },
37   traits::JoinView,
38   utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
39   CommentSortType,
40   ListingType,
41   SubscribedType,
42 };
43
44 type CommentViewTuple = (
45   Comment,
46   Person,
47   Post,
48   Community,
49   CommentAggregates,
50   bool,
51   SubscribedType,
52   bool,
53   bool,
54   Option<i16>,
55 );
56
57 fn queries<'a>() -> Queries<
58   impl ReadFn<'a, CommentView, (CommentId, Option<PersonId>)>,
59   impl ListFn<'a, CommentView, CommentQuery<'a>>,
60 > {
61   let all_joins = |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| {
62     // The left join below will return None in this case
63     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
64     query
65       .inner_join(person::table)
66       .inner_join(post::table)
67       .inner_join(community::table.on(post::community_id.eq(community::id)))
68       .inner_join(comment_aggregates::table)
69       .left_join(
70         community_person_ban::table.on(
71           community::id
72             .eq(community_person_ban::community_id)
73             .and(community_person_ban::person_id.eq(comment::creator_id)),
74         ),
75       )
76       .left_join(
77         community_follower::table.on(
78           post::community_id
79             .eq(community_follower::community_id)
80             .and(community_follower::person_id.eq(person_id_join)),
81         ),
82       )
83       .left_join(
84         comment_saved::table.on(
85           comment::id
86             .eq(comment_saved::comment_id)
87             .and(comment_saved::person_id.eq(person_id_join)),
88         ),
89       )
90       .left_join(
91         person_block::table.on(
92           comment::creator_id
93             .eq(person_block::target_id)
94             .and(person_block::person_id.eq(person_id_join)),
95         ),
96       )
97       .left_join(
98         comment_like::table.on(
99           comment::id
100             .eq(comment_like::comment_id)
101             .and(comment_like::person_id.eq(person_id_join)),
102         ),
103       )
104   };
105
106   let selection = (
107     comment::all_columns,
108     person::all_columns,
109     post::all_columns,
110     community::all_columns,
111     comment_aggregates::all_columns,
112     community_person_ban::id.nullable().is_not_null(),
113     CommunityFollower::select_subscribed_type(),
114     comment_saved::id.nullable().is_not_null(),
115     person_block::id.nullable().is_not_null(),
116     comment_like::score.nullable(),
117   );
118
119   let read = move |mut conn: DbConn<'a>,
120                    (comment_id, my_person_id): (CommentId, Option<PersonId>)| async move {
121     all_joins(comment::table.find(comment_id).into_boxed(), my_person_id)
122       .select(selection)
123       .first::<CommentViewTuple>(&mut conn)
124       .await
125   };
126
127   let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move {
128     let person_id = options.local_user.map(|l| l.person.id);
129     let local_user_id = options.local_user.map(|l| l.local_user.id);
130
131     // The left join below will return None in this case
132     let person_id_join = person_id.unwrap_or(PersonId(-1));
133     let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
134
135     let mut query = all_joins(comment::table.into_boxed(), person_id)
136       .left_join(
137         community_block::table.on(
138           community::id
139             .eq(community_block::community_id)
140             .and(community_block::person_id.eq(person_id_join)),
141         ),
142       )
143       .left_join(
144         local_user_language::table.on(
145           comment::language_id
146             .eq(local_user_language::language_id)
147             .and(local_user_language::local_user_id.eq(local_user_id_join)),
148         ),
149       )
150       .select(selection);
151
152     if let Some(creator_id) = options.creator_id {
153       query = query.filter(comment::creator_id.eq(creator_id));
154     };
155
156     if let Some(post_id) = options.post_id {
157       query = query.filter(comment::post_id.eq(post_id));
158     };
159
160     if let Some(parent_path) = options.parent_path.as_ref() {
161       query = query.filter(comment::path.contained_by(parent_path));
162     };
163
164     if let Some(search_term) = options.search_term {
165       query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
166     };
167
168     if let Some(community_id) = options.community_id {
169       query = query.filter(post::community_id.eq(community_id));
170     }
171
172     if let Some(listing_type) = options.listing_type {
173       match listing_type {
174         ListingType::Subscribed => query = query.filter(community_follower::pending.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
175         ListingType::Local => {
176           query = query.filter(community::local.eq(true)).filter(
177             community::hidden
178               .eq(false)
179               .or(community_follower::person_id.eq(person_id_join)),
180           )
181         }
182         ListingType::All => {
183           query = query.filter(
184             community::hidden
185               .eq(false)
186               .or(community_follower::person_id.eq(person_id_join)),
187           )
188         }
189       }
190     }
191
192     if options.saved_only {
193       query = query.filter(comment_saved::comment_id.is_not_null());
194     }
195
196     if options.liked_only {
197       query = query.filter(comment_like::score.eq(1));
198     } else if options.disliked_only {
199       query = query.filter(comment_like::score.eq(-1));
200     }
201
202     let is_creator = options.creator_id == options.local_user.map(|l| l.person.id);
203     // only show deleted comments to creator
204     if !is_creator {
205       query = query.filter(comment::deleted.eq(false));
206     }
207
208     let is_admin = options.local_user.map(|l| l.person.admin).unwrap_or(false);
209     // only show removed comments to admin when viewing user profile
210     if !(options.is_profile_view && is_admin) {
211       query = query.filter(comment::removed.eq(false));
212     }
213
214     if !options
215       .local_user
216       .map(|l| l.local_user.show_bot_accounts)
217       .unwrap_or(true)
218     {
219       query = query.filter(person::bot_account.eq(false));
220     };
221
222     if options.local_user.is_some() {
223       // Filter out the rows with missing languages
224       query = query.filter(local_user_language::language_id.is_not_null());
225
226       // Don't show blocked communities or persons
227       if options.post_id.is_none() {
228         query = query.filter(community_block::person_id.is_null());
229       }
230       query = query.filter(person_block::person_id.is_null());
231     }
232
233     // A Max depth given means its a tree fetch
234     let (limit, offset) = if let Some(max_depth) = options.max_depth {
235       let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() {
236         parent_path.0.split('.').count() as i32 + max_depth
237         // Add one because of root "0"
238       } else {
239         max_depth + 1
240       };
241
242       query = query.filter(nlevel(comment::path).le(depth_limit));
243
244       // only order if filtering by a post id, or parent_path. DOS potential otherwise and max_depth + !post_id isn't used anyways (afaik)
245       if options.post_id.is_some() || options.parent_path.is_some() {
246         // Always order by the parent path first
247         query = query.order_by(subpath(comment::path, 0, -1));
248       }
249
250       // TODO limit question. Limiting does not work for comment threads ATM, only max_depth
251       // For now, don't do any limiting for tree fetches
252       // https://stackoverflow.com/questions/72983614/postgres-ltree-how-to-limit-the-max-number-of-children-at-any-given-level
253
254       // Don't use the regular error-checking one, many more comments must ofter be fetched.
255       // This does not work for comment trees, and the limit should be manually set to a high number
256       //
257       // If a max depth is given, then you know its a tree fetch, and limits should be ignored
258       // TODO a kludge to prevent attacks. Limit comments to 300 for now.
259       // (i64::MAX, 0)
260       (300, 0)
261     } else {
262       // limit_and_offset_unlimited(options.page, options.limit)
263       limit_and_offset(options.page, options.limit)?
264     };
265
266     query = match options.sort.unwrap_or(CommentSortType::Hot) {
267       CommentSortType::Hot => query
268         .then_order_by(comment_aggregates::hot_rank.desc())
269         .then_order_by(comment_aggregates::score.desc()),
270       CommentSortType::Controversial => {
271         query.then_order_by(comment_aggregates::controversy_rank.desc())
272       }
273       CommentSortType::New => query.then_order_by(comment::published.desc()),
274       CommentSortType::Old => query.then_order_by(comment::published.asc()),
275       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
276     };
277
278     // Note: deleted and removed comments are done on the front side
279     query
280       .limit(limit)
281       .offset(offset)
282       .load::<CommentViewTuple>(&mut conn)
283       .await
284   };
285
286   Queries::new(read, list)
287 }
288
289 impl CommentView {
290   pub async fn read(
291     pool: &mut DbPool<'_>,
292     comment_id: CommentId,
293     my_person_id: Option<PersonId>,
294   ) -> Result<Self, Error> {
295     // If a person is given, then my_vote (res.9), if None, should be 0, not null
296     // Necessary to differentiate between other person's votes
297     let mut res = queries().read(pool, (comment_id, my_person_id)).await?;
298     if my_person_id.is_some() && res.my_vote.is_none() {
299       res.my_vote = Some(0);
300     }
301     Ok(res)
302   }
303 }
304
305 #[derive(Default)]
306 pub struct CommentQuery<'a> {
307   pub listing_type: Option<ListingType>,
308   pub sort: Option<CommentSortType>,
309   pub community_id: Option<CommunityId>,
310   pub post_id: Option<PostId>,
311   pub parent_path: Option<Ltree>,
312   pub creator_id: Option<PersonId>,
313   pub local_user: Option<&'a LocalUserView>,
314   pub search_term: Option<String>,
315   pub saved_only: bool,
316   pub liked_only: bool,
317   pub disliked_only: bool,
318   pub is_profile_view: bool,
319   pub page: Option<i64>,
320   pub limit: Option<i64>,
321   pub max_depth: Option<i32>,
322 }
323
324 impl<'a> CommentQuery<'a> {
325   pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<CommentView>, Error> {
326     queries().list(pool, self).await
327   }
328 }
329
330 impl JoinView for CommentView {
331   type JoinTuple = CommentViewTuple;
332   fn from_tuple(a: Self::JoinTuple) -> Self {
333     Self {
334       comment: a.0,
335       creator: a.1,
336       post: a.2,
337       community: a.3,
338       counts: a.4,
339       creator_banned_from_community: a.5,
340       subscribed: a.6,
341       saved: a.7,
342       creator_blocked: a.8,
343       my_vote: a.9,
344     }
345   }
346 }
347
348 #[cfg(test)]
349 mod tests {
350   #![allow(clippy::unwrap_used)]
351   #![allow(clippy::indexing_slicing)]
352
353   use crate::{
354     comment_view::{
355       Comment,
356       CommentQuery,
357       CommentSortType,
358       CommentView,
359       Community,
360       DbPool,
361       Person,
362       Post,
363     },
364     structs::LocalUserView,
365   };
366   use lemmy_db_schema::{
367     aggregates::structs::CommentAggregates,
368     impls::actor_language::UNDETERMINED_ID,
369     newtypes::LanguageId,
370     source::{
371       actor_language::LocalUserLanguage,
372       comment::{CommentInsertForm, CommentLike, CommentLikeForm},
373       community::CommunityInsertForm,
374       instance::Instance,
375       language::Language,
376       local_user::{LocalUser, LocalUserInsertForm},
377       person::PersonInsertForm,
378       person_block::{PersonBlock, PersonBlockForm},
379       post::PostInsertForm,
380     },
381     traits::{Blockable, Crud, Likeable},
382     utils::build_db_pool_for_tests,
383     SubscribedType,
384   };
385   use serial_test::serial;
386
387   struct Data {
388     inserted_instance: Instance,
389     inserted_comment_0: Comment,
390     inserted_comment_1: Comment,
391     inserted_comment_2: Comment,
392     inserted_post: Post,
393     local_user_view: LocalUserView,
394     inserted_person_2: Person,
395     inserted_community: Community,
396   }
397
398   async fn init_data(pool: &mut DbPool<'_>) -> Data {
399     let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
400       .await
401       .unwrap();
402
403     let new_person = PersonInsertForm::builder()
404       .name("timmy".into())
405       .public_key("pubkey".to_string())
406       .instance_id(inserted_instance.id)
407       .build();
408     let inserted_person = Person::create(pool, &new_person).await.unwrap();
409     let local_user_form = LocalUserInsertForm::builder()
410       .person_id(inserted_person.id)
411       .password_encrypted(String::new())
412       .build();
413     let inserted_local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
414
415     let new_person_2 = PersonInsertForm::builder()
416       .name("sara".into())
417       .public_key("pubkey".to_string())
418       .instance_id(inserted_instance.id)
419       .build();
420     let inserted_person_2 = Person::create(pool, &new_person_2).await.unwrap();
421
422     let new_community = CommunityInsertForm::builder()
423       .name("test community 5".to_string())
424       .title("nada".to_owned())
425       .public_key("pubkey".to_string())
426       .instance_id(inserted_instance.id)
427       .build();
428
429     let inserted_community = Community::create(pool, &new_community).await.unwrap();
430
431     let new_post = PostInsertForm::builder()
432       .name("A test post 2".into())
433       .creator_id(inserted_person.id)
434       .community_id(inserted_community.id)
435       .build();
436
437     let inserted_post = Post::create(pool, &new_post).await.unwrap();
438     let english_id = Language::read_id_from_code(pool, Some("en")).await.unwrap();
439
440     // Create a comment tree with this hierarchy
441     //       0
442     //     \     \
443     //    1      2
444     //    \
445     //  3  4
446     //     \
447     //     5
448     let comment_form_0 = CommentInsertForm::builder()
449       .content("Comment 0".into())
450       .creator_id(inserted_person.id)
451       .post_id(inserted_post.id)
452       .language_id(english_id)
453       .build();
454
455     let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await.unwrap();
456
457     let comment_form_1 = CommentInsertForm::builder()
458       .content("Comment 1, A test blocked comment".into())
459       .creator_id(inserted_person_2.id)
460       .post_id(inserted_post.id)
461       .language_id(english_id)
462       .build();
463
464     let inserted_comment_1 = Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path))
465       .await
466       .unwrap();
467
468     let finnish_id = Language::read_id_from_code(pool, Some("fi")).await.unwrap();
469     let comment_form_2 = CommentInsertForm::builder()
470       .content("Comment 2".into())
471       .creator_id(inserted_person.id)
472       .post_id(inserted_post.id)
473       .language_id(finnish_id)
474       .build();
475
476     let inserted_comment_2 = Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path))
477       .await
478       .unwrap();
479
480     let comment_form_3 = CommentInsertForm::builder()
481       .content("Comment 3".into())
482       .creator_id(inserted_person.id)
483       .post_id(inserted_post.id)
484       .language_id(english_id)
485       .build();
486
487     let _inserted_comment_3 =
488       Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path))
489         .await
490         .unwrap();
491
492     let polish_id = Language::read_id_from_code(pool, Some("pl"))
493       .await
494       .unwrap()
495       .unwrap();
496     let comment_form_4 = CommentInsertForm::builder()
497       .content("Comment 4".into())
498       .creator_id(inserted_person.id)
499       .post_id(inserted_post.id)
500       .language_id(Some(polish_id))
501       .build();
502
503     let inserted_comment_4 = Comment::create(pool, &comment_form_4, Some(&inserted_comment_1.path))
504       .await
505       .unwrap();
506
507     let comment_form_5 = CommentInsertForm::builder()
508       .content("Comment 5".into())
509       .creator_id(inserted_person.id)
510       .post_id(inserted_post.id)
511       .build();
512
513     let _inserted_comment_5 =
514       Comment::create(pool, &comment_form_5, Some(&inserted_comment_4.path))
515         .await
516         .unwrap();
517
518     let timmy_blocks_sara_form = PersonBlockForm {
519       person_id: inserted_person.id,
520       target_id: inserted_person_2.id,
521     };
522
523     let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form)
524       .await
525       .unwrap();
526
527     let expected_block = PersonBlock {
528       id: inserted_block.id,
529       person_id: inserted_person.id,
530       target_id: inserted_person_2.id,
531       published: inserted_block.published,
532     };
533     assert_eq!(expected_block, inserted_block);
534
535     let comment_like_form = CommentLikeForm {
536       comment_id: inserted_comment_0.id,
537       post_id: inserted_post.id,
538       person_id: inserted_person.id,
539       score: 1,
540     };
541
542     let _inserted_comment_like = CommentLike::like(pool, &comment_like_form).await.unwrap();
543
544     let local_user_view = LocalUserView {
545       local_user: inserted_local_user.clone(),
546       person: inserted_person.clone(),
547       counts: Default::default(),
548     };
549     Data {
550       inserted_instance,
551       inserted_comment_0,
552       inserted_comment_1,
553       inserted_comment_2,
554       inserted_post,
555       local_user_view,
556       inserted_person_2,
557       inserted_community,
558     }
559   }
560
561   #[tokio::test]
562   #[serial]
563   async fn test_crud() {
564     let pool = &build_db_pool_for_tests().await;
565     let pool = &mut pool.into();
566     let data = init_data(pool).await;
567
568     let expected_comment_view_no_person = expected_comment_view(&data, pool).await;
569
570     let mut expected_comment_view_with_person = expected_comment_view_no_person.clone();
571     expected_comment_view_with_person.my_vote = Some(1);
572
573     let read_comment_views_no_person = CommentQuery {
574       sort: (Some(CommentSortType::Old)),
575       post_id: (Some(data.inserted_post.id)),
576       ..Default::default()
577     }
578     .list(pool)
579     .await
580     .unwrap();
581
582     assert_eq!(
583       expected_comment_view_no_person,
584       read_comment_views_no_person[0]
585     );
586
587     let read_comment_views_with_person = CommentQuery {
588       sort: (Some(CommentSortType::Old)),
589       post_id: (Some(data.inserted_post.id)),
590       local_user: (Some(&data.local_user_view)),
591       ..Default::default()
592     }
593     .list(pool)
594     .await
595     .unwrap();
596
597     assert_eq!(
598       expected_comment_view_with_person,
599       read_comment_views_with_person[0]
600     );
601
602     // Make sure its 1, not showing the blocked comment
603     assert_eq!(5, read_comment_views_with_person.len());
604
605     let read_comment_from_blocked_person = CommentView::read(
606       pool,
607       data.inserted_comment_1.id,
608       Some(data.local_user_view.person.id),
609     )
610     .await
611     .unwrap();
612
613     // Make sure block set the creator blocked
614     assert!(read_comment_from_blocked_person.creator_blocked);
615
616     let read_liked_comment_views = CommentQuery {
617       local_user: (Some(&data.local_user_view)),
618       liked_only: (true),
619       ..Default::default()
620     }
621     .list(pool)
622     .await
623     .unwrap();
624
625     assert_eq!(
626       expected_comment_view_with_person,
627       read_liked_comment_views[0]
628     );
629
630     assert_eq!(1, read_liked_comment_views.len());
631
632     let read_disliked_comment_views: Vec<CommentView> = CommentQuery {
633       local_user: (Some(&data.local_user_view)),
634       disliked_only: (true),
635       ..Default::default()
636     }
637     .list(pool)
638     .await
639     .unwrap();
640
641     assert!(read_disliked_comment_views.is_empty());
642
643     cleanup(data, pool).await;
644   }
645
646   #[tokio::test]
647   #[serial]
648   async fn test_comment_tree() {
649     let pool = &build_db_pool_for_tests().await;
650     let pool = &mut pool.into();
651     let data = init_data(pool).await;
652
653     let top_path = data.inserted_comment_0.path.clone();
654     let read_comment_views_top_path = CommentQuery {
655       post_id: (Some(data.inserted_post.id)),
656       parent_path: (Some(top_path)),
657       ..Default::default()
658     }
659     .list(pool)
660     .await
661     .unwrap();
662
663     let child_path = data.inserted_comment_1.path.clone();
664     let read_comment_views_child_path = CommentQuery {
665       post_id: (Some(data.inserted_post.id)),
666       parent_path: (Some(child_path)),
667       ..Default::default()
668     }
669     .list(pool)
670     .await
671     .unwrap();
672
673     // Make sure the comment parent-limited fetch is correct
674     assert_eq!(6, read_comment_views_top_path.len());
675     assert_eq!(4, read_comment_views_child_path.len());
676
677     // Make sure it contains the parent, but not the comment from the other tree
678     let child_comments = read_comment_views_child_path
679       .into_iter()
680       .map(|c| c.comment)
681       .collect::<Vec<Comment>>();
682     assert!(child_comments.contains(&data.inserted_comment_1));
683     assert!(!child_comments.contains(&data.inserted_comment_2));
684
685     let read_comment_views_top_max_depth = CommentQuery {
686       post_id: (Some(data.inserted_post.id)),
687       max_depth: (Some(1)),
688       ..Default::default()
689     }
690     .list(pool)
691     .await
692     .unwrap();
693
694     // Make sure a depth limited one only has the top comment
695     assert_eq!(
696       expected_comment_view(&data, pool).await,
697       read_comment_views_top_max_depth[0]
698     );
699     assert_eq!(1, read_comment_views_top_max_depth.len());
700
701     let child_path = data.inserted_comment_1.path.clone();
702     let read_comment_views_parent_max_depth = CommentQuery {
703       post_id: (Some(data.inserted_post.id)),
704       parent_path: (Some(child_path)),
705       max_depth: (Some(1)),
706       sort: (Some(CommentSortType::New)),
707       ..Default::default()
708     }
709     .list(pool)
710     .await
711     .unwrap();
712
713     // Make sure a depth limited one, and given child comment 1, has 3
714     assert!(read_comment_views_parent_max_depth[2]
715       .comment
716       .content
717       .eq("Comment 3"));
718     assert_eq!(3, read_comment_views_parent_max_depth.len());
719
720     cleanup(data, pool).await;
721   }
722
723   #[tokio::test]
724   #[serial]
725   async fn test_languages() {
726     let pool = &build_db_pool_for_tests().await;
727     let pool = &mut pool.into();
728     let data = init_data(pool).await;
729
730     // by default, user has all languages enabled and should see all comments
731     // (except from blocked user)
732     let all_languages = CommentQuery {
733       local_user: (Some(&data.local_user_view)),
734       ..Default::default()
735     }
736     .list(pool)
737     .await
738     .unwrap();
739     assert_eq!(5, all_languages.len());
740
741     // change user lang to finnish, should only show one post in finnish and one undetermined
742     let finnish_id = Language::read_id_from_code(pool, Some("fi"))
743       .await
744       .unwrap()
745       .unwrap();
746     LocalUserLanguage::update(pool, vec![finnish_id], data.local_user_view.local_user.id)
747       .await
748       .unwrap();
749     let finnish_comments = CommentQuery {
750       local_user: (Some(&data.local_user_view)),
751       ..Default::default()
752     }
753     .list(pool)
754     .await
755     .unwrap();
756     assert_eq!(2, finnish_comments.len());
757     let finnish_comment = finnish_comments
758       .iter()
759       .find(|c| c.comment.language_id == finnish_id);
760     assert!(finnish_comment.is_some());
761     assert_eq!(
762       data.inserted_comment_2.content,
763       finnish_comment.unwrap().comment.content
764     );
765
766     // now show all comments with undetermined language (which is the default value)
767     LocalUserLanguage::update(
768       pool,
769       vec![UNDETERMINED_ID],
770       data.local_user_view.local_user.id,
771     )
772     .await
773     .unwrap();
774     let undetermined_comment = CommentQuery {
775       local_user: (Some(&data.local_user_view)),
776       ..Default::default()
777     }
778     .list(pool)
779     .await
780     .unwrap();
781     assert_eq!(1, undetermined_comment.len());
782
783     cleanup(data, pool).await;
784   }
785
786   async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
787     CommentLike::remove(
788       pool,
789       data.local_user_view.person.id,
790       data.inserted_comment_0.id,
791     )
792     .await
793     .unwrap();
794     Comment::delete(pool, data.inserted_comment_0.id)
795       .await
796       .unwrap();
797     Comment::delete(pool, data.inserted_comment_1.id)
798       .await
799       .unwrap();
800     Post::delete(pool, data.inserted_post.id).await.unwrap();
801     Community::delete(pool, data.inserted_community.id)
802       .await
803       .unwrap();
804     Person::delete(pool, data.local_user_view.person.id)
805       .await
806       .unwrap();
807     Person::delete(pool, data.inserted_person_2.id)
808       .await
809       .unwrap();
810     Instance::delete(pool, data.inserted_instance.id)
811       .await
812       .unwrap();
813   }
814
815   async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> CommentView {
816     let agg = CommentAggregates::read(pool, data.inserted_comment_0.id)
817       .await
818       .unwrap();
819     CommentView {
820       creator_banned_from_community: false,
821       my_vote: None,
822       subscribed: SubscribedType::NotSubscribed,
823       saved: false,
824       creator_blocked: false,
825       comment: Comment {
826         id: data.inserted_comment_0.id,
827         content: "Comment 0".into(),
828         creator_id: data.local_user_view.person.id,
829         post_id: data.inserted_post.id,
830         removed: false,
831         deleted: false,
832         published: data.inserted_comment_0.published,
833         ap_id: data.inserted_comment_0.ap_id.clone(),
834         updated: None,
835         local: true,
836         distinguished: false,
837         path: data.inserted_comment_0.clone().path,
838         language_id: LanguageId(37),
839       },
840       creator: Person {
841         id: data.local_user_view.person.id,
842         name: "timmy".into(),
843         display_name: None,
844         published: data.local_user_view.person.published,
845         avatar: None,
846         actor_id: data.local_user_view.person.actor_id.clone(),
847         local: true,
848         banned: false,
849         deleted: false,
850         admin: false,
851         bot_account: false,
852         bio: None,
853         banner: None,
854         updated: None,
855         inbox_url: data.local_user_view.person.inbox_url.clone(),
856         shared_inbox_url: None,
857         matrix_user_id: None,
858         ban_expires: None,
859         instance_id: data.inserted_instance.id,
860         private_key: data.local_user_view.person.private_key.clone(),
861         public_key: data.local_user_view.person.public_key.clone(),
862         last_refreshed_at: data.local_user_view.person.last_refreshed_at,
863       },
864       post: Post {
865         id: data.inserted_post.id,
866         name: data.inserted_post.name.clone(),
867         creator_id: data.local_user_view.person.id,
868         url: None,
869         body: None,
870         published: data.inserted_post.published,
871         updated: None,
872         community_id: data.inserted_community.id,
873         removed: false,
874         deleted: false,
875         locked: false,
876         nsfw: false,
877         embed_title: None,
878         embed_description: None,
879         embed_video_url: None,
880         thumbnail_url: None,
881         ap_id: data.inserted_post.ap_id.clone(),
882         local: true,
883         language_id: Default::default(),
884         featured_community: false,
885         featured_local: false,
886       },
887       community: Community {
888         id: data.inserted_community.id,
889         name: "test community 5".to_string(),
890         icon: None,
891         removed: false,
892         deleted: false,
893         nsfw: false,
894         actor_id: data.inserted_community.actor_id.clone(),
895         local: true,
896         title: "nada".to_owned(),
897         description: None,
898         updated: None,
899         banner: None,
900         hidden: false,
901         posting_restricted_to_mods: false,
902         published: data.inserted_community.published,
903         instance_id: data.inserted_instance.id,
904         private_key: data.inserted_community.private_key.clone(),
905         public_key: data.inserted_community.public_key.clone(),
906         last_refreshed_at: data.inserted_community.last_refreshed_at,
907         followers_url: data.inserted_community.followers_url.clone(),
908         inbox_url: data.inserted_community.inbox_url.clone(),
909         shared_inbox_url: data.inserted_community.shared_inbox_url.clone(),
910         moderators_url: data.inserted_community.moderators_url.clone(),
911         featured_url: data.inserted_community.featured_url.clone(),
912       },
913       counts: CommentAggregates {
914         id: agg.id,
915         comment_id: data.inserted_comment_0.id,
916         score: 1,
917         upvotes: 1,
918         downvotes: 0,
919         published: agg.published,
920         child_count: 5,
921         hot_rank: 1728,
922         controversy_rank: 0.0,
923       },
924     }
925   }
926 }