]> Untitled Git - lemmy.git/blob - lemmy_db/src/views/post_view.rs
4888f9cfbc63f1bbe9d7c9696f5d72d2e5eaf0f1
[lemmy.git] / lemmy_db / src / views / post_view.rs
1 use crate::{
2   aggregates::post_aggregates::PostAggregates,
3   functions::hot_rank,
4   fuzzy_search,
5   limit_and_offset,
6   schema::{
7     community,
8     community_follower,
9     community_user_ban,
10     post,
11     post_aggregates,
12     post_like,
13     post_read,
14     post_saved,
15     user_,
16   },
17   source::{
18     community::{Community, CommunityFollower, CommunitySafe, CommunityUserBan},
19     post::{Post, PostRead, PostSaved},
20     user::{UserSafe, User_},
21   },
22   views::ViewToVec,
23   ListingType,
24   MaybeOptional,
25   SortType,
26   ToSafe,
27 };
28 use diesel::{result::Error, *};
29 use serde::Serialize;
30
31 #[derive(Debug, PartialEq, Serialize, Clone)]
32 pub struct PostView {
33   pub post: Post,
34   pub creator: UserSafe,
35   pub community: CommunitySafe,
36   pub counts: PostAggregates,
37   pub subscribed: bool,                    // Left join to CommunityFollower
38   pub creator_banned_from_community: bool, // Left Join to CommunityUserBan
39   pub saved: bool,                         // Left join to PostSaved
40   pub read: bool,                          // Left join to PostRead
41   pub my_vote: Option<i16>,                // Left join to PostLike
42 }
43
44 type PostViewTuple = (
45   Post,
46   UserSafe,
47   CommunitySafe,
48   Option<CommunityUserBan>,
49   PostAggregates,
50   Option<CommunityFollower>,
51   Option<PostSaved>,
52   Option<PostRead>,
53   Option<i16>,
54 );
55
56 impl PostView {
57   pub fn read(conn: &PgConnection, post_id: i32, my_user_id: Option<i32>) -> Result<Self, Error> {
58     // The left join below will return None in this case
59     let user_id_join = my_user_id.unwrap_or(-1);
60
61     let (
62       post,
63       creator,
64       community,
65       creator_banned_from_community,
66       counts,
67       follower,
68       saved,
69       read,
70       my_vote,
71     ) = post::table
72       .find(post_id)
73       .inner_join(user_::table)
74       .inner_join(community::table)
75       .left_join(
76         community_user_ban::table.on(
77           post::community_id
78             .eq(community_user_ban::community_id)
79             .and(community_user_ban::user_id.eq(community::creator_id)),
80         ),
81       )
82       .inner_join(post_aggregates::table)
83       .left_join(
84         community_follower::table.on(
85           post::community_id
86             .eq(community_follower::community_id)
87             .and(community_follower::user_id.eq(user_id_join)),
88         ),
89       )
90       .left_join(
91         post_saved::table.on(
92           post::id
93             .eq(post_saved::post_id)
94             .and(post_saved::user_id.eq(user_id_join)),
95         ),
96       )
97       .left_join(
98         post_read::table.on(
99           post::id
100             .eq(post_read::post_id)
101             .and(post_read::user_id.eq(user_id_join)),
102         ),
103       )
104       .left_join(
105         post_like::table.on(
106           post::id
107             .eq(post_like::post_id)
108             .and(post_like::user_id.eq(user_id_join)),
109         ),
110       )
111       .select((
112         post::all_columns,
113         User_::safe_columns_tuple(),
114         Community::safe_columns_tuple(),
115         community_user_ban::all_columns.nullable(),
116         post_aggregates::all_columns,
117         community_follower::all_columns.nullable(),
118         post_saved::all_columns.nullable(),
119         post_read::all_columns.nullable(),
120         post_like::score.nullable(),
121       ))
122       .first::<PostViewTuple>(conn)?;
123
124     Ok(PostView {
125       post,
126       creator,
127       community,
128       creator_banned_from_community: creator_banned_from_community.is_some(),
129       counts,
130       subscribed: follower.is_some(),
131       saved: saved.is_some(),
132       read: read.is_some(),
133       my_vote,
134     })
135   }
136 }
137
138 mod join_types {
139   use crate::schema::{
140     community,
141     community_follower,
142     community_user_ban,
143     post,
144     post_aggregates,
145     post_like,
146     post_read,
147     post_saved,
148     user_,
149   };
150   use diesel::{
151     pg::Pg,
152     query_builder::BoxedSelectStatement,
153     query_source::joins::{Inner, Join, JoinOn, LeftOuter},
154     sql_types::*,
155   };
156
157   // /// TODO awful, but necessary because of the boxed join
158   pub(super) type BoxedPostJoin<'a> = BoxedSelectStatement<
159     'a,
160     (
161       (
162         Integer,
163         Text,
164         Nullable<Text>,
165         Nullable<Text>,
166         Integer,
167         Integer,
168         Bool,
169         Bool,
170         Timestamp,
171         Nullable<Timestamp>,
172         Bool,
173         Bool,
174         Bool,
175         Nullable<Text>,
176         Nullable<Text>,
177         Nullable<Text>,
178         Nullable<Text>,
179         Text,
180         Bool,
181       ),
182       (
183         Integer,
184         Text,
185         Nullable<Text>,
186         Nullable<Text>,
187         Bool,
188         Bool,
189         Timestamp,
190         Nullable<Timestamp>,
191         Nullable<Text>,
192         Text,
193         Nullable<Text>,
194         Bool,
195         Nullable<Text>,
196         Bool,
197       ),
198       (
199         Integer,
200         Text,
201         Text,
202         Nullable<Text>,
203         Integer,
204         Integer,
205         Bool,
206         Timestamp,
207         Nullable<Timestamp>,
208         Bool,
209         Bool,
210         Text,
211         Bool,
212         Nullable<Text>,
213         Nullable<Text>,
214       ),
215       Nullable<(Integer, Integer, Integer, Timestamp)>,
216       (Integer, Integer, BigInt, BigInt, BigInt, BigInt, Timestamp),
217       Nullable<(Integer, Integer, Integer, Timestamp, Nullable<Bool>)>,
218       Nullable<(Integer, Integer, Integer, Timestamp)>,
219       Nullable<(Integer, Integer, Integer, Timestamp)>,
220       Nullable<SmallInt>,
221     ),
222     JoinOn<
223       Join<
224         JoinOn<
225           Join<
226             JoinOn<
227               Join<
228                 JoinOn<
229                   Join<
230                     JoinOn<
231                       Join<
232                         JoinOn<
233                           Join<
234                             JoinOn<
235                               Join<
236                                 JoinOn<
237                                   Join<post::table, user_::table, Inner>,
238                                   diesel::expression::operators::Eq<
239                                     diesel::expression::nullable::Nullable<
240                                       post::columns::creator_id,
241                                     >,
242                                     diesel::expression::nullable::Nullable<user_::columns::id>,
243                                   >,
244                                 >,
245                                 community::table,
246                                 Inner,
247                               >,
248                               diesel::expression::operators::Eq<
249                                 diesel::expression::nullable::Nullable<post::columns::community_id>,
250                                 diesel::expression::nullable::Nullable<community::columns::id>,
251                               >,
252                             >,
253                             community_user_ban::table,
254                             LeftOuter,
255                           >,
256                           diesel::expression::operators::And<
257                             diesel::expression::operators::Eq<
258                               post::columns::community_id,
259                               community_user_ban::columns::community_id,
260                             >,
261                             diesel::expression::operators::Eq<
262                               community_user_ban::columns::user_id,
263                               community::columns::creator_id,
264                             >,
265                           >,
266                         >,
267                         post_aggregates::table,
268                         Inner,
269                       >,
270                       diesel::expression::operators::Eq<
271                         diesel::expression::nullable::Nullable<post_aggregates::columns::post_id>,
272                         diesel::expression::nullable::Nullable<post::columns::id>,
273                       >,
274                     >,
275                     community_follower::table,
276                     LeftOuter,
277                   >,
278                   diesel::expression::operators::And<
279                     diesel::expression::operators::Eq<
280                       post::columns::community_id,
281                       community_follower::columns::community_id,
282                     >,
283                     diesel::expression::operators::Eq<
284                       community_follower::columns::user_id,
285                       diesel::expression::bound::Bound<Integer, i32>,
286                     >,
287                   >,
288                 >,
289                 post_saved::table,
290                 LeftOuter,
291               >,
292               diesel::expression::operators::And<
293                 diesel::expression::operators::Eq<post::columns::id, post_saved::columns::post_id>,
294                 diesel::expression::operators::Eq<
295                   post_saved::columns::user_id,
296                   diesel::expression::bound::Bound<Integer, i32>,
297                 >,
298               >,
299             >,
300             post_read::table,
301             LeftOuter,
302           >,
303           diesel::expression::operators::And<
304             diesel::expression::operators::Eq<post::columns::id, post_read::columns::post_id>,
305             diesel::expression::operators::Eq<
306               post_read::columns::user_id,
307               diesel::expression::bound::Bound<Integer, i32>,
308             >,
309           >,
310         >,
311         post_like::table,
312         LeftOuter,
313       >,
314       diesel::expression::operators::And<
315         diesel::expression::operators::Eq<post::columns::id, post_like::columns::post_id>,
316         diesel::expression::operators::Eq<
317           post_like::columns::user_id,
318           diesel::expression::bound::Bound<Integer, i32>,
319         >,
320       >,
321     >,
322     Pg,
323   >;
324 }
325
326 pub struct PostQueryBuilder<'a> {
327   conn: &'a PgConnection,
328   query: join_types::BoxedPostJoin<'a>,
329   listing_type: &'a ListingType,
330   sort: &'a SortType,
331   for_creator_id: Option<i32>,
332   for_community_id: Option<i32>,
333   for_community_name: Option<String>,
334   search_term: Option<String>,
335   url_search: Option<String>,
336   show_nsfw: bool,
337   saved_only: bool,
338   unread_only: bool,
339   page: Option<i64>,
340   limit: Option<i64>,
341 }
342
343 impl<'a> PostQueryBuilder<'a> {
344   pub fn create(conn: &'a PgConnection, my_user_id: Option<i32>) -> Self {
345     // The left join below will return None in this case
346     let user_id_join = my_user_id.unwrap_or(-1);
347
348     let query = post::table
349       .inner_join(user_::table)
350       .inner_join(community::table)
351       .left_join(
352         community_user_ban::table.on(
353           post::community_id
354             .eq(community_user_ban::community_id)
355             .and(community_user_ban::user_id.eq(community::creator_id)),
356         ),
357       )
358       .inner_join(post_aggregates::table)
359       .left_join(
360         community_follower::table.on(
361           post::community_id
362             .eq(community_follower::community_id)
363             .and(community_follower::user_id.eq(user_id_join)),
364         ),
365       )
366       .left_join(
367         post_saved::table.on(
368           post::id
369             .eq(post_saved::post_id)
370             .and(post_saved::user_id.eq(user_id_join)),
371         ),
372       )
373       .left_join(
374         post_read::table.on(
375           post::id
376             .eq(post_read::post_id)
377             .and(post_read::user_id.eq(user_id_join)),
378         ),
379       )
380       .left_join(
381         post_like::table.on(
382           post::id
383             .eq(post_like::post_id)
384             .and(post_like::user_id.eq(user_id_join)),
385         ),
386       )
387       .select((
388         post::all_columns,
389         User_::safe_columns_tuple(),
390         Community::safe_columns_tuple(),
391         community_user_ban::all_columns.nullable(),
392         post_aggregates::all_columns,
393         community_follower::all_columns.nullable(),
394         post_saved::all_columns.nullable(),
395         post_read::all_columns.nullable(),
396         post_like::score.nullable(),
397       ))
398       .into_boxed();
399
400     PostQueryBuilder {
401       conn,
402       query,
403       listing_type: &ListingType::All,
404       sort: &SortType::Hot,
405       for_creator_id: None,
406       for_community_id: None,
407       for_community_name: None,
408       search_term: None,
409       url_search: None,
410       show_nsfw: true,
411       saved_only: false,
412       unread_only: false,
413       page: None,
414       limit: None,
415     }
416   }
417
418   pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
419     self.listing_type = listing_type;
420     self
421   }
422
423   pub fn sort(mut self, sort: &'a SortType) -> Self {
424     self.sort = sort;
425     self
426   }
427
428   pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
429     self.for_community_id = for_community_id.get_optional();
430     self
431   }
432
433   pub fn for_community_name<T: MaybeOptional<String>>(mut self, for_community_name: T) -> Self {
434     self.for_community_name = for_community_name.get_optional();
435     self
436   }
437
438   pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
439     self.for_creator_id = for_creator_id.get_optional();
440     self
441   }
442
443   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
444     self.search_term = search_term.get_optional();
445     self
446   }
447
448   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
449     self.url_search = url_search.get_optional();
450     self
451   }
452
453   pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
454     self.show_nsfw = show_nsfw;
455     self
456   }
457
458   pub fn saved_only(mut self, saved_only: bool) -> Self {
459     self.saved_only = saved_only;
460     self
461   }
462
463   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
464     self.page = page.get_optional();
465     self
466   }
467
468   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
469     self.limit = limit.get_optional();
470     self
471   }
472
473   pub fn list(self) -> Result<Vec<PostView>, Error> {
474     use diesel::dsl::*;
475
476     let mut query = self.query;
477
478     query = match self.listing_type {
479       ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)),
480       ListingType::Local => query.filter(community::local.eq(true)),
481       _ => query,
482     };
483
484     if let Some(for_community_id) = self.for_community_id {
485       query = query
486         .filter(post::community_id.eq(for_community_id))
487         .then_order_by(post::stickied.desc());
488     }
489
490     if let Some(for_community_name) = self.for_community_name {
491       query = query
492         .filter(community::name.eq(for_community_name))
493         .filter(community::local.eq(true))
494         .then_order_by(post::stickied.desc());
495     }
496
497     if let Some(url_search) = self.url_search {
498       query = query.filter(post::url.eq(url_search));
499     }
500
501     if let Some(search_term) = self.search_term {
502       let searcher = fuzzy_search(&search_term);
503       query = query.filter(
504         post::name
505           .ilike(searcher.to_owned())
506           .or(post::body.ilike(searcher)),
507       );
508     }
509
510     query = match self.sort {
511       SortType::Active => query
512         .then_order_by(
513           hot_rank(post_aggregates::score, post_aggregates::newest_comment_time).desc(),
514         )
515         .then_order_by(post::published.desc()),
516       SortType::Hot => query
517         .then_order_by(hot_rank(post_aggregates::score, post::published).desc())
518         .then_order_by(post::published.desc()),
519       SortType::New => query.then_order_by(post::published.desc()),
520       SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
521       SortType::TopYear => query
522         .filter(post::published.gt(now - 1.years()))
523         .then_order_by(post_aggregates::score.desc()),
524       SortType::TopMonth => query
525         .filter(post::published.gt(now - 1.months()))
526         .then_order_by(post_aggregates::score.desc()),
527       SortType::TopWeek => query
528         .filter(post::published.gt(now - 1.weeks()))
529         .then_order_by(post_aggregates::score.desc()),
530       SortType::TopDay => query
531         .filter(post::published.gt(now - 1.days()))
532         .then_order_by(post_aggregates::score.desc()),
533     };
534
535     // If its for a specific user, show the removed / deleted
536     if let Some(for_creator_id) = self.for_creator_id {
537       query = query.filter(post::creator_id.eq(for_creator_id));
538     }
539
540     if !self.show_nsfw {
541       query = query
542         .filter(post::nsfw.eq(false))
543         .filter(community::nsfw.eq(false));
544     };
545
546     // TODO  These two might be wrong
547     if self.saved_only {
548       query = query.filter(post_saved::id.is_not_null());
549     };
550
551     if self.unread_only {
552       query = query.filter(post_read::id.is_not_null());
553     };
554
555     let (limit, offset) = limit_and_offset(self.page, self.limit);
556
557     let res = query
558       .limit(limit)
559       .offset(offset)
560       .filter(post::removed.eq(false))
561       .filter(post::deleted.eq(false))
562       .filter(community::removed.eq(false))
563       .filter(community::deleted.eq(false))
564       .load::<PostViewTuple>(self.conn)?;
565
566     Ok(PostView::to_vec(res))
567   }
568 }
569
570 impl ViewToVec for PostView {
571   type DbTuple = PostViewTuple;
572   fn to_vec(posts: Vec<Self::DbTuple>) -> Vec<Self> {
573     posts
574       .iter()
575       .map(|a| Self {
576         post: a.0.to_owned(),
577         creator: a.1.to_owned(),
578         community: a.2.to_owned(),
579         creator_banned_from_community: a.3.is_some(),
580         counts: a.4.to_owned(),
581         subscribed: a.5.is_some(),
582         saved: a.6.is_some(),
583         read: a.7.is_some(),
584         my_vote: a.8,
585       })
586       .collect::<Vec<Self>>()
587   }
588 }
589
590 #[cfg(test)]
591 mod tests {
592   use crate::{
593     aggregates::post_aggregates::PostAggregates,
594     source::{community::*, post::*, user::*},
595     tests::establish_unpooled_connection,
596     views::post_view::{PostQueryBuilder, PostView},
597     Crud,
598     Likeable,
599     *,
600   };
601
602   #[test]
603   fn test_crud() {
604     let conn = establish_unpooled_connection();
605
606     let user_name = "tegan".to_string();
607     let community_name = "test_community_3".to_string();
608     let post_name = "test post 3".to_string();
609
610     let new_user = UserForm {
611       name: user_name.to_owned(),
612       preferred_username: None,
613       password_encrypted: "nope".into(),
614       email: None,
615       matrix_user_id: None,
616       avatar: None,
617       banner: None,
618       published: None,
619       updated: None,
620       admin: false,
621       banned: Some(false),
622       show_nsfw: false,
623       theme: "browser".into(),
624       default_sort_type: SortType::Hot as i16,
625       default_listing_type: ListingType::Subscribed as i16,
626       lang: "browser".into(),
627       show_avatars: true,
628       send_notifications_to_email: false,
629       actor_id: None,
630       bio: None,
631       local: true,
632       private_key: None,
633       public_key: None,
634       last_refreshed_at: None,
635     };
636
637     let inserted_user = User_::create(&conn, &new_user).unwrap();
638
639     let new_community = CommunityForm {
640       name: community_name.to_owned(),
641       title: "nada".to_owned(),
642       description: None,
643       creator_id: inserted_user.id,
644       category_id: 1,
645       removed: None,
646       deleted: None,
647       updated: None,
648       nsfw: false,
649       actor_id: None,
650       local: true,
651       private_key: None,
652       public_key: None,
653       last_refreshed_at: None,
654       published: None,
655       icon: None,
656       banner: None,
657     };
658
659     let inserted_community = Community::create(&conn, &new_community).unwrap();
660
661     let new_post = PostForm {
662       name: post_name.to_owned(),
663       url: None,
664       body: None,
665       creator_id: inserted_user.id,
666       community_id: inserted_community.id,
667       removed: None,
668       deleted: None,
669       locked: None,
670       stickied: None,
671       updated: None,
672       nsfw: false,
673       embed_title: None,
674       embed_description: None,
675       embed_html: None,
676       thumbnail_url: None,
677       ap_id: None,
678       local: true,
679       published: None,
680     };
681
682     let inserted_post = Post::create(&conn, &new_post).unwrap();
683
684     let post_like_form = PostLikeForm {
685       post_id: inserted_post.id,
686       user_id: inserted_user.id,
687       score: 1,
688     };
689
690     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
691
692     let expected_post_like = PostLike {
693       id: inserted_post_like.id,
694       post_id: inserted_post.id,
695       user_id: inserted_user.id,
696       published: inserted_post_like.published,
697       score: 1,
698     };
699
700     let read_post_listings_with_user = PostQueryBuilder::create(&conn, Some(inserted_user.id))
701       .listing_type(&ListingType::Community)
702       .sort(&SortType::New)
703       .for_community_id(inserted_community.id)
704       .list()
705       .unwrap();
706
707     let read_post_listings_no_user = PostQueryBuilder::create(&conn, None)
708       .listing_type(&ListingType::Community)
709       .sort(&SortType::New)
710       .for_community_id(inserted_community.id)
711       .list()
712       .unwrap();
713
714     let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
715     let read_post_listing_with_user =
716       PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
717
718     // the non user version
719     let expected_post_listing_no_user = PostView {
720       post: Post {
721         id: inserted_post.id,
722         name: post_name.to_owned(),
723         creator_id: inserted_user.id,
724         url: None,
725         body: None,
726         published: inserted_post.published,
727         updated: None,
728         community_id: inserted_community.id,
729         removed: false,
730         deleted: false,
731         locked: false,
732         stickied: false,
733         nsfw: false,
734         embed_title: None,
735         embed_description: None,
736         embed_html: None,
737         thumbnail_url: None,
738         ap_id: inserted_post.ap_id.to_owned(),
739         local: true,
740       },
741       my_vote: None,
742       creator: UserSafe {
743         id: inserted_user.id,
744         name: user_name.to_owned(),
745         preferred_username: None,
746         published: inserted_user.published,
747         avatar: None,
748         actor_id: inserted_user.actor_id.to_owned(),
749         local: true,
750         banned: false,
751         deleted: false,
752         bio: None,
753         banner: None,
754         admin: false,
755         updated: None,
756         matrix_user_id: None,
757       },
758       creator_banned_from_community: false,
759       community: CommunitySafe {
760         id: inserted_community.id,
761         name: community_name.to_owned(),
762         icon: None,
763         removed: false,
764         deleted: false,
765         nsfw: false,
766         actor_id: inserted_community.actor_id.to_owned(),
767         local: true,
768         title: "nada".to_owned(),
769         description: None,
770         creator_id: inserted_user.id,
771         category_id: 1,
772         updated: None,
773         banner: None,
774         published: inserted_community.published,
775       },
776       counts: PostAggregates {
777         id: inserted_post.id, // TODO this might fail
778         post_id: inserted_post.id,
779         comments: 0,
780         score: 1,
781         upvotes: 1,
782         downvotes: 0,
783         newest_comment_time: inserted_post.published,
784       },
785       subscribed: false,
786       read: false,
787       saved: false,
788     };
789
790     // TODO More needs to be added here
791     let mut expected_post_listing_with_user = expected_post_listing_no_user.to_owned();
792     expected_post_listing_with_user.my_vote = Some(1);
793
794     let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
795     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
796     Community::delete(&conn, inserted_community.id).unwrap();
797     User_::delete(&conn, inserted_user.id).unwrap();
798
799     // The with user
800     assert_eq!(
801       expected_post_listing_with_user,
802       read_post_listings_with_user[0]
803     );
804     assert_eq!(expected_post_listing_with_user, read_post_listing_with_user);
805     assert_eq!(1, read_post_listings_with_user.len());
806
807     // Without the user
808     assert_eq!(expected_post_listing_no_user, read_post_listings_no_user[0]);
809     assert_eq!(expected_post_listing_no_user, read_post_listing_no_user);
810     assert_eq!(1, read_post_listings_no_user.len());
811
812     // assert_eq!(expected_post, inserted_post);
813     // assert_eq!(expected_post, updated_post);
814     assert_eq!(expected_post_like, inserted_post_like);
815     assert_eq!(1, like_removed);
816     assert_eq!(1, num_deleted);
817   }
818 }