]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
Still continuing on....
[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, CommunityPersonBan},
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<CommunityPersonBan>,
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 | SortType::NewComments => {
384         query.order_by(comment::published.desc())
385       }
386       SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
387       SortType::TopYear => query
388         .filter(comment::published.gt(now - 1.years()))
389         .order_by(comment_aggregates::score.desc()),
390       SortType::TopMonth => query
391         .filter(comment::published.gt(now - 1.months()))
392         .order_by(comment_aggregates::score.desc()),
393       SortType::TopWeek => query
394         .filter(comment::published.gt(now - 1.weeks()))
395         .order_by(comment_aggregates::score.desc()),
396       SortType::TopDay => query
397         .filter(comment::published.gt(now - 1.days()))
398         .order_by(comment_aggregates::score.desc()),
399     };
400
401     let (limit, offset) = limit_and_offset(self.page, self.limit);
402
403     // Note: deleted and removed comments are done on the front side
404     let res = query
405       .limit(limit)
406       .offset(offset)
407       .load::<CommentViewTuple>(self.conn)?;
408
409     Ok(CommentView::from_tuple_to_vec(res))
410   }
411 }
412
413 impl ViewToVec for CommentView {
414   type DbTuple = CommentViewTuple;
415   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
416     items
417       .iter()
418       .map(|a| Self {
419         comment: a.0.to_owned(),
420         creator: a.1.to_owned(),
421         recipient: a.3.to_owned(),
422         post: a.4.to_owned(),
423         community: a.5.to_owned(),
424         counts: a.6.to_owned(),
425         creator_banned_from_community: a.7.is_some(),
426         subscribed: a.8.is_some(),
427         saved: a.9.is_some(),
428         my_vote: a.10,
429       })
430       .collect::<Vec<Self>>()
431   }
432 }
433
434 #[cfg(test)]
435 mod tests {
436   use crate::comment_view::*;
437   use lemmy_db_queries::{
438     aggregates::comment_aggregates::CommentAggregates,
439     establish_unpooled_connection,
440     Crud,
441     Likeable,
442     ListingType,
443     SortType,
444   };
445   use lemmy_db_schema::source::{comment::*, community::*, post::*, user::*};
446
447   #[test]
448   fn test_crud() {
449     let conn = establish_unpooled_connection();
450
451     let new_user = UserForm {
452       name: "timmy".into(),
453       preferred_username: None,
454       password_encrypted: "nope".into(),
455       email: None,
456       matrix_user_id: None,
457       avatar: None,
458       banner: None,
459       admin: false,
460       banned: Some(false),
461       published: None,
462       updated: None,
463       show_nsfw: false,
464       theme: "browser".into(),
465       default_sort_type: SortType::Hot as i16,
466       default_listing_type: ListingType::Subscribed as i16,
467       lang: "browser".into(),
468       show_avatars: true,
469       send_notifications_to_email: false,
470       actor_id: None,
471       bio: None,
472       local: true,
473       private_key: None,
474       public_key: None,
475       last_refreshed_at: None,
476       inbox_url: None,
477       shared_inbox_url: None,
478     };
479
480     let inserted_user = User_::create(&conn, &new_user).unwrap();
481
482     let new_community = CommunityForm {
483       name: "test community 5".to_string(),
484       title: "nada".to_owned(),
485       description: None,
486       creator_id: inserted_user.id,
487       removed: None,
488       deleted: None,
489       updated: None,
490       nsfw: false,
491       actor_id: None,
492       local: true,
493       private_key: None,
494       public_key: None,
495       last_refreshed_at: None,
496       published: None,
497       icon: None,
498       banner: None,
499       followers_url: None,
500       inbox_url: None,
501       shared_inbox_url: None,
502     };
503
504     let inserted_community = Community::create(&conn, &new_community).unwrap();
505
506     let new_post = PostForm {
507       name: "A test post 2".into(),
508       creator_id: inserted_user.id,
509       url: None,
510       body: None,
511       community_id: inserted_community.id,
512       removed: None,
513       deleted: None,
514       locked: None,
515       stickied: None,
516       updated: None,
517       nsfw: false,
518       embed_title: None,
519       embed_description: None,
520       embed_html: None,
521       thumbnail_url: None,
522       ap_id: None,
523       local: true,
524       published: None,
525     };
526
527     let inserted_post = Post::create(&conn, &new_post).unwrap();
528
529     let comment_form = CommentForm {
530       content: "A test comment 32".into(),
531       creator_id: inserted_user.id,
532       post_id: inserted_post.id,
533       parent_id: None,
534       removed: None,
535       deleted: None,
536       read: None,
537       published: None,
538       updated: None,
539       ap_id: None,
540       local: true,
541     };
542
543     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
544
545     let comment_like_form = CommentLikeForm {
546       comment_id: inserted_comment.id,
547       post_id: inserted_post.id,
548       person_id: inserted_user.id,
549       score: 1,
550     };
551
552     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
553
554     let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
555
556     let expected_comment_view_no_user = CommentView {
557       creator_banned_from_community: false,
558       my_vote: None,
559       subscribed: false,
560       saved: false,
561       comment: Comment {
562         id: inserted_comment.id,
563         content: "A test comment 32".into(),
564         creator_id: inserted_user.id,
565         post_id: inserted_post.id,
566         parent_id: None,
567         removed: false,
568         deleted: false,
569         read: false,
570         published: inserted_comment.published,
571         ap_id: inserted_comment.ap_id,
572         updated: None,
573         local: true,
574       },
575       creator: UserSafe {
576         id: inserted_user.id,
577         name: "timmy".into(),
578         preferred_username: None,
579         published: inserted_user.published,
580         avatar: None,
581         actor_id: inserted_user.actor_id.to_owned(),
582         local: true,
583         banned: false,
584         deleted: false,
585         bio: None,
586         banner: None,
587         admin: false,
588         updated: None,
589         matrix_user_id: None,
590         inbox_url: inserted_user.inbox_url.to_owned(),
591         shared_inbox_url: None,
592       },
593       recipient: None,
594       post: Post {
595         id: inserted_post.id,
596         name: inserted_post.name.to_owned(),
597         creator_id: inserted_user.id,
598         url: None,
599         body: None,
600         published: inserted_post.published,
601         updated: None,
602         community_id: inserted_community.id,
603         removed: false,
604         deleted: false,
605         locked: false,
606         stickied: false,
607         nsfw: false,
608         embed_title: None,
609         embed_description: None,
610         embed_html: None,
611         thumbnail_url: None,
612         ap_id: inserted_post.ap_id.to_owned(),
613         local: true,
614       },
615       community: CommunitySafe {
616         id: inserted_community.id,
617         name: "test community 5".to_string(),
618         icon: None,
619         removed: false,
620         deleted: false,
621         nsfw: false,
622         actor_id: inserted_community.actor_id.to_owned(),
623         local: true,
624         title: "nada".to_owned(),
625         description: None,
626         creator_id: inserted_user.id,
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 }