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