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