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