]> Untitled Git - lemmy.git/blob - crates/db_views_actor/src/community_view.rs
Merge remote-tracking branch 'yerba/main'
[lemmy.git] / crates / db_views_actor / src / community_view.rs
1 use crate::{community_moderator_view::CommunityModeratorView, user_view::UserViewSafe};
2 use diesel::{result::Error, *};
3 use lemmy_db_queries::{
4   aggregates::community_aggregates::CommunityAggregates,
5   functions::hot_rank,
6   fuzzy_search,
7   limit_and_offset,
8   ListingType,
9   MaybeOptional,
10   SortType,
11   ToSafe,
12   ViewToVec,
13 };
14 use lemmy_db_schema::{
15   schema::{community, community_aggregates, community_follower, user_},
16   source::{
17     community::{Community, CommunityFollower, CommunitySafe},
18     user::{UserSafe, User_},
19   },
20 };
21 use serde::Serialize;
22
23 #[derive(Debug, Serialize, Clone)]
24 pub struct CommunityView {
25   pub community: CommunitySafe,
26   pub creator: UserSafe,
27   pub subscribed: bool,
28   pub counts: CommunityAggregates,
29 }
30
31 type CommunityViewTuple = (
32   CommunitySafe,
33   UserSafe,
34   CommunityAggregates,
35   Option<CommunityFollower>,
36 );
37
38 impl CommunityView {
39   pub fn read(
40     conn: &PgConnection,
41     community_id: i32,
42     my_user_id: Option<i32>,
43   ) -> Result<Self, Error> {
44     // The left join below will return None in this case
45     let user_id_join = my_user_id.unwrap_or(-1);
46
47     let (community, creator, counts, follower) = community::table
48       .find(community_id)
49       .inner_join(user_::table)
50       .inner_join(community_aggregates::table)
51       .left_join(
52         community_follower::table.on(
53           community::id
54             .eq(community_follower::community_id)
55             .and(community_follower::user_id.eq(user_id_join)),
56         ),
57       )
58       .select((
59         Community::safe_columns_tuple(),
60         User_::safe_columns_tuple(),
61         community_aggregates::all_columns,
62         community_follower::all_columns.nullable(),
63       ))
64       .first::<CommunityViewTuple>(conn)?;
65
66     Ok(CommunityView {
67       community,
68       creator,
69       subscribed: follower.is_some(),
70       counts,
71     })
72   }
73
74   // TODO: this function is only used by is_mod_or_admin() below, can probably be merged
75   fn community_mods_and_admins(conn: &PgConnection, community_id: i32) -> Result<Vec<i32>, Error> {
76     let mut mods_and_admins: Vec<i32> = Vec::new();
77     mods_and_admins.append(
78       &mut CommunityModeratorView::for_community(conn, community_id)
79         .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?,
80     );
81     mods_and_admins
82       .append(&mut UserViewSafe::admins(conn).map(|v| v.into_iter().map(|a| a.user.id).collect())?);
83     Ok(mods_and_admins)
84   }
85
86   pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
87     Self::community_mods_and_admins(conn, community_id)
88       .unwrap_or_default()
89       .contains(&user_id)
90   }
91 }
92
93 pub struct CommunityQueryBuilder<'a> {
94   conn: &'a PgConnection,
95   listing_type: &'a ListingType,
96   sort: &'a SortType,
97   my_user_id: Option<i32>,
98   show_nsfw: bool,
99   search_term: Option<String>,
100   page: Option<i64>,
101   limit: Option<i64>,
102 }
103
104 impl<'a> CommunityQueryBuilder<'a> {
105   pub fn create(conn: &'a PgConnection) -> Self {
106     CommunityQueryBuilder {
107       conn,
108       my_user_id: None,
109       listing_type: &ListingType::All,
110       sort: &SortType::Hot,
111       show_nsfw: true,
112       search_term: None,
113       page: None,
114       limit: None,
115     }
116   }
117
118   pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
119     self.listing_type = listing_type;
120     self
121   }
122
123   pub fn sort(mut self, sort: &'a SortType) -> Self {
124     self.sort = sort;
125     self
126   }
127
128   pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
129     self.show_nsfw = show_nsfw;
130     self
131   }
132
133   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
134     self.search_term = search_term.get_optional();
135     self
136   }
137
138   pub fn my_user_id<T: MaybeOptional<i32>>(mut self, my_user_id: T) -> Self {
139     self.my_user_id = my_user_id.get_optional();
140     self
141   }
142
143   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
144     self.page = page.get_optional();
145     self
146   }
147
148   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
149     self.limit = limit.get_optional();
150     self
151   }
152
153   pub fn list(self) -> Result<Vec<CommunityView>, Error> {
154     // The left join below will return None in this case
155     let user_id_join = self.my_user_id.unwrap_or(-1);
156
157     let mut query = community::table
158       .inner_join(user_::table)
159       .inner_join(community_aggregates::table)
160       .left_join(
161         community_follower::table.on(
162           community::id
163             .eq(community_follower::community_id)
164             .and(community_follower::user_id.eq(user_id_join)),
165         ),
166       )
167       .select((
168         Community::safe_columns_tuple(),
169         User_::safe_columns_tuple(),
170         community_aggregates::all_columns,
171         community_follower::all_columns.nullable(),
172       ))
173       .into_boxed();
174
175     if let Some(search_term) = self.search_term {
176       let searcher = fuzzy_search(&search_term);
177       query = query
178         .filter(community::name.ilike(searcher.to_owned()))
179         .or_filter(community::title.ilike(searcher.to_owned()))
180         .or_filter(community::description.ilike(searcher));
181     };
182
183     match self.sort {
184       SortType::New => query = query.order_by(community::published.desc()),
185       SortType::TopAll => query = query.order_by(community_aggregates::subscribers.desc()),
186       // Covers all other sorts, including hot
187       _ => {
188         query = query
189           .order_by(
190             hot_rank(
191               community_aggregates::subscribers,
192               community_aggregates::published,
193             )
194             .desc(),
195           )
196           .then_order_by(community_aggregates::published.desc())
197       }
198     };
199
200     if !self.show_nsfw {
201       query = query.filter(community::nsfw.eq(false));
202     };
203
204     query = match self.listing_type {
205       ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)),
206       ListingType::Local => query.filter(community::local.eq(true)),
207       _ => query,
208     };
209
210     let (limit, offset) = limit_and_offset(self.page, self.limit);
211     let res = query
212       .limit(limit)
213       .offset(offset)
214       .filter(community::removed.eq(false))
215       .filter(community::deleted.eq(false))
216       .load::<CommunityViewTuple>(self.conn)?;
217
218     Ok(CommunityView::from_tuple_to_vec(res))
219   }
220 }
221
222 impl ViewToVec for CommunityView {
223   type DbTuple = CommunityViewTuple;
224   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
225     items
226       .iter()
227       .map(|a| Self {
228         community: a.0.to_owned(),
229         creator: a.1.to_owned(),
230         counts: a.2.to_owned(),
231         subscribed: a.3.is_some(),
232       })
233       .collect::<Vec<Self>>()
234   }
235 }