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