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