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