]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
Merge pull request #1401 from LemmyNet/non_null_post_view_vote
[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 => 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     };
475
476     let inserted_user = User_::create(&conn, &new_user).unwrap();
477
478     let new_community = CommunityForm {
479       name: "test community 5".to_string(),
480       title: "nada".to_owned(),
481       description: None,
482       category_id: 1,
483       creator_id: inserted_user.id,
484       removed: None,
485       deleted: None,
486       updated: None,
487       nsfw: false,
488       actor_id: None,
489       local: true,
490       private_key: None,
491       public_key: None,
492       last_refreshed_at: None,
493       published: None,
494       icon: None,
495       banner: None,
496     };
497
498     let inserted_community = Community::create(&conn, &new_community).unwrap();
499
500     let new_post = PostForm {
501       name: "A test post 2".into(),
502       creator_id: inserted_user.id,
503       url: None,
504       body: None,
505       community_id: inserted_community.id,
506       removed: None,
507       deleted: None,
508       locked: None,
509       stickied: None,
510       updated: None,
511       nsfw: false,
512       embed_title: None,
513       embed_description: None,
514       embed_html: None,
515       thumbnail_url: None,
516       ap_id: None,
517       local: true,
518       published: None,
519     };
520
521     let inserted_post = Post::create(&conn, &new_post).unwrap();
522
523     let comment_form = CommentForm {
524       content: "A test comment 32".into(),
525       creator_id: inserted_user.id,
526       post_id: inserted_post.id,
527       parent_id: None,
528       removed: None,
529       deleted: None,
530       read: None,
531       published: None,
532       updated: None,
533       ap_id: None,
534       local: true,
535     };
536
537     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
538
539     let comment_like_form = CommentLikeForm {
540       comment_id: inserted_comment.id,
541       post_id: inserted_post.id,
542       user_id: inserted_user.id,
543       score: 1,
544     };
545
546     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
547
548     let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
549
550     let expected_comment_view_no_user = CommentView {
551       creator_banned_from_community: false,
552       my_vote: None,
553       subscribed: false,
554       saved: false,
555       comment: Comment {
556         id: inserted_comment.id,
557         content: "A test comment 32".into(),
558         creator_id: inserted_user.id,
559         post_id: inserted_post.id,
560         parent_id: None,
561         removed: false,
562         deleted: false,
563         read: false,
564         published: inserted_comment.published,
565         ap_id: inserted_comment.ap_id,
566         updated: None,
567         local: true,
568       },
569       creator: UserSafe {
570         id: inserted_user.id,
571         name: "timmy".into(),
572         preferred_username: None,
573         published: inserted_user.published,
574         avatar: None,
575         actor_id: inserted_user.actor_id.to_owned(),
576         local: true,
577         banned: false,
578         deleted: false,
579         bio: None,
580         banner: None,
581         admin: false,
582         updated: None,
583         matrix_user_id: None,
584       },
585       recipient: None,
586       post: Post {
587         id: inserted_post.id,
588         name: inserted_post.name.to_owned(),
589         creator_id: inserted_user.id,
590         url: None,
591         body: None,
592         published: inserted_post.published,
593         updated: None,
594         community_id: inserted_community.id,
595         removed: false,
596         deleted: false,
597         locked: false,
598         stickied: false,
599         nsfw: false,
600         embed_title: None,
601         embed_description: None,
602         embed_html: None,
603         thumbnail_url: None,
604         ap_id: inserted_post.ap_id.to_owned(),
605         local: true,
606       },
607       community: CommunitySafe {
608         id: inserted_community.id,
609         name: "test community 5".to_string(),
610         icon: None,
611         removed: false,
612         deleted: false,
613         nsfw: false,
614         actor_id: inserted_community.actor_id.to_owned(),
615         local: true,
616         title: "nada".to_owned(),
617         description: None,
618         creator_id: inserted_user.id,
619         category_id: 1,
620         updated: None,
621         banner: None,
622         published: inserted_community.published,
623       },
624       counts: CommentAggregates {
625         id: agg.id,
626         comment_id: inserted_comment.id,
627         score: 1,
628         upvotes: 1,
629         downvotes: 0,
630         published: agg.published,
631       },
632     };
633
634     let mut expected_comment_view_with_user = expected_comment_view_no_user.to_owned();
635     expected_comment_view_with_user.my_vote = Some(1);
636
637     let read_comment_views_no_user = CommentQueryBuilder::create(&conn)
638       .post_id(inserted_post.id)
639       .list()
640       .unwrap();
641
642     let read_comment_views_with_user = CommentQueryBuilder::create(&conn)
643       .post_id(inserted_post.id)
644       .my_user_id(inserted_user.id)
645       .list()
646       .unwrap();
647
648     let like_removed = CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
649     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
650     Post::delete(&conn, inserted_post.id).unwrap();
651     Community::delete(&conn, inserted_community.id).unwrap();
652     User_::delete(&conn, inserted_user.id).unwrap();
653
654     assert_eq!(expected_comment_view_no_user, read_comment_views_no_user[0]);
655     assert_eq!(
656       expected_comment_view_with_user,
657       read_comment_views_with_user[0]
658     );
659     assert_eq!(1, num_deleted);
660     assert_eq!(1, like_removed);
661   }
662 }