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