]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_report_view.rs
Addressing slow profile queries. #2777 (#2830)
[lemmy.git] / crates / db_views / src / comment_report_view.rs
1 use crate::structs::CommentReportView;
2 use diesel::{
3   dsl::now,
4   result::Error,
5   BoolExpressionMethods,
6   ExpressionMethods,
7   JoinOnDsl,
8   NullableExpressionMethods,
9   QueryDsl,
10 };
11 use diesel_async::RunQueryDsl;
12 use lemmy_db_schema::{
13   aggregates::structs::CommentAggregates,
14   newtypes::{CommentReportId, CommunityId, PersonId},
15   schema::{
16     comment,
17     comment_aggregates,
18     comment_like,
19     comment_report,
20     community,
21     community_moderator,
22     community_person_ban,
23     person,
24     post,
25   },
26   source::{
27     comment::Comment,
28     comment_report::CommentReport,
29     community::{Community, CommunityPersonBan},
30     person::Person,
31     post::Post,
32   },
33   traits::JoinView,
34   utils::{get_conn, limit_and_offset, DbPool},
35 };
36 use typed_builder::TypedBuilder;
37
38 impl CommentReportView {
39   /// returns the CommentReportView for the provided report_id
40   ///
41   /// * `report_id` - the report id to obtain
42   pub async fn read(
43     pool: &DbPool,
44     report_id: CommentReportId,
45     my_person_id: PersonId,
46   ) -> Result<Self, Error> {
47     let conn = &mut get_conn(pool).await?;
48
49     let (person_alias_1, person_alias_2) = diesel::alias!(person as person1, person as person2);
50
51     let res = comment_report::table
52       .find(report_id)
53       .inner_join(comment::table)
54       .inner_join(post::table.on(comment::post_id.eq(post::id)))
55       .inner_join(community::table.on(post::community_id.eq(community::id)))
56       .inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
57       .inner_join(person_alias_1.on(comment::creator_id.eq(person_alias_1.field(person::id))))
58       .inner_join(
59         comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
60       )
61       .left_join(
62         community_person_ban::table.on(
63           community::id
64             .eq(community_person_ban::community_id)
65             .and(community_person_ban::person_id.eq(comment::creator_id)),
66         ),
67       )
68       .left_join(
69         comment_like::table.on(
70           comment::id
71             .eq(comment_like::comment_id)
72             .and(comment_like::person_id.eq(my_person_id)),
73         ),
74       )
75       .left_join(
76         person_alias_2
77           .on(comment_report::resolver_id.eq(person_alias_2.field(person::id).nullable())),
78       )
79       .select((
80         comment_report::all_columns,
81         comment::all_columns,
82         post::all_columns,
83         community::all_columns,
84         person::all_columns,
85         person_alias_1.fields(person::all_columns),
86         comment_aggregates::all_columns,
87         community_person_ban::all_columns.nullable(),
88         comment_like::score.nullable(),
89         person_alias_2.fields(person::all_columns).nullable(),
90       ))
91       .first::<<CommentReportView as JoinView>::JoinTuple>(conn)
92       .await?;
93
94     Ok(Self::from_tuple(res))
95   }
96
97   /// Returns the current unresolved post report count for the communities you mod
98   pub async fn get_report_count(
99     pool: &DbPool,
100     my_person_id: PersonId,
101     admin: bool,
102     community_id: Option<CommunityId>,
103   ) -> Result<i64, Error> {
104     use diesel::dsl::count;
105
106     let conn = &mut get_conn(pool).await?;
107
108     let mut query = comment_report::table
109       .inner_join(comment::table)
110       .inner_join(post::table.on(comment::post_id.eq(post::id)))
111       .filter(comment_report::resolved.eq(false))
112       .into_boxed();
113
114     if let Some(community_id) = community_id {
115       query = query.filter(post::community_id.eq(community_id))
116     }
117
118     // If its not an admin, get only the ones you mod
119     if !admin {
120       query
121         .inner_join(
122           community_moderator::table.on(
123             community_moderator::community_id
124               .eq(post::community_id)
125               .and(community_moderator::person_id.eq(my_person_id)),
126           ),
127         )
128         .select(count(comment_report::id))
129         .first::<i64>(conn)
130         .await
131     } else {
132       query
133         .select(count(comment_report::id))
134         .first::<i64>(conn)
135         .await
136     }
137   }
138 }
139
140 #[derive(TypedBuilder)]
141 #[builder(field_defaults(default))]
142 pub struct CommentReportQuery<'a> {
143   #[builder(!default)]
144   pool: &'a DbPool,
145   #[builder(!default)]
146   my_person_id: PersonId,
147   #[builder(!default)]
148   admin: bool,
149   community_id: Option<CommunityId>,
150   page: Option<i64>,
151   limit: Option<i64>,
152   unresolved_only: Option<bool>,
153 }
154
155 impl<'a> CommentReportQuery<'a> {
156   pub async fn list(self) -> Result<Vec<CommentReportView>, Error> {
157     let conn = &mut get_conn(self.pool).await?;
158
159     let (person_alias_1, person_alias_2) = diesel::alias!(person as person1, person as person2);
160
161     let mut query = comment_report::table
162       .inner_join(comment::table)
163       .inner_join(post::table.on(comment::post_id.eq(post::id)))
164       .inner_join(community::table.on(post::community_id.eq(community::id)))
165       .inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
166       .inner_join(person_alias_1.on(comment::creator_id.eq(person_alias_1.field(person::id))))
167       .inner_join(
168         comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
169       )
170       .left_join(
171         community_person_ban::table.on(
172           community::id
173             .eq(community_person_ban::community_id)
174             .and(community_person_ban::person_id.eq(comment::creator_id))
175             .and(
176               community_person_ban::expires
177                 .is_null()
178                 .or(community_person_ban::expires.gt(now)),
179             ),
180         ),
181       )
182       .left_join(
183         comment_like::table.on(
184           comment::id
185             .eq(comment_like::comment_id)
186             .and(comment_like::person_id.eq(self.my_person_id)),
187         ),
188       )
189       .left_join(
190         person_alias_2
191           .on(comment_report::resolver_id.eq(person_alias_2.field(person::id).nullable())),
192       )
193       .select((
194         comment_report::all_columns,
195         comment::all_columns,
196         post::all_columns,
197         community::all_columns,
198         person::all_columns,
199         person_alias_1.fields(person::all_columns),
200         comment_aggregates::all_columns,
201         community_person_ban::all_columns.nullable(),
202         comment_like::score.nullable(),
203         person_alias_2.fields(person::all_columns).nullable(),
204       ))
205       .into_boxed();
206
207     if let Some(community_id) = self.community_id {
208       query = query.filter(post::community_id.eq(community_id));
209     }
210
211     if self.unresolved_only.unwrap_or(true) {
212       query = query.filter(comment_report::resolved.eq(false));
213     }
214
215     let (limit, offset) = limit_and_offset(self.page, self.limit)?;
216
217     query = query
218       .order_by(comment_report::published.desc())
219       .limit(limit)
220       .offset(offset);
221
222     // If its not an admin, get only the ones you mod
223     let res = if !self.admin {
224       query
225         .inner_join(
226           community_moderator::table.on(
227             community_moderator::community_id
228               .eq(post::community_id)
229               .and(community_moderator::person_id.eq(self.my_person_id)),
230           ),
231         )
232         .load::<<CommentReportView as JoinView>::JoinTuple>(conn)
233         .await?
234     } else {
235       query
236         .load::<<CommentReportView as JoinView>::JoinTuple>(conn)
237         .await?
238     };
239
240     Ok(res.into_iter().map(CommentReportView::from_tuple).collect())
241   }
242 }
243
244 impl JoinView for CommentReportView {
245   type JoinTuple = (
246     CommentReport,
247     Comment,
248     Post,
249     Community,
250     Person,
251     Person,
252     CommentAggregates,
253     Option<CommunityPersonBan>,
254     Option<i16>,
255     Option<Person>,
256   );
257
258   fn from_tuple(a: Self::JoinTuple) -> Self {
259     Self {
260       comment_report: a.0,
261       comment: a.1,
262       post: a.2,
263       community: a.3,
264       creator: a.4,
265       comment_creator: a.5,
266       counts: a.6,
267       creator_banned_from_community: a.7.is_some(),
268       my_vote: a.8,
269       resolver: a.9,
270     }
271   }
272 }
273
274 #[cfg(test)]
275 mod tests {
276   use crate::comment_report_view::{CommentReportQuery, CommentReportView};
277   use lemmy_db_schema::{
278     aggregates::structs::CommentAggregates,
279     source::{
280       comment::{Comment, CommentInsertForm},
281       comment_report::{CommentReport, CommentReportForm},
282       community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
283       instance::Instance,
284       person::{Person, PersonInsertForm},
285       post::{Post, PostInsertForm},
286     },
287     traits::{Crud, Joinable, Reportable},
288     utils::build_db_pool_for_tests,
289   };
290   use serial_test::serial;
291
292   #[tokio::test]
293   #[serial]
294   async fn test_crud() {
295     let pool = &build_db_pool_for_tests().await;
296
297     let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
298       .await
299       .unwrap();
300
301     let new_person = PersonInsertForm::builder()
302       .name("timmy_crv".into())
303       .public_key("pubkey".to_string())
304       .instance_id(inserted_instance.id)
305       .build();
306
307     let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
308
309     let new_person_2 = PersonInsertForm::builder()
310       .name("sara_crv".into())
311       .public_key("pubkey".to_string())
312       .instance_id(inserted_instance.id)
313       .build();
314
315     let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();
316
317     // Add a third person, since new ppl can only report something once.
318     let new_person_3 = PersonInsertForm::builder()
319       .name("jessica_crv".into())
320       .public_key("pubkey".to_string())
321       .instance_id(inserted_instance.id)
322       .build();
323
324     let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap();
325
326     let new_community = CommunityInsertForm::builder()
327       .name("test community crv".to_string())
328       .title("nada".to_owned())
329       .public_key("pubkey".to_string())
330       .instance_id(inserted_instance.id)
331       .build();
332
333     let inserted_community = Community::create(pool, &new_community).await.unwrap();
334
335     // Make timmy a mod
336     let timmy_moderator_form = CommunityModeratorForm {
337       community_id: inserted_community.id,
338       person_id: inserted_timmy.id,
339     };
340
341     let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form)
342       .await
343       .unwrap();
344
345     let new_post = PostInsertForm::builder()
346       .name("A test post crv".into())
347       .creator_id(inserted_timmy.id)
348       .community_id(inserted_community.id)
349       .build();
350
351     let inserted_post = Post::create(pool, &new_post).await.unwrap();
352
353     let comment_form = CommentInsertForm::builder()
354       .content("A test comment 32".into())
355       .creator_id(inserted_timmy.id)
356       .post_id(inserted_post.id)
357       .build();
358
359     let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
360
361     // sara reports
362     let sara_report_form = CommentReportForm {
363       creator_id: inserted_sara.id,
364       comment_id: inserted_comment.id,
365       original_comment_text: "this was it at time of creation".into(),
366       reason: "from sara".into(),
367     };
368
369     let inserted_sara_report = CommentReport::report(pool, &sara_report_form)
370       .await
371       .unwrap();
372
373     // jessica reports
374     let jessica_report_form = CommentReportForm {
375       creator_id: inserted_jessica.id,
376       comment_id: inserted_comment.id,
377       original_comment_text: "this was it at time of creation".into(),
378       reason: "from jessica".into(),
379     };
380
381     let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form)
382       .await
383       .unwrap();
384
385     let agg = CommentAggregates::read(pool, inserted_comment.id)
386       .await
387       .unwrap();
388
389     let read_jessica_report_view =
390       CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
391         .await
392         .unwrap();
393     let expected_jessica_report_view = CommentReportView {
394       comment_report: inserted_jessica_report.clone(),
395       comment: inserted_comment.clone(),
396       post: inserted_post,
397       community: Community {
398         id: inserted_community.id,
399         name: inserted_community.name,
400         icon: None,
401         removed: false,
402         deleted: false,
403         nsfw: false,
404         actor_id: inserted_community.actor_id.clone(),
405         local: true,
406         title: inserted_community.title,
407         description: None,
408         updated: None,
409         banner: None,
410         hidden: false,
411         posting_restricted_to_mods: false,
412         published: inserted_community.published,
413         private_key: inserted_community.private_key,
414         public_key: inserted_community.public_key,
415         last_refreshed_at: inserted_community.last_refreshed_at,
416         followers_url: inserted_community.followers_url,
417         inbox_url: inserted_community.inbox_url,
418         shared_inbox_url: inserted_community.shared_inbox_url,
419         moderators_url: inserted_community.moderators_url,
420         featured_url: inserted_community.featured_url,
421         instance_id: inserted_instance.id,
422       },
423       creator: Person {
424         id: inserted_jessica.id,
425         name: inserted_jessica.name,
426         display_name: None,
427         published: inserted_jessica.published,
428         avatar: None,
429         actor_id: inserted_jessica.actor_id.clone(),
430         local: true,
431         banned: false,
432         deleted: false,
433         admin: false,
434         bot_account: false,
435         bio: None,
436         banner: None,
437         updated: None,
438         inbox_url: inserted_jessica.inbox_url.clone(),
439         shared_inbox_url: None,
440         matrix_user_id: None,
441         ban_expires: None,
442         instance_id: inserted_instance.id,
443         private_key: inserted_jessica.private_key,
444         public_key: inserted_jessica.public_key,
445         last_refreshed_at: inserted_jessica.last_refreshed_at,
446       },
447       comment_creator: Person {
448         id: inserted_timmy.id,
449         name: inserted_timmy.name.clone(),
450         display_name: None,
451         published: inserted_timmy.published,
452         avatar: None,
453         actor_id: inserted_timmy.actor_id.clone(),
454         local: true,
455         banned: false,
456         deleted: false,
457         admin: false,
458         bot_account: false,
459         bio: None,
460         banner: None,
461         updated: None,
462         inbox_url: inserted_timmy.inbox_url.clone(),
463         shared_inbox_url: None,
464         matrix_user_id: None,
465         ban_expires: None,
466         instance_id: inserted_instance.id,
467         private_key: inserted_timmy.private_key.clone(),
468         public_key: inserted_timmy.public_key.clone(),
469         last_refreshed_at: inserted_timmy.last_refreshed_at,
470       },
471       creator_banned_from_community: false,
472       counts: CommentAggregates {
473         id: agg.id,
474         comment_id: inserted_comment.id,
475         score: 0,
476         upvotes: 0,
477         downvotes: 0,
478         published: agg.published,
479         child_count: 0,
480       },
481       my_vote: None,
482       resolver: None,
483     };
484
485     assert_eq!(read_jessica_report_view, expected_jessica_report_view);
486
487     let mut expected_sara_report_view = expected_jessica_report_view.clone();
488     expected_sara_report_view.comment_report = inserted_sara_report;
489     expected_sara_report_view.creator = Person {
490       id: inserted_sara.id,
491       name: inserted_sara.name,
492       display_name: None,
493       published: inserted_sara.published,
494       avatar: None,
495       actor_id: inserted_sara.actor_id.clone(),
496       local: true,
497       banned: false,
498       deleted: false,
499       admin: false,
500       bot_account: false,
501       bio: None,
502       banner: None,
503       updated: None,
504       inbox_url: inserted_sara.inbox_url.clone(),
505       shared_inbox_url: None,
506       matrix_user_id: None,
507       ban_expires: None,
508       instance_id: inserted_instance.id,
509       private_key: inserted_sara.private_key,
510       public_key: inserted_sara.public_key,
511       last_refreshed_at: inserted_sara.last_refreshed_at,
512     };
513
514     // Do a batch read of timmys reports
515     let reports = CommentReportQuery::builder()
516       .pool(pool)
517       .my_person_id(inserted_timmy.id)
518       .admin(false)
519       .build()
520       .list()
521       .await
522       .unwrap();
523
524     assert_eq!(
525       reports,
526       [
527         expected_jessica_report_view.clone(),
528         expected_sara_report_view.clone()
529       ]
530     );
531
532     // Make sure the counts are correct
533     let report_count = CommentReportView::get_report_count(pool, inserted_timmy.id, false, None)
534       .await
535       .unwrap();
536     assert_eq!(2, report_count);
537
538     // Try to resolve the report
539     CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id)
540       .await
541       .unwrap();
542     let read_jessica_report_view_after_resolve =
543       CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
544         .await
545         .unwrap();
546
547     let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view;
548     expected_jessica_report_view_after_resolve
549       .comment_report
550       .resolved = true;
551     expected_jessica_report_view_after_resolve
552       .comment_report
553       .resolver_id = Some(inserted_timmy.id);
554     expected_jessica_report_view_after_resolve
555       .comment_report
556       .updated = read_jessica_report_view_after_resolve
557       .comment_report
558       .updated;
559     expected_jessica_report_view_after_resolve.resolver = Some(Person {
560       id: inserted_timmy.id,
561       name: inserted_timmy.name.clone(),
562       display_name: None,
563       published: inserted_timmy.published,
564       avatar: None,
565       actor_id: inserted_timmy.actor_id.clone(),
566       local: true,
567       banned: false,
568       deleted: false,
569       admin: false,
570       bot_account: false,
571       bio: None,
572       banner: None,
573       updated: None,
574       inbox_url: inserted_timmy.inbox_url.clone(),
575       private_key: inserted_timmy.private_key.clone(),
576       public_key: inserted_timmy.public_key.clone(),
577       last_refreshed_at: inserted_timmy.last_refreshed_at,
578       shared_inbox_url: None,
579       matrix_user_id: None,
580       ban_expires: None,
581       instance_id: inserted_instance.id,
582     });
583
584     assert_eq!(
585       read_jessica_report_view_after_resolve,
586       expected_jessica_report_view_after_resolve
587     );
588
589     // Do a batch read of timmys reports
590     // It should only show saras, which is unresolved
591     let reports_after_resolve = CommentReportQuery::builder()
592       .pool(pool)
593       .my_person_id(inserted_timmy.id)
594       .admin(false)
595       .build()
596       .list()
597       .await
598       .unwrap();
599     assert_eq!(reports_after_resolve[0], expected_sara_report_view);
600     assert_eq!(reports_after_resolve.len(), 1);
601
602     // Make sure the counts are correct
603     let report_count_after_resolved =
604       CommentReportView::get_report_count(pool, inserted_timmy.id, false, None)
605         .await
606         .unwrap();
607     assert_eq!(1, report_count_after_resolved);
608
609     Person::delete(pool, inserted_timmy.id).await.unwrap();
610     Person::delete(pool, inserted_sara.id).await.unwrap();
611     Person::delete(pool, inserted_jessica.id).await.unwrap();
612     Community::delete(pool, inserted_community.id)
613       .await
614       .unwrap();
615     Instance::delete(pool, inserted_instance.id).await.unwrap();
616   }
617 }