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