]> Untitled Git - lemmy.git/blob - lemmy_db/src/views/comment_view.rs
Merge remote-tracking branch 'origin/split-db-workspace' into move_views_to_diesel_split
[lemmy.git] / lemmy_db / src / views / comment_view.rs
1 use crate::{
2   aggregates::comment_aggregates::CommentAggregates,
3   functions::hot_rank,
4   fuzzy_search,
5   limit_and_offset,
6   views::ViewToVec,
7   ListingType,
8   MaybeOptional,
9   SortType,
10   ToSafe,
11 };
12 use diesel::{result::Error, *};
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))
325         .filter(comment::deleted.eq(false))
326         .filter(comment::removed.eq(false));
327     }
328
329     if self.unread_only {
330       query = query.filter(comment::read.eq(false));
331     }
332
333     if let Some(creator_id) = self.creator_id {
334       query = query.filter(comment::creator_id.eq(creator_id));
335     };
336
337     if let Some(community_id) = self.community_id {
338       query = query.filter(post::community_id.eq(community_id));
339     }
340
341     if let Some(community_name) = self.community_name {
342       query = query
343         .filter(community::name.eq(community_name))
344         .filter(comment::local.eq(true));
345     }
346
347     if let Some(post_id) = self.post_id {
348       query = query.filter(comment::post_id.eq(post_id));
349     };
350
351     if let Some(search_term) = self.search_term {
352       query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
353     };
354
355     query = match self.listing_type {
356       // ListingType::Subscribed => query.filter(community_follower::subscribed.eq(true)),
357       ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)),
358       ListingType::Local => query.filter(community::local.eq(true)),
359       _ => query,
360     };
361
362     if self.saved_only {
363       query = query.filter(comment_saved::id.is_not_null());
364     }
365
366     query = match self.sort {
367       SortType::Hot | SortType::Active => query
368         .order_by(hot_rank(comment_aggregates::score, comment::published).desc())
369         .then_order_by(comment::published.desc()),
370       SortType::New => query.order_by(comment::published.desc()),
371       SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
372       SortType::TopYear => query
373         .filter(comment::published.gt(now - 1.years()))
374         .order_by(comment_aggregates::score.desc()),
375       SortType::TopMonth => query
376         .filter(comment::published.gt(now - 1.months()))
377         .order_by(comment_aggregates::score.desc()),
378       SortType::TopWeek => query
379         .filter(comment::published.gt(now - 1.weeks()))
380         .order_by(comment_aggregates::score.desc()),
381       SortType::TopDay => query
382         .filter(comment::published.gt(now - 1.days()))
383         .order_by(comment_aggregates::score.desc()),
384     };
385
386     let (limit, offset) = limit_and_offset(self.page, self.limit);
387
388     // Note: deleted and removed comments are done on the front side
389     let res = query
390       .limit(limit)
391       .offset(offset)
392       .load::<CommentViewTuple>(self.conn)?;
393
394     Ok(CommentView::to_vec(res))
395   }
396 }
397
398 impl ViewToVec for CommentView {
399   type DbTuple = CommentViewTuple;
400   fn to_vec(posts: Vec<Self::DbTuple>) -> Vec<Self> {
401     posts
402       .iter()
403       .map(|a| Self {
404         comment: a.0.to_owned(),
405         creator: a.1.to_owned(),
406         recipient: a.3.to_owned(),
407         post: a.4.to_owned(),
408         community: a.5.to_owned(),
409         counts: a.6.to_owned(),
410         creator_banned_from_community: a.7.is_some(),
411         subscribed: a.8.is_some(),
412         saved: a.9.is_some(),
413         my_vote: a.10,
414       })
415       .collect::<Vec<Self>>()
416   }
417 }
418
419 #[cfg(test)]
420 mod tests {
421   use crate::{tests::establish_unpooled_connection, views::comment_view::*, Crud, Likeable, *};
422   use lemmy_db_schema::source::{comment::*, community::*, post::*, user::*};
423
424   #[test]
425   fn test_crud() {
426     let conn = establish_unpooled_connection();
427
428     let new_user = UserForm {
429       name: "timmy".into(),
430       preferred_username: None,
431       password_encrypted: "nope".into(),
432       email: None,
433       matrix_user_id: None,
434       avatar: None,
435       banner: None,
436       admin: false,
437       banned: Some(false),
438       published: None,
439       updated: None,
440       show_nsfw: false,
441       theme: "browser".into(),
442       default_sort_type: SortType::Hot as i16,
443       default_listing_type: ListingType::Subscribed as i16,
444       lang: "browser".into(),
445       show_avatars: true,
446       send_notifications_to_email: false,
447       actor_id: None,
448       bio: None,
449       local: true,
450       private_key: None,
451       public_key: None,
452       last_refreshed_at: None,
453     };
454
455     let inserted_user = User_::create(&conn, &new_user).unwrap();
456
457     let new_community = CommunityForm {
458       name: "test community 5".to_string(),
459       title: "nada".to_owned(),
460       description: None,
461       category_id: 1,
462       creator_id: inserted_user.id,
463       removed: None,
464       deleted: None,
465       updated: None,
466       nsfw: false,
467       actor_id: None,
468       local: true,
469       private_key: None,
470       public_key: None,
471       last_refreshed_at: None,
472       published: None,
473       icon: None,
474       banner: None,
475     };
476
477     let inserted_community = Community::create(&conn, &new_community).unwrap();
478
479     let new_post = PostForm {
480       name: "A test post 2".into(),
481       creator_id: inserted_user.id,
482       url: None,
483       body: None,
484       community_id: inserted_community.id,
485       removed: None,
486       deleted: None,
487       locked: None,
488       stickied: None,
489       updated: None,
490       nsfw: false,
491       embed_title: None,
492       embed_description: None,
493       embed_html: None,
494       thumbnail_url: None,
495       ap_id: None,
496       local: true,
497       published: None,
498     };
499
500     let inserted_post = Post::create(&conn, &new_post).unwrap();
501
502     let comment_form = CommentForm {
503       content: "A test comment 32".into(),
504       creator_id: inserted_user.id,
505       post_id: inserted_post.id,
506       parent_id: None,
507       removed: None,
508       deleted: None,
509       read: None,
510       published: None,
511       updated: None,
512       ap_id: None,
513       local: true,
514     };
515
516     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
517
518     let comment_like_form = CommentLikeForm {
519       comment_id: inserted_comment.id,
520       post_id: inserted_post.id,
521       user_id: inserted_user.id,
522       score: 1,
523     };
524
525     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
526
527     let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
528
529     let expected_comment_view_no_user = CommentView {
530       creator_banned_from_community: false,
531       my_vote: None,
532       subscribed: false,
533       saved: false,
534       comment: Comment {
535         id: inserted_comment.id,
536         content: "A test comment 32".into(),
537         creator_id: inserted_user.id,
538         post_id: inserted_post.id,
539         parent_id: None,
540         removed: false,
541         deleted: false,
542         read: false,
543         published: inserted_comment.published,
544         ap_id: inserted_comment.ap_id,
545         updated: None,
546         local: true,
547       },
548       creator: UserSafe {
549         id: inserted_user.id,
550         name: "timmy".into(),
551         preferred_username: None,
552         published: inserted_user.published,
553         avatar: None,
554         actor_id: inserted_user.actor_id.to_owned(),
555         local: true,
556         banned: false,
557         deleted: false,
558         bio: None,
559         banner: None,
560         admin: false,
561         updated: None,
562         matrix_user_id: None,
563       },
564       recipient: None,
565       post: Post {
566         id: inserted_post.id,
567         name: inserted_post.name.to_owned(),
568         creator_id: inserted_user.id,
569         url: None,
570         body: None,
571         published: inserted_post.published,
572         updated: None,
573         community_id: inserted_community.id,
574         removed: false,
575         deleted: false,
576         locked: false,
577         stickied: false,
578         nsfw: false,
579         embed_title: None,
580         embed_description: None,
581         embed_html: None,
582         thumbnail_url: None,
583         ap_id: inserted_post.ap_id.to_owned(),
584         local: true,
585       },
586       community: CommunitySafe {
587         id: inserted_community.id,
588         name: "test community 5".to_string(),
589         icon: None,
590         removed: false,
591         deleted: false,
592         nsfw: false,
593         actor_id: inserted_community.actor_id.to_owned(),
594         local: true,
595         title: "nada".to_owned(),
596         description: None,
597         creator_id: inserted_user.id,
598         category_id: 1,
599         updated: None,
600         banner: None,
601         published: inserted_community.published,
602       },
603       counts: CommentAggregates {
604         id: agg.id,
605         comment_id: inserted_comment.id,
606         score: 1,
607         upvotes: 1,
608         downvotes: 0,
609       },
610     };
611
612     let mut expected_comment_view_with_user = expected_comment_view_no_user.to_owned();
613     expected_comment_view_with_user.my_vote = Some(1);
614
615     let read_comment_views_no_user = CommentQueryBuilder::create(&conn)
616       .post_id(inserted_post.id)
617       .list()
618       .unwrap();
619
620     let read_comment_views_with_user = CommentQueryBuilder::create(&conn)
621       .post_id(inserted_post.id)
622       .my_user_id(inserted_user.id)
623       .list()
624       .unwrap();
625
626     let like_removed = CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
627     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
628     Post::delete(&conn, inserted_post.id).unwrap();
629     Community::delete(&conn, inserted_community.id).unwrap();
630     User_::delete(&conn, inserted_user.id).unwrap();
631
632     assert_eq!(expected_comment_view_no_user, read_comment_views_no_user[0]);
633     assert_eq!(
634       expected_comment_view_with_user,
635       read_comment_views_with_user[0]
636     );
637     assert_eq!(1, num_deleted);
638     assert_eq!(1, like_removed);
639   }
640 }