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