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