]> Untitled Git - lemmy.git/blob - crates/db_views_actor/src/community_view.rs
Adding check for requests with no id or name, adding max limit. (#2265)
[lemmy.git] / crates / db_views_actor / src / community_view.rs
1 use crate::structs::{CommunityModeratorView, CommunityView, PersonViewSafe};
2 use diesel::{result::Error, *};
3 use lemmy_db_schema::{
4   aggregates::structs::CommunityAggregates,
5   newtypes::{CommunityId, PersonId},
6   schema::{community, community_aggregates, community_block, community_follower, local_user},
7   source::{
8     community::{Community, CommunityFollower, CommunitySafe},
9     community_block::CommunityBlock,
10   },
11   traits::{MaybeOptional, ToSafe, ViewToVec},
12   utils::{functions::hot_rank, fuzzy_search, limit_and_offset},
13   ListingType,
14   SortType,
15 };
16
17 type CommunityViewTuple = (
18   CommunitySafe,
19   CommunityAggregates,
20   Option<CommunityFollower>,
21   Option<CommunityBlock>,
22 );
23
24 impl CommunityView {
25   pub fn read(
26     conn: &PgConnection,
27     community_id: CommunityId,
28     my_person_id: Option<PersonId>,
29   ) -> Result<Self, Error> {
30     // The left join below will return None in this case
31     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
32
33     let (community, counts, follower, blocked) = community::table
34       .find(community_id)
35       .inner_join(community_aggregates::table)
36       .left_join(
37         community_follower::table.on(
38           community::id
39             .eq(community_follower::community_id)
40             .and(community_follower::person_id.eq(person_id_join)),
41         ),
42       )
43       .left_join(
44         community_block::table.on(
45           community::id
46             .eq(community_block::community_id)
47             .and(community_block::person_id.eq(person_id_join)),
48         ),
49       )
50       .select((
51         Community::safe_columns_tuple(),
52         community_aggregates::all_columns,
53         community_follower::all_columns.nullable(),
54         community_block::all_columns.nullable(),
55       ))
56       .first::<CommunityViewTuple>(conn)?;
57
58     Ok(CommunityView {
59       community,
60       subscribed: CommunityFollower::to_subscribed_type(&follower),
61       blocked: blocked.is_some(),
62       counts,
63     })
64   }
65
66   pub fn is_mod_or_admin(
67     conn: &PgConnection,
68     person_id: PersonId,
69     community_id: CommunityId,
70   ) -> bool {
71     let is_mod = CommunityModeratorView::for_community(conn, community_id)
72       .map(|v| {
73         v.into_iter()
74           .map(|m| m.moderator.id)
75           .collect::<Vec<PersonId>>()
76       })
77       .unwrap_or_default()
78       .contains(&person_id);
79     if is_mod {
80       return true;
81     }
82
83     PersonViewSafe::admins(conn)
84       .map(|v| {
85         v.into_iter()
86           .map(|a| a.person.id)
87           .collect::<Vec<PersonId>>()
88       })
89       .unwrap_or_default()
90       .contains(&person_id)
91   }
92 }
93
94 pub struct CommunityQueryBuilder<'a> {
95   conn: &'a PgConnection,
96   listing_type: Option<ListingType>,
97   sort: Option<SortType>,
98   my_person_id: Option<PersonId>,
99   show_nsfw: Option<bool>,
100   search_term: Option<String>,
101   page: Option<i64>,
102   limit: Option<i64>,
103 }
104
105 impl<'a> CommunityQueryBuilder<'a> {
106   pub fn create(conn: &'a PgConnection) -> Self {
107     CommunityQueryBuilder {
108       conn,
109       my_person_id: None,
110       listing_type: None,
111       sort: None,
112       show_nsfw: None,
113       search_term: None,
114       page: None,
115       limit: None,
116     }
117   }
118
119   pub fn listing_type<T: MaybeOptional<ListingType>>(mut self, listing_type: T) -> Self {
120     self.listing_type = listing_type.get_optional();
121     self
122   }
123
124   pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self {
125     self.sort = sort.get_optional();
126     self
127   }
128
129   pub fn show_nsfw<T: MaybeOptional<bool>>(mut self, show_nsfw: T) -> Self {
130     self.show_nsfw = show_nsfw.get_optional();
131     self
132   }
133
134   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
135     self.search_term = search_term.get_optional();
136     self
137   }
138
139   pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
140     self.my_person_id = my_person_id.get_optional();
141     self
142   }
143
144   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
145     self.page = page.get_optional();
146     self
147   }
148
149   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
150     self.limit = limit.get_optional();
151     self
152   }
153
154   pub fn list(self) -> Result<Vec<CommunityView>, Error> {
155     // The left join below will return None in this case
156     let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
157
158     let mut query = community::table
159       .inner_join(community_aggregates::table)
160       .left_join(local_user::table.on(local_user::person_id.eq(person_id_join)))
161       .left_join(
162         community_follower::table.on(
163           community::id
164             .eq(community_follower::community_id)
165             .and(community_follower::person_id.eq(person_id_join)),
166         ),
167       )
168       .left_join(
169         community_block::table.on(
170           community::id
171             .eq(community_block::community_id)
172             .and(community_block::person_id.eq(person_id_join)),
173         ),
174       )
175       .select((
176         Community::safe_columns_tuple(),
177         community_aggregates::all_columns,
178         community_follower::all_columns.nullable(),
179         community_block::all_columns.nullable(),
180       ))
181       .into_boxed();
182
183     if let Some(search_term) = self.search_term {
184       let searcher = fuzzy_search(&search_term);
185       query = query
186         .filter(community::name.ilike(searcher.to_owned()))
187         .or_filter(community::title.ilike(searcher.to_owned()))
188         .or_filter(community::description.ilike(searcher));
189     };
190
191     match self.sort.unwrap_or(SortType::Hot) {
192       SortType::New => query = query.order_by(community::published.desc()),
193       SortType::TopAll => query = query.order_by(community_aggregates::subscribers.desc()),
194       SortType::TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()),
195       SortType::Hot => {
196         query = query
197           .order_by(
198             hot_rank(
199               community_aggregates::subscribers,
200               community_aggregates::published,
201             )
202             .desc(),
203           )
204           .then_order_by(community_aggregates::published.desc());
205         // Don't show hidden communities in Hot (trending)
206         query = query.filter(
207           community::hidden
208             .eq(false)
209             .or(community_follower::person_id.eq(person_id_join)),
210         );
211       }
212       // Covers all other sorts
213       _ => {
214         query = query
215           .order_by(
216             hot_rank(
217               community_aggregates::subscribers,
218               community_aggregates::published,
219             )
220             .desc(),
221           )
222           .then_order_by(community_aggregates::published.desc())
223       }
224     };
225
226     if let Some(listing_type) = self.listing_type {
227       query = match listing_type {
228         ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
229         ListingType::Local => query.filter(community::local.eq(true)),
230         _ => query,
231       };
232     }
233
234     // Don't show blocked communities or nsfw communities if not enabled in profile
235     if self.my_person_id.is_some() {
236       query = query.filter(community_block::person_id.is_null());
237       query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true)));
238     } else {
239       // No person in request, only show nsfw communities if show_nsfw passed into request
240       if !self.show_nsfw.unwrap_or(false) {
241         query = query.filter(community::nsfw.eq(false));
242       }
243     }
244
245     let (limit, offset) = limit_and_offset(self.page, self.limit)?;
246     let res = query
247       .limit(limit)
248       .offset(offset)
249       .filter(community::removed.eq(false))
250       .filter(community::deleted.eq(false))
251       .load::<CommunityViewTuple>(self.conn)?;
252
253     Ok(CommunityView::from_tuple_to_vec(res))
254   }
255 }
256
257 impl ViewToVec for CommunityView {
258   type DbTuple = CommunityViewTuple;
259   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
260     items
261       .iter()
262       .map(|a| Self {
263         community: a.0.to_owned(),
264         counts: a.1.to_owned(),
265         subscribed: CommunityFollower::to_subscribed_type(&a.2),
266         blocked: a.3.is_some(),
267       })
268       .collect::<Vec<Self>>()
269   }
270 }