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