]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
Some formatting
[lemmy.git] / crates / db_views / src / post_view.rs
1 use diesel::{pg::Pg, result::Error, *};
2 use lemmy_db_queries::{
3   aggregates::post_aggregates::PostAggregates,
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     community,
16     community_follower,
17     community_person_ban,
18     person,
19     post,
20     post_aggregates,
21     post_like,
22     post_read,
23     post_saved,
24   },
25   source::{
26     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
27     person::{Person, PersonSafe},
28     post::{Post, PostRead, PostSaved},
29   },
30   CommunityId,
31   PersonId,
32   PostId,
33 };
34 use log::debug;
35 use serde::Serialize;
36
37 #[derive(Debug, PartialEq, Serialize, Clone)]
38 pub struct PostView {
39   pub post: Post,
40   pub creator: PersonSafe,
41   pub community: CommunitySafe,
42   pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
43   pub counts: PostAggregates,
44   pub subscribed: bool,     // Left join to CommunityFollower
45   pub saved: bool,          // Left join to PostSaved
46   pub read: bool,           // Left join to PostRead
47   pub my_vote: Option<i16>, // Left join to PostLike
48 }
49
50 type PostViewTuple = (
51   Post,
52   PersonSafe,
53   CommunitySafe,
54   Option<CommunityPersonBan>,
55   PostAggregates,
56   Option<CommunityFollower>,
57   Option<PostSaved>,
58   Option<PostRead>,
59   Option<i16>,
60 );
61
62 impl PostView {
63   pub fn read(
64     conn: &PgConnection,
65     post_id: PostId,
66     my_person_id: Option<PersonId>,
67   ) -> Result<Self, Error> {
68     // The left join below will return None in this case
69     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
70
71     let (
72       post,
73       creator,
74       community,
75       creator_banned_from_community,
76       counts,
77       follower,
78       saved,
79       read,
80       post_like,
81     ) = post::table
82       .find(post_id)
83       .inner_join(person::table)
84       .inner_join(community::table)
85       .left_join(
86         community_person_ban::table.on(
87           post::community_id
88             .eq(community_person_ban::community_id)
89             .and(community_person_ban::person_id.eq(post::creator_id)),
90         ),
91       )
92       .inner_join(post_aggregates::table)
93       .left_join(
94         community_follower::table.on(
95           post::community_id
96             .eq(community_follower::community_id)
97             .and(community_follower::person_id.eq(person_id_join)),
98         ),
99       )
100       .left_join(
101         post_saved::table.on(
102           post::id
103             .eq(post_saved::post_id)
104             .and(post_saved::person_id.eq(person_id_join)),
105         ),
106       )
107       .left_join(
108         post_read::table.on(
109           post::id
110             .eq(post_read::post_id)
111             .and(post_read::person_id.eq(person_id_join)),
112         ),
113       )
114       .left_join(
115         post_like::table.on(
116           post::id
117             .eq(post_like::post_id)
118             .and(post_like::person_id.eq(person_id_join)),
119         ),
120       )
121       .select((
122         post::all_columns,
123         Person::safe_columns_tuple(),
124         Community::safe_columns_tuple(),
125         community_person_ban::all_columns.nullable(),
126         post_aggregates::all_columns,
127         community_follower::all_columns.nullable(),
128         post_saved::all_columns.nullable(),
129         post_read::all_columns.nullable(),
130         post_like::score.nullable(),
131       ))
132       .first::<PostViewTuple>(conn)?;
133
134     // If a person is given, then my_vote, if None, should be 0, not null
135     // Necessary to differentiate between other person's votes
136     let my_vote = if my_person_id.is_some() && post_like.is_none() {
137       Some(0)
138     } else {
139       post_like
140     };
141
142     Ok(PostView {
143       post,
144       creator,
145       community,
146       creator_banned_from_community: creator_banned_from_community.is_some(),
147       counts,
148       subscribed: follower.is_some(),
149       saved: saved.is_some(),
150       read: read.is_some(),
151       my_vote,
152     })
153   }
154 }
155
156 pub struct PostQueryBuilder<'a> {
157   conn: &'a PgConnection,
158   listing_type: Option<ListingType>,
159   sort: Option<SortType>,
160   creator_id: Option<PersonId>,
161   community_id: Option<CommunityId>,
162   community_name: Option<String>,
163   my_person_id: Option<PersonId>,
164   search_term: Option<String>,
165   url_search: Option<String>,
166   show_nsfw: Option<bool>,
167   show_bot_accounts: Option<bool>,
168   saved_only: Option<bool>,
169   unread_only: Option<bool>,
170   page: Option<i64>,
171   limit: Option<i64>,
172 }
173
174 impl<'a> PostQueryBuilder<'a> {
175   pub fn create(conn: &'a PgConnection) -> Self {
176     PostQueryBuilder {
177       conn,
178       listing_type: None,
179       sort: None,
180       creator_id: None,
181       community_id: None,
182       community_name: None,
183       my_person_id: None,
184       search_term: None,
185       url_search: None,
186       show_nsfw: None,
187       show_bot_accounts: None,
188       saved_only: None,
189       unread_only: None,
190       page: None,
191       limit: None,
192     }
193   }
194
195   pub fn listing_type<T: MaybeOptional<ListingType>>(mut self, listing_type: T) -> Self {
196     self.listing_type = listing_type.get_optional();
197     self
198   }
199
200   pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self {
201     self.sort = sort.get_optional();
202     self
203   }
204
205   pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
206     self.community_id = community_id.get_optional();
207     self
208   }
209
210   pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
211     self.my_person_id = my_person_id.get_optional();
212     self
213   }
214
215   pub fn community_name<T: MaybeOptional<String>>(mut self, community_name: T) -> Self {
216     self.community_name = community_name.get_optional();
217     self
218   }
219
220   pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
221     self.creator_id = creator_id.get_optional();
222     self
223   }
224
225   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
226     self.search_term = search_term.get_optional();
227     self
228   }
229
230   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
231     self.url_search = url_search.get_optional();
232     self
233   }
234
235   pub fn show_nsfw<T: MaybeOptional<bool>>(mut self, show_nsfw: T) -> Self {
236     self.show_nsfw = show_nsfw.get_optional();
237     self
238   }
239
240   pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
241     self.show_bot_accounts = show_bot_accounts.get_optional();
242     self
243   }
244
245   pub fn saved_only<T: MaybeOptional<bool>>(mut self, saved_only: T) -> Self {
246     self.saved_only = saved_only.get_optional();
247     self
248   }
249
250   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
251     self.page = page.get_optional();
252     self
253   }
254
255   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
256     self.limit = limit.get_optional();
257     self
258   }
259
260   pub fn list(self) -> Result<Vec<PostView>, Error> {
261     use diesel::dsl::*;
262
263     // The left join below will return None in this case
264     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
265
266     let mut query = post::table
267       .inner_join(person::table)
268       .inner_join(community::table)
269       .left_join(
270         community_person_ban::table.on(
271           post::community_id
272             .eq(community_person_ban::community_id)
273             .and(community_person_ban::person_id.eq(post::creator_id)),
274         ),
275       )
276       .inner_join(post_aggregates::table)
277       .left_join(
278         community_follower::table.on(
279           post::community_id
280             .eq(community_follower::community_id)
281             .and(community_follower::person_id.eq(person_id_join)),
282         ),
283       )
284       .left_join(
285         post_saved::table.on(
286           post::id
287             .eq(post_saved::post_id)
288             .and(post_saved::person_id.eq(person_id_join)),
289         ),
290       )
291       .left_join(
292         post_read::table.on(
293           post::id
294             .eq(post_read::post_id)
295             .and(post_read::person_id.eq(person_id_join)),
296         ),
297       )
298       .left_join(
299         post_like::table.on(
300           post::id
301             .eq(post_like::post_id)
302             .and(post_like::person_id.eq(person_id_join)),
303         ),
304       )
305       .select((
306         post::all_columns,
307         Person::safe_columns_tuple(),
308         Community::safe_columns_tuple(),
309         community_person_ban::all_columns.nullable(),
310         post_aggregates::all_columns,
311         community_follower::all_columns.nullable(),
312         post_saved::all_columns.nullable(),
313         post_read::all_columns.nullable(),
314         post_like::score.nullable(),
315       ))
316       .into_boxed();
317
318     if let Some(listing_type) = self.listing_type {
319       query = match listing_type {
320         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
321         ListingType::Local => query.filter(community::local.eq(true)),
322         _ => query,
323       };
324     }
325
326     if let Some(community_id) = self.community_id {
327       query = query
328         .filter(post::community_id.eq(community_id))
329         .then_order_by(post_aggregates::stickied.desc());
330     }
331
332     if let Some(community_name) = self.community_name {
333       query = query
334         .filter(community::name.eq(community_name))
335         .filter(community::local.eq(true))
336         .then_order_by(post_aggregates::stickied.desc());
337     }
338
339     if let Some(url_search) = self.url_search {
340       query = query.filter(post::url.eq(url_search));
341     }
342
343     if let Some(search_term) = self.search_term {
344       let searcher = fuzzy_search(&search_term);
345       query = query.filter(
346         post::name
347           .ilike(searcher.to_owned())
348           .or(post::body.ilike(searcher)),
349       );
350     }
351
352     // If its for a specific person, show the removed / deleted
353     if let Some(creator_id) = self.creator_id {
354       query = query.filter(post::creator_id.eq(creator_id));
355     }
356
357     if !self.show_nsfw.unwrap_or(true) {
358       query = query
359         .filter(post::nsfw.eq(false))
360         .filter(community::nsfw.eq(false));
361     };
362
363     if !self.show_bot_accounts.unwrap_or(true) {
364       query = query.filter(person::bot_account.eq(false));
365     };
366
367     // TODO  These two might be wrong
368     if self.saved_only.unwrap_or_default() {
369       query = query.filter(post_saved::id.is_not_null());
370     };
371
372     if self.unread_only.unwrap_or_default() {
373       query = query.filter(post_read::id.is_not_null());
374     };
375
376     query = match self.sort.unwrap_or(SortType::Hot) {
377       SortType::Active => query
378         .then_order_by(
379           hot_rank(
380             post_aggregates::score,
381             post_aggregates::newest_comment_time_necro,
382           )
383           .desc(),
384         )
385         .then_order_by(post_aggregates::newest_comment_time_necro.desc()),
386       SortType::Hot => query
387         .then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
388         .then_order_by(post_aggregates::published.desc()),
389       SortType::New => query.then_order_by(post_aggregates::published.desc()),
390       SortType::MostComments => query.then_order_by(post_aggregates::comments.desc()),
391       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
392       SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
393       SortType::TopYear => query
394         .filter(post::published.gt(now - 1.years()))
395         .then_order_by(post_aggregates::score.desc()),
396       SortType::TopMonth => query
397         .filter(post::published.gt(now - 1.months()))
398         .then_order_by(post_aggregates::score.desc()),
399       SortType::TopWeek => query
400         .filter(post::published.gt(now - 1.weeks()))
401         .then_order_by(post_aggregates::score.desc()),
402       SortType::TopDay => query
403         .filter(post::published.gt(now - 1.days()))
404         .then_order_by(post_aggregates::score.desc()),
405     };
406
407     let (limit, offset) = limit_and_offset(self.page, self.limit);
408
409     query = query
410       .limit(limit)
411       .offset(offset)
412       .filter(post::removed.eq(false))
413       .filter(post::deleted.eq(false))
414       .filter(community::removed.eq(false))
415       .filter(community::deleted.eq(false));
416
417     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
418
419     let res = query.load::<PostViewTuple>(self.conn)?;
420
421     Ok(PostView::from_tuple_to_vec(res))
422   }
423 }
424
425 impl ViewToVec for PostView {
426   type DbTuple = PostViewTuple;
427   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
428     items
429       .iter()
430       .map(|a| Self {
431         post: a.0.to_owned(),
432         creator: a.1.to_owned(),
433         community: a.2.to_owned(),
434         creator_banned_from_community: a.3.is_some(),
435         counts: a.4.to_owned(),
436         subscribed: a.5.is_some(),
437         saved: a.6.is_some(),
438         read: a.7.is_some(),
439         my_vote: a.8,
440       })
441       .collect::<Vec<Self>>()
442   }
443 }
444
445 #[cfg(test)]
446 mod tests {
447   use crate::post_view::{PostQueryBuilder, PostView};
448   use lemmy_db_queries::{
449     aggregates::post_aggregates::PostAggregates,
450     establish_unpooled_connection,
451     Crud,
452     Likeable,
453     ListingType,
454     SortType,
455   };
456   use lemmy_db_schema::source::{community::*, person::*, post::*};
457   use serial_test::serial;
458
459   #[test]
460   #[serial]
461   fn test_crud() {
462     let conn = establish_unpooled_connection();
463
464     let person_name = "tegan".to_string();
465     let community_name = "test_community_3".to_string();
466     let post_name = "test post 3".to_string();
467     let bot_post_name = "test bot post".to_string();
468
469     let new_person = PersonForm {
470       name: person_name.to_owned(),
471       ..PersonForm::default()
472     };
473
474     let inserted_person = Person::create(&conn, &new_person).unwrap();
475
476     let new_bot = PersonForm {
477       name: person_name.to_owned(),
478       bot_account: Some(true),
479       ..PersonForm::default()
480     };
481
482     let inserted_bot = Person::create(&conn, &new_bot).unwrap();
483
484     let new_community = CommunityForm {
485       name: community_name.to_owned(),
486       title: "nada".to_owned(),
487       ..CommunityForm::default()
488     };
489
490     let inserted_community = Community::create(&conn, &new_community).unwrap();
491
492     let new_post = PostForm {
493       name: post_name.to_owned(),
494       creator_id: inserted_person.id,
495       community_id: inserted_community.id,
496       ..PostForm::default()
497     };
498
499     let inserted_post = Post::create(&conn, &new_post).unwrap();
500
501     let new_bot_post = PostForm {
502       name: bot_post_name,
503       creator_id: inserted_bot.id,
504       community_id: inserted_community.id,
505       ..PostForm::default()
506     };
507
508     let _inserted_bot_post = Post::create(&conn, &new_bot_post).unwrap();
509
510     let post_like_form = PostLikeForm {
511       post_id: inserted_post.id,
512       person_id: inserted_person.id,
513       score: 1,
514     };
515
516     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
517
518     let expected_post_like = PostLike {
519       id: inserted_post_like.id,
520       post_id: inserted_post.id,
521       person_id: inserted_person.id,
522       published: inserted_post_like.published,
523       score: 1,
524     };
525
526     let read_post_listings_with_person = PostQueryBuilder::create(&conn)
527       .listing_type(ListingType::Community)
528       .sort(SortType::New)
529       .show_bot_accounts(false)
530       .community_id(inserted_community.id)
531       .my_person_id(inserted_person.id)
532       .list()
533       .unwrap();
534
535     let read_post_listings_no_person = PostQueryBuilder::create(&conn)
536       .listing_type(ListingType::Community)
537       .sort(SortType::New)
538       .community_id(inserted_community.id)
539       .list()
540       .unwrap();
541
542     let read_post_listing_no_person = PostView::read(&conn, inserted_post.id, None).unwrap();
543     let read_post_listing_with_person =
544       PostView::read(&conn, inserted_post.id, Some(inserted_person.id)).unwrap();
545
546     let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
547
548     // the non person version
549     let expected_post_listing_no_person = PostView {
550       post: Post {
551         id: inserted_post.id,
552         name: post_name,
553         creator_id: inserted_person.id,
554         url: None,
555         body: None,
556         published: inserted_post.published,
557         updated: None,
558         community_id: inserted_community.id,
559         removed: false,
560         deleted: false,
561         locked: false,
562         stickied: false,
563         nsfw: false,
564         embed_title: None,
565         embed_description: None,
566         embed_html: None,
567         thumbnail_url: None,
568         ap_id: inserted_post.ap_id.to_owned(),
569         local: true,
570       },
571       my_vote: None,
572       creator: PersonSafe {
573         id: inserted_person.id,
574         name: person_name,
575         display_name: None,
576         published: inserted_person.published,
577         avatar: None,
578         actor_id: inserted_person.actor_id.to_owned(),
579         local: true,
580         admin: false,
581         bot_account: false,
582         banned: false,
583         deleted: false,
584         bio: None,
585         banner: None,
586         updated: None,
587         inbox_url: inserted_person.inbox_url.to_owned(),
588         shared_inbox_url: None,
589         matrix_user_id: None,
590       },
591       creator_banned_from_community: false,
592       community: CommunitySafe {
593         id: inserted_community.id,
594         name: community_name,
595         icon: None,
596         removed: false,
597         deleted: false,
598         nsfw: false,
599         actor_id: inserted_community.actor_id.to_owned(),
600         local: true,
601         title: "nada".to_owned(),
602         description: None,
603         updated: None,
604         banner: None,
605         published: inserted_community.published,
606       },
607       counts: PostAggregates {
608         id: agg.id,
609         post_id: inserted_post.id,
610         comments: 0,
611         score: 1,
612         upvotes: 1,
613         downvotes: 0,
614         stickied: false,
615         published: agg.published,
616         newest_comment_time_necro: inserted_post.published,
617         newest_comment_time: inserted_post.published,
618       },
619       subscribed: false,
620       read: false,
621       saved: false,
622     };
623
624     // TODO More needs to be added here
625     let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned();
626     expected_post_listing_with_user.my_vote = Some(1);
627
628     let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
629     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
630     Community::delete(&conn, inserted_community.id).unwrap();
631     Person::delete(&conn, inserted_person.id).unwrap();
632     Person::delete(&conn, inserted_bot.id).unwrap();
633
634     // The with user
635     assert_eq!(
636       expected_post_listing_with_user,
637       read_post_listings_with_person[0]
638     );
639     assert_eq!(
640       expected_post_listing_with_user,
641       read_post_listing_with_person
642     );
643
644     // Should be only one person, IE the bot post should be missing
645     assert_eq!(1, read_post_listings_with_person.len());
646
647     // Without the user
648     assert_eq!(
649       expected_post_listing_no_person,
650       read_post_listings_no_person[1]
651     );
652     assert_eq!(expected_post_listing_no_person, read_post_listing_no_person);
653
654     // Should be 2 posts, with the bot post
655     assert_eq!(2, read_post_listings_no_person.len());
656
657     assert_eq!(expected_post_like, inserted_post_like);
658     assert_eq!(1, like_removed);
659     assert_eq!(1, num_deleted);
660   }
661 }