]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
Adding more site setup vars. Fixes #678 (#1718)
[lemmy.git] / crates / db_views / src / comment_view.rs
1 use diesel::{result::Error, *};
2 use lemmy_db_queries::{
3   aggregates::comment_aggregates::CommentAggregates,
4   functions::hot_rank,
5   fuzzy_search,
6   limit_and_offset,
7   ListingType,
8   MaybeOptional,
9   SortType,
10   ToSafe,
11   ViewToVec,
12 };
13 use lemmy_db_schema::{
14   schema::{
15     comment,
16     comment_aggregates,
17     comment_alias_1,
18     comment_like,
19     comment_saved,
20     community,
21     community_block,
22     community_follower,
23     community_person_ban,
24     person,
25     person_alias_1,
26     person_block,
27     post,
28   },
29   source::{
30     comment::{Comment, CommentAlias1, CommentSaved},
31     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
32     person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1},
33     person_block::PersonBlock,
34     post::Post,
35   },
36   CommentId,
37   CommunityId,
38   DbUrl,
39   PersonId,
40   PostId,
41 };
42 use serde::Serialize;
43
44 #[derive(Debug, PartialEq, Serialize, Clone)]
45 pub struct CommentView {
46   pub comment: Comment,
47   pub creator: PersonSafe,
48   pub recipient: Option<PersonSafeAlias1>, // Left joins to comment and person
49   pub post: Post,
50   pub community: CommunitySafe,
51   pub counts: CommentAggregates,
52   pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
53   pub subscribed: bool,                    // Left join to CommunityFollower
54   pub saved: bool,                         // Left join to CommentSaved
55   pub creator_blocked: bool,               // Left join to PersonBlock
56   pub my_vote: Option<i16>,                // Left join to CommentLike
57 }
58
59 type CommentViewTuple = (
60   Comment,
61   PersonSafe,
62   Option<CommentAlias1>,
63   Option<PersonSafeAlias1>,
64   Post,
65   CommunitySafe,
66   CommentAggregates,
67   Option<CommunityPersonBan>,
68   Option<CommunityFollower>,
69   Option<CommentSaved>,
70   Option<PersonBlock>,
71   Option<i16>,
72 );
73
74 impl CommentView {
75   pub fn read(
76     conn: &PgConnection,
77     comment_id: CommentId,
78     my_person_id: Option<PersonId>,
79   ) -> Result<Self, Error> {
80     // The left join below will return None in this case
81     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
82
83     let (
84       comment,
85       creator,
86       _parent_comment,
87       recipient,
88       post,
89       community,
90       counts,
91       creator_banned_from_community,
92       subscribed,
93       saved,
94       creator_blocked,
95       comment_like,
96     ) = comment::table
97       .find(comment_id)
98       .inner_join(person::table)
99       // recipient here
100       .left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
101       .left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
102       .inner_join(post::table)
103       .inner_join(community::table.on(post::community_id.eq(community::id)))
104       .inner_join(comment_aggregates::table)
105       .left_join(
106         community_person_ban::table.on(
107           community::id
108             .eq(community_person_ban::community_id)
109             .and(community_person_ban::person_id.eq(comment::creator_id)),
110         ),
111       )
112       .left_join(
113         community_follower::table.on(
114           post::community_id
115             .eq(community_follower::community_id)
116             .and(community_follower::person_id.eq(person_id_join)),
117         ),
118       )
119       .left_join(
120         comment_saved::table.on(
121           comment::id
122             .eq(comment_saved::comment_id)
123             .and(comment_saved::person_id.eq(person_id_join)),
124         ),
125       )
126       .left_join(
127         person_block::table.on(
128           comment::creator_id
129             .eq(person_block::target_id)
130             .and(person_block::person_id.eq(person_id_join)),
131         ),
132       )
133       .left_join(
134         comment_like::table.on(
135           comment::id
136             .eq(comment_like::comment_id)
137             .and(comment_like::person_id.eq(person_id_join)),
138         ),
139       )
140       .select((
141         comment::all_columns,
142         Person::safe_columns_tuple(),
143         comment_alias_1::all_columns.nullable(),
144         PersonAlias1::safe_columns_tuple().nullable(),
145         post::all_columns,
146         Community::safe_columns_tuple(),
147         comment_aggregates::all_columns,
148         community_person_ban::all_columns.nullable(),
149         community_follower::all_columns.nullable(),
150         comment_saved::all_columns.nullable(),
151         person_block::all_columns.nullable(),
152         comment_like::score.nullable(),
153       ))
154       .first::<CommentViewTuple>(conn)?;
155
156     // If a person is given, then my_vote, if None, should be 0, not null
157     // Necessary to differentiate between other person's votes
158     let my_vote = if my_person_id.is_some() && comment_like.is_none() {
159       Some(0)
160     } else {
161       comment_like
162     };
163
164     Ok(CommentView {
165       comment,
166       recipient,
167       post,
168       creator,
169       community,
170       counts,
171       creator_banned_from_community: creator_banned_from_community.is_some(),
172       subscribed: subscribed.is_some(),
173       saved: saved.is_some(),
174       creator_blocked: creator_blocked.is_some(),
175       my_vote,
176     })
177   }
178
179   /// Gets the recipient person id.
180   /// If there is no parent comment, its the post creator
181   pub fn get_recipient_id(&self) -> PersonId {
182     match &self.recipient {
183       Some(parent_commenter) => parent_commenter.id,
184       None => self.post.creator_id,
185     }
186   }
187 }
188
189 pub struct CommentQueryBuilder<'a> {
190   conn: &'a PgConnection,
191   listing_type: Option<ListingType>,
192   sort: Option<SortType>,
193   community_id: Option<CommunityId>,
194   community_actor_id: Option<DbUrl>,
195   post_id: Option<PostId>,
196   creator_id: Option<PersonId>,
197   recipient_id: Option<PersonId>,
198   my_person_id: Option<PersonId>,
199   search_term: Option<String>,
200   saved_only: Option<bool>,
201   unread_only: Option<bool>,
202   show_bot_accounts: Option<bool>,
203   page: Option<i64>,
204   limit: Option<i64>,
205 }
206
207 impl<'a> CommentQueryBuilder<'a> {
208   pub fn create(conn: &'a PgConnection) -> Self {
209     CommentQueryBuilder {
210       conn,
211       listing_type: None,
212       sort: None,
213       community_id: None,
214       community_actor_id: None,
215       post_id: None,
216       creator_id: None,
217       recipient_id: None,
218       my_person_id: None,
219       search_term: None,
220       saved_only: None,
221       unread_only: None,
222       show_bot_accounts: None,
223       page: None,
224       limit: None,
225     }
226   }
227
228   pub fn listing_type<T: MaybeOptional<ListingType>>(mut self, listing_type: T) -> Self {
229     self.listing_type = listing_type.get_optional();
230     self
231   }
232
233   pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self {
234     self.sort = sort.get_optional();
235     self
236   }
237
238   pub fn post_id<T: MaybeOptional<PostId>>(mut self, post_id: T) -> Self {
239     self.post_id = post_id.get_optional();
240     self
241   }
242
243   pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
244     self.creator_id = creator_id.get_optional();
245     self
246   }
247
248   pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self {
249     self.recipient_id = recipient_id.get_optional();
250     self
251   }
252
253   pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
254     self.community_id = community_id.get_optional();
255     self
256   }
257
258   pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
259     self.my_person_id = my_person_id.get_optional();
260     self
261   }
262
263   pub fn community_actor_id<T: MaybeOptional<DbUrl>>(mut self, community_actor_id: T) -> Self {
264     self.community_actor_id = community_actor_id.get_optional();
265     self
266   }
267
268   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
269     self.search_term = search_term.get_optional();
270     self
271   }
272
273   pub fn saved_only<T: MaybeOptional<bool>>(mut self, saved_only: T) -> Self {
274     self.saved_only = saved_only.get_optional();
275     self
276   }
277
278   pub fn unread_only<T: MaybeOptional<bool>>(mut self, unread_only: T) -> Self {
279     self.unread_only = unread_only.get_optional();
280     self
281   }
282
283   pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
284     self.show_bot_accounts = show_bot_accounts.get_optional();
285     self
286   }
287
288   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
289     self.page = page.get_optional();
290     self
291   }
292
293   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
294     self.limit = limit.get_optional();
295     self
296   }
297
298   pub fn list(self) -> Result<Vec<CommentView>, Error> {
299     use diesel::dsl::*;
300
301     // The left join below will return None in this case
302     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
303
304     let mut query = comment::table
305       .inner_join(person::table)
306       // recipient here
307       .left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
308       .left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
309       .inner_join(post::table)
310       .inner_join(community::table.on(post::community_id.eq(community::id)))
311       .inner_join(comment_aggregates::table)
312       .left_join(
313         community_person_ban::table.on(
314           community::id
315             .eq(community_person_ban::community_id)
316             .and(community_person_ban::person_id.eq(comment::creator_id)),
317         ),
318       )
319       .left_join(
320         community_follower::table.on(
321           post::community_id
322             .eq(community_follower::community_id)
323             .and(community_follower::person_id.eq(person_id_join)),
324         ),
325       )
326       .left_join(
327         comment_saved::table.on(
328           comment::id
329             .eq(comment_saved::comment_id)
330             .and(comment_saved::person_id.eq(person_id_join)),
331         ),
332       )
333       .left_join(
334         person_block::table.on(
335           comment::creator_id
336             .eq(person_block::target_id)
337             .and(person_block::person_id.eq(person_id_join)),
338         ),
339       )
340       .left_join(
341         community_block::table.on(
342           community::id
343             .eq(community_block::community_id)
344             .and(community_block::person_id.eq(person_id_join)),
345         ),
346       )
347       .left_join(
348         comment_like::table.on(
349           comment::id
350             .eq(comment_like::comment_id)
351             .and(comment_like::person_id.eq(person_id_join)),
352         ),
353       )
354       .select((
355         comment::all_columns,
356         Person::safe_columns_tuple(),
357         comment_alias_1::all_columns.nullable(),
358         PersonAlias1::safe_columns_tuple().nullable(),
359         post::all_columns,
360         Community::safe_columns_tuple(),
361         comment_aggregates::all_columns,
362         community_person_ban::all_columns.nullable(),
363         community_follower::all_columns.nullable(),
364         comment_saved::all_columns.nullable(),
365         person_block::all_columns.nullable(),
366         comment_like::score.nullable(),
367       ))
368       .into_boxed();
369
370     // The replies
371     if let Some(recipient_id) = self.recipient_id {
372       query = query
373         // TODO needs lots of testing
374         .filter(person_alias_1::id.eq(recipient_id)) // Gets the comment replies
375         .or_filter(
376           comment::parent_id
377             .is_null()
378             .and(post::creator_id.eq(recipient_id)),
379         ) // Gets the top level replies
380         .filter(comment::deleted.eq(false))
381         .filter(comment::removed.eq(false));
382     }
383
384     if self.unread_only.unwrap_or(false) {
385       query = query.filter(comment::read.eq(false));
386     }
387
388     if let Some(creator_id) = self.creator_id {
389       query = query.filter(comment::creator_id.eq(creator_id));
390     };
391
392     if let Some(community_id) = self.community_id {
393       query = query.filter(post::community_id.eq(community_id));
394     }
395
396     if let Some(community_actor_id) = self.community_actor_id {
397       query = query.filter(community::actor_id.eq(community_actor_id))
398     }
399
400     if let Some(post_id) = self.post_id {
401       query = query.filter(comment::post_id.eq(post_id));
402     };
403
404     if let Some(search_term) = self.search_term {
405       query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
406     };
407
408     if let Some(listing_type) = self.listing_type {
409       query = match listing_type {
410         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
411         ListingType::Local => query.filter(community::local.eq(true)),
412         _ => query,
413       };
414     }
415
416     if self.saved_only.unwrap_or(false) {
417       query = query.filter(comment_saved::id.is_not_null());
418     }
419
420     if !self.show_bot_accounts.unwrap_or(true) {
421       query = query.filter(person::bot_account.eq(false));
422     };
423
424     query = match self.sort.unwrap_or(SortType::New) {
425       SortType::Hot | SortType::Active => query
426         .order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
427         .then_order_by(comment_aggregates::published.desc()),
428       SortType::New | SortType::MostComments | SortType::NewComments => {
429         query.order_by(comment::published.desc())
430       }
431       SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
432       SortType::TopYear => query
433         .filter(comment::published.gt(now - 1.years()))
434         .order_by(comment_aggregates::score.desc()),
435       SortType::TopMonth => query
436         .filter(comment::published.gt(now - 1.months()))
437         .order_by(comment_aggregates::score.desc()),
438       SortType::TopWeek => query
439         .filter(comment::published.gt(now - 1.weeks()))
440         .order_by(comment_aggregates::score.desc()),
441       SortType::TopDay => query
442         .filter(comment::published.gt(now - 1.days()))
443         .order_by(comment_aggregates::score.desc()),
444     };
445
446     // Don't show blocked communities or persons
447     if self.my_person_id.is_some() {
448       query = query.filter(community_block::person_id.is_null());
449       query = query.filter(person_block::person_id.is_null());
450     }
451
452     let (limit, offset) = limit_and_offset(self.page, self.limit);
453
454     // Note: deleted and removed comments are done on the front side
455     let res = query
456       .limit(limit)
457       .offset(offset)
458       .load::<CommentViewTuple>(self.conn)?;
459
460     Ok(CommentView::from_tuple_to_vec(res))
461   }
462 }
463
464 impl ViewToVec for CommentView {
465   type DbTuple = CommentViewTuple;
466   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
467     items
468       .iter()
469       .map(|a| Self {
470         comment: a.0.to_owned(),
471         creator: a.1.to_owned(),
472         recipient: a.3.to_owned(),
473         post: a.4.to_owned(),
474         community: a.5.to_owned(),
475         counts: a.6.to_owned(),
476         creator_banned_from_community: a.7.is_some(),
477         subscribed: a.8.is_some(),
478         saved: a.9.is_some(),
479         creator_blocked: a.10.is_some(),
480         my_vote: a.11,
481       })
482       .collect::<Vec<Self>>()
483   }
484 }
485
486 #[cfg(test)]
487 mod tests {
488   use crate::comment_view::*;
489   use lemmy_db_queries::{
490     aggregates::comment_aggregates::CommentAggregates,
491     establish_unpooled_connection,
492     Blockable,
493     Crud,
494     Likeable,
495   };
496   use lemmy_db_schema::source::{
497     comment::*,
498     community::*,
499     person::*,
500     person_block::PersonBlockForm,
501     post::*,
502   };
503   use serial_test::serial;
504
505   #[test]
506   #[serial]
507   fn test_crud() {
508     let conn = establish_unpooled_connection();
509
510     let new_person = PersonForm {
511       name: "timmy".into(),
512       ..PersonForm::default()
513     };
514
515     let inserted_person = Person::create(&conn, &new_person).unwrap();
516
517     let new_person_2 = PersonForm {
518       name: "sara".into(),
519       ..PersonForm::default()
520     };
521
522     let inserted_person_2 = Person::create(&conn, &new_person_2).unwrap();
523
524     let new_community = CommunityForm {
525       name: "test community 5".to_string(),
526       title: "nada".to_owned(),
527       ..CommunityForm::default()
528     };
529
530     let inserted_community = Community::create(&conn, &new_community).unwrap();
531
532     let new_post = PostForm {
533       name: "A test post 2".into(),
534       creator_id: inserted_person.id,
535       community_id: inserted_community.id,
536       ..PostForm::default()
537     };
538
539     let inserted_post = Post::create(&conn, &new_post).unwrap();
540
541     let comment_form = CommentForm {
542       content: "A test comment 32".into(),
543       creator_id: inserted_person.id,
544       post_id: inserted_post.id,
545       ..CommentForm::default()
546     };
547
548     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
549
550     let comment_form_2 = CommentForm {
551       content: "A test blocked comment".into(),
552       creator_id: inserted_person_2.id,
553       post_id: inserted_post.id,
554       parent_id: Some(inserted_comment.id),
555       ..CommentForm::default()
556     };
557
558     let inserted_comment_2 = Comment::create(&conn, &comment_form_2).unwrap();
559
560     let timmy_blocks_sara_form = PersonBlockForm {
561       person_id: inserted_person.id,
562       target_id: inserted_person_2.id,
563     };
564
565     let inserted_block = PersonBlock::block(&conn, &timmy_blocks_sara_form).unwrap();
566
567     let expected_block = PersonBlock {
568       id: inserted_block.id,
569       person_id: inserted_person.id,
570       target_id: inserted_person_2.id,
571       published: inserted_block.published,
572     };
573
574     assert_eq!(expected_block, inserted_block);
575
576     let comment_like_form = CommentLikeForm {
577       comment_id: inserted_comment.id,
578       post_id: inserted_post.id,
579       person_id: inserted_person.id,
580       score: 1,
581     };
582
583     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
584
585     let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
586
587     let expected_comment_view_no_person = CommentView {
588       creator_banned_from_community: false,
589       my_vote: None,
590       subscribed: false,
591       saved: false,
592       creator_blocked: false,
593       comment: Comment {
594         id: inserted_comment.id,
595         content: "A test comment 32".into(),
596         creator_id: inserted_person.id,
597         post_id: inserted_post.id,
598         parent_id: None,
599         removed: false,
600         deleted: false,
601         read: false,
602         published: inserted_comment.published,
603         ap_id: inserted_comment.ap_id,
604         updated: None,
605         local: true,
606       },
607       creator: PersonSafe {
608         id: inserted_person.id,
609         name: "timmy".into(),
610         display_name: None,
611         published: inserted_person.published,
612         avatar: None,
613         actor_id: inserted_person.actor_id.to_owned(),
614         local: true,
615         banned: false,
616         deleted: false,
617         admin: false,
618         bot_account: false,
619         bio: None,
620         banner: None,
621         updated: None,
622         inbox_url: inserted_person.inbox_url.to_owned(),
623         shared_inbox_url: None,
624         matrix_user_id: None,
625       },
626       recipient: None,
627       post: Post {
628         id: inserted_post.id,
629         name: inserted_post.name.to_owned(),
630         creator_id: inserted_person.id,
631         url: None,
632         body: None,
633         published: inserted_post.published,
634         updated: None,
635         community_id: inserted_community.id,
636         removed: false,
637         deleted: false,
638         locked: false,
639         stickied: false,
640         nsfw: false,
641         embed_title: None,
642         embed_description: None,
643         embed_html: None,
644         thumbnail_url: None,
645         ap_id: inserted_post.ap_id.to_owned(),
646         local: true,
647       },
648       community: CommunitySafe {
649         id: inserted_community.id,
650         name: "test community 5".to_string(),
651         icon: None,
652         removed: false,
653         deleted: false,
654         nsfw: false,
655         actor_id: inserted_community.actor_id.to_owned(),
656         local: true,
657         title: "nada".to_owned(),
658         description: None,
659         updated: None,
660         banner: None,
661         published: inserted_community.published,
662       },
663       counts: CommentAggregates {
664         id: agg.id,
665         comment_id: inserted_comment.id,
666         score: 1,
667         upvotes: 1,
668         downvotes: 0,
669         published: agg.published,
670       },
671     };
672
673     let mut expected_comment_view_with_person = expected_comment_view_no_person.to_owned();
674     expected_comment_view_with_person.my_vote = Some(1);
675
676     let read_comment_views_no_person = CommentQueryBuilder::create(&conn)
677       .post_id(inserted_post.id)
678       .list()
679       .unwrap();
680
681     let read_comment_views_with_person = CommentQueryBuilder::create(&conn)
682       .post_id(inserted_post.id)
683       .my_person_id(inserted_person.id)
684       .list()
685       .unwrap();
686
687     let read_comment_from_blocked_person =
688       CommentView::read(&conn, inserted_comment_2.id, Some(inserted_person.id)).unwrap();
689
690     let like_removed = CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
691     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
692     Comment::delete(&conn, inserted_comment_2.id).unwrap();
693     Post::delete(&conn, inserted_post.id).unwrap();
694     Community::delete(&conn, inserted_community.id).unwrap();
695     Person::delete(&conn, inserted_person.id).unwrap();
696     Person::delete(&conn, inserted_person_2.id).unwrap();
697
698     // Make sure its 1, not showing the blocked comment
699     assert_eq!(1, read_comment_views_with_person.len());
700
701     assert_eq!(
702       expected_comment_view_no_person,
703       read_comment_views_no_person[1]
704     );
705     assert_eq!(
706       expected_comment_view_with_person,
707       read_comment_views_with_person[0]
708     );
709
710     // Make sure block set the creator blocked
711     assert!(read_comment_from_blocked_person.creator_blocked);
712
713     assert_eq!(1, num_deleted);
714     assert_eq!(1, like_removed);
715   }
716 }