]> Untitled Git - lemmy.git/blob - crates/api_common/src/lib.rs
Moving settings and secrets to context.
[lemmy.git] / crates / api_common / src / lib.rs
1 pub mod comment;
2 pub mod community;
3 pub mod person;
4 pub mod post;
5 pub mod site;
6 pub mod websocket;
7
8 use crate::site::FederatedInstances;
9 use diesel::PgConnection;
10 use lemmy_db_queries::{
11   source::{
12     community::{CommunityModerator_, Community_},
13     person_block::PersonBlock_,
14     site::Site_,
15   },
16   Crud,
17   DbPool,
18   Readable,
19 };
20 use lemmy_db_schema::{
21   source::{
22     comment::Comment,
23     community::{Community, CommunityModerator},
24     person::Person,
25     person_block::PersonBlock,
26     person_mention::{PersonMention, PersonMentionForm},
27     post::{Post, PostRead, PostReadForm},
28     secret::Secret,
29     site::Site,
30   },
31   CommunityId,
32   LocalUserId,
33   PersonId,
34   PostId,
35 };
36 use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
37 use lemmy_db_views_actor::{
38   community_person_ban_view::CommunityPersonBanView,
39   community_view::CommunityView,
40 };
41 use lemmy_utils::{
42   claims::Claims,
43   email::send_email,
44   settings::structs::{FederationConfig, Settings},
45   utils::MentionData,
46   ApiError,
47   LemmyError,
48 };
49 use log::error;
50 use url::Url;
51
52 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
53 where
54   F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
55   T: Send + 'static,
56 {
57   let pool = pool.clone();
58   let res = actix_web::web::block(move || {
59     let conn = pool.get()?;
60     let res = (f)(&conn);
61     Ok(res) as Result<T, LemmyError>
62   })
63   .await?;
64
65   res
66 }
67
68 pub async fn send_local_notifs(
69   mentions: Vec<MentionData>,
70   comment: Comment,
71   person: Person,
72   post: Post,
73   pool: &DbPool,
74   do_send_email: bool,
75   settings: &Settings,
76 ) -> Result<Vec<LocalUserId>, LemmyError> {
77   let settings = settings.to_owned();
78   let ids = blocking(pool, move |conn| {
79     do_send_local_notifs(
80       conn,
81       &mentions,
82       &comment,
83       &person,
84       &post,
85       do_send_email,
86       &settings,
87     )
88   })
89   .await?;
90
91   Ok(ids)
92 }
93
94 fn do_send_local_notifs(
95   conn: &PgConnection,
96   mentions: &[MentionData],
97   comment: &Comment,
98   person: &Person,
99   post: &Post,
100   do_send_email: bool,
101   settings: &Settings,
102 ) -> Vec<LocalUserId> {
103   let mut recipient_ids = Vec::new();
104
105   // Send the local mentions
106   for mention in mentions
107     .iter()
108     .filter(|m| m.is_local(&settings.hostname) && m.name.ne(&person.name))
109     .collect::<Vec<&MentionData>>()
110   {
111     if let Ok(mention_user_view) = LocalUserView::read_from_name(conn, &mention.name) {
112       // TODO
113       // At some point, make it so you can't tag the parent creator either
114       // This can cause two notifications, one for reply and the other for mention
115       recipient_ids.push(mention_user_view.local_user.id);
116
117       let user_mention_form = PersonMentionForm {
118         recipient_id: mention_user_view.person.id,
119         comment_id: comment.id,
120         read: None,
121       };
122
123       // Allow this to fail softly, since comment edits might re-update or replace it
124       // Let the uniqueness handle this fail
125       PersonMention::create(conn, &user_mention_form).ok();
126
127       // Send an email to those local users that have notifications on
128       if do_send_email {
129         send_email_to_user(
130           &mention_user_view,
131           "Mentioned by",
132           "Person Mention",
133           &comment.content,
134           settings,
135         )
136       }
137     }
138   }
139
140   // Send notifs to the parent commenter / poster
141   match comment.parent_id {
142     Some(parent_id) => {
143       if let Ok(parent_comment) = Comment::read(conn, parent_id) {
144         // Don't send a notif to yourself
145         if parent_comment.creator_id != person.id {
146           // Get the parent commenter local_user
147           if let Ok(parent_user_view) = LocalUserView::read_person(conn, parent_comment.creator_id)
148           {
149             recipient_ids.push(parent_user_view.local_user.id);
150
151             if do_send_email {
152               send_email_to_user(
153                 &parent_user_view,
154                 "Reply from",
155                 "Comment Reply",
156                 &comment.content,
157                 settings,
158               )
159             }
160           }
161         }
162       }
163     }
164     // Its a post
165     None => {
166       if post.creator_id != person.id {
167         if let Ok(parent_user_view) = LocalUserView::read_person(conn, post.creator_id) {
168           recipient_ids.push(parent_user_view.local_user.id);
169
170           if do_send_email {
171             send_email_to_user(
172               &parent_user_view,
173               "Reply from",
174               "Post Reply",
175               &comment.content,
176               settings,
177             )
178           }
179         }
180       }
181     }
182   };
183   recipient_ids
184 }
185
186 pub fn send_email_to_user(
187   local_user_view: &LocalUserView,
188   subject_text: &str,
189   body_text: &str,
190   comment_content: &str,
191   settings: &Settings,
192 ) {
193   if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
194     return;
195   }
196
197   if let Some(user_email) = &local_user_view.local_user.email {
198     let subject = &format!(
199       "{} - {} {}",
200       subject_text, settings.hostname, local_user_view.person.name,
201     );
202     let html = &format!(
203       "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
204       body_text,
205       local_user_view.person.name,
206       comment_content,
207       settings.get_protocol_and_hostname()
208     );
209     match send_email(
210       subject,
211       user_email,
212       &local_user_view.person.name,
213       html,
214       settings,
215     ) {
216       Ok(_o) => _o,
217       Err(e) => error!("{}", e),
218     };
219   }
220 }
221
222 pub async fn is_mod_or_admin(
223   pool: &DbPool,
224   person_id: PersonId,
225   community_id: CommunityId,
226 ) -> Result<(), LemmyError> {
227   let is_mod_or_admin = blocking(pool, move |conn| {
228     CommunityView::is_mod_or_admin(conn, person_id, community_id)
229   })
230   .await?;
231   if !is_mod_or_admin {
232     return Err(ApiError::err("not_a_mod_or_admin").into());
233   }
234   Ok(())
235 }
236
237 pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
238   if !local_user_view.person.admin {
239     return Err(ApiError::err("not_an_admin").into());
240   }
241   Ok(())
242 }
243
244 pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
245   blocking(pool, move |conn| Post::read(conn, post_id))
246     .await?
247     .map_err(|_| ApiError::err("couldnt_find_post").into())
248 }
249
250 pub async fn mark_post_as_read(
251   person_id: PersonId,
252   post_id: PostId,
253   pool: &DbPool,
254 ) -> Result<PostRead, LemmyError> {
255   let post_read_form = PostReadForm { post_id, person_id };
256
257   blocking(pool, move |conn| {
258     PostRead::mark_as_read(conn, &post_read_form)
259   })
260   .await?
261   .map_err(|_| ApiError::err("couldnt_mark_post_as_read").into())
262 }
263
264 pub async fn get_local_user_view_from_jwt(
265   jwt: &str,
266   pool: &DbPool,
267   secret: &Secret,
268 ) -> Result<LocalUserView, LemmyError> {
269   let claims = Claims::decode(jwt, &secret.jwt_secret)
270     .map_err(|_| ApiError::err("not_logged_in"))?
271     .claims;
272   let local_user_id = LocalUserId(claims.sub);
273   let local_user_view =
274     blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
275   // Check for a site ban
276   if local_user_view.person.banned {
277     return Err(ApiError::err("site_ban").into());
278   }
279
280   // Check for user deletion
281   if local_user_view.person.deleted {
282     return Err(ApiError::err("deleted").into());
283   }
284
285   check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
286
287   Ok(local_user_view)
288 }
289
290 /// Checks if user's token was issued before user's password reset.
291 pub fn check_validator_time(
292   validator_time: &chrono::NaiveDateTime,
293   claims: &Claims,
294 ) -> Result<(), LemmyError> {
295   let user_validation_time = validator_time.timestamp();
296   if user_validation_time > claims.iat {
297     Err(ApiError::err("not_logged_in").into())
298   } else {
299     Ok(())
300   }
301 }
302
303 pub async fn get_local_user_view_from_jwt_opt(
304   jwt: &Option<String>,
305   pool: &DbPool,
306   secret: &Secret,
307 ) -> Result<Option<LocalUserView>, LemmyError> {
308   match jwt {
309     Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool, secret).await?)),
310     None => Ok(None),
311   }
312 }
313
314 pub async fn get_local_user_settings_view_from_jwt(
315   jwt: &str,
316   pool: &DbPool,
317   secret: &Secret,
318 ) -> Result<LocalUserSettingsView, LemmyError> {
319   let claims = Claims::decode(jwt, &secret.jwt_secret)
320     .map_err(|_| ApiError::err("not_logged_in"))?
321     .claims;
322   let local_user_id = LocalUserId(claims.sub);
323   let local_user_view = blocking(pool, move |conn| {
324     LocalUserSettingsView::read(conn, local_user_id)
325   })
326   .await??;
327   // Check for a site ban
328   if local_user_view.person.banned {
329     return Err(ApiError::err("site_ban").into());
330   }
331
332   check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
333
334   Ok(local_user_view)
335 }
336
337 pub async fn get_local_user_settings_view_from_jwt_opt(
338   jwt: &Option<String>,
339   pool: &DbPool,
340   secret: &Secret,
341 ) -> Result<Option<LocalUserSettingsView>, LemmyError> {
342   match jwt {
343     Some(jwt) => Ok(Some(
344       get_local_user_settings_view_from_jwt(jwt, pool, secret).await?,
345     )),
346     None => Ok(None),
347   }
348 }
349
350 pub async fn check_community_ban(
351   person_id: PersonId,
352   community_id: CommunityId,
353   pool: &DbPool,
354 ) -> Result<(), LemmyError> {
355   let is_banned =
356     move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
357   if blocking(pool, is_banned).await? {
358     Err(ApiError::err("community_ban").into())
359   } else {
360     Ok(())
361   }
362 }
363
364 pub async fn check_person_block(
365   my_id: PersonId,
366   potential_blocker_id: PersonId,
367   pool: &DbPool,
368 ) -> Result<(), LemmyError> {
369   let is_blocked = move |conn: &'_ _| PersonBlock::read(conn, potential_blocker_id, my_id).is_ok();
370   if blocking(pool, is_blocked).await? {
371     Err(ApiError::err("person_block").into())
372   } else {
373     Ok(())
374   }
375 }
376
377 pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
378   if score == -1 {
379     let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
380     if !site.enable_downvotes {
381       return Err(ApiError::err("downvotes_disabled").into());
382     }
383   }
384   Ok(())
385 }
386
387 /// Returns a list of communities that the user moderates
388 /// or if a community_id is supplied validates the user is a moderator
389 /// of that community and returns the community id in a vec
390 ///
391 /// * `person_id` - the person id of the moderator
392 /// * `community_id` - optional community id to check for moderator privileges
393 /// * `pool` - the diesel db pool
394 pub async fn collect_moderated_communities(
395   person_id: PersonId,
396   community_id: Option<CommunityId>,
397   pool: &DbPool,
398 ) -> Result<Vec<CommunityId>, LemmyError> {
399   if let Some(community_id) = community_id {
400     // if the user provides a community_id, just check for mod/admin privileges
401     is_mod_or_admin(pool, person_id, community_id).await?;
402     Ok(vec![community_id])
403   } else {
404     let ids = blocking(pool, move |conn: &'_ _| {
405       CommunityModerator::get_person_moderated_communities(conn, person_id)
406     })
407     .await??;
408     Ok(ids)
409   }
410 }
411
412 pub async fn build_federated_instances(
413   pool: &DbPool,
414   federation_config: &FederationConfig,
415   hostname: &str,
416 ) -> Result<Option<FederatedInstances>, LemmyError> {
417   let federation = federation_config.to_owned();
418   if federation.enabled {
419     let distinct_communities = blocking(pool, move |conn| {
420       Community::distinct_federated_communities(conn)
421     })
422     .await??;
423
424     let allowed = federation.allowed_instances;
425     let blocked = federation.blocked_instances;
426
427     let mut linked = distinct_communities
428       .iter()
429       .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
430       .collect::<Result<Vec<String>, LemmyError>>()?;
431
432     if let Some(allowed) = allowed.as_ref() {
433       linked.extend_from_slice(allowed);
434     }
435
436     if let Some(blocked) = blocked.as_ref() {
437       linked.retain(|a| !blocked.contains(a) && !a.eq(hostname));
438     }
439
440     // Sort and remove dupes
441     linked.sort_unstable();
442     linked.dedup();
443
444     Ok(Some(FederatedInstances {
445       linked,
446       allowed,
447       blocked,
448     }))
449   } else {
450     Ok(None)
451   }
452 }
453
454 /// Checks the password length
455 pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
456   if !(10..=60).contains(&pass.len()) {
457     Err(ApiError::err("invalid_password").into())
458   } else {
459     Ok(())
460   }
461 }
462
463 /// Checks the site description length
464 pub fn site_description_length_check(description: &str) -> Result<(), LemmyError> {
465   if description.len() > 150 {
466     Err(ApiError::err("site_description_length_overflow").into())
467   } else {
468     Ok(())
469   }
470 }