]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
04cd099ecff72478cfdcbc92c4455068373193f6
[lemmy.git] / crates / api / src / site.rs
1 use crate::Perform;
2 use actix_web::web::Data;
3 use anyhow::Context;
4 use diesel::NotFound;
5 use lemmy_api_common::{
6   blocking,
7   build_federated_instances,
8   check_private_instance,
9   get_local_user_view_from_jwt,
10   get_local_user_view_from_jwt_opt,
11   is_admin,
12   send_application_approved_email,
13   site::*,
14 };
15 use lemmy_apub::{
16   fetcher::{
17     search::{search_by_apub_id, SearchableObjects},
18     webfinger::webfinger_resolve,
19   },
20   objects::community::ApubCommunity,
21   EndpointType,
22 };
23 use lemmy_db_schema::{
24   diesel_option_overwrite,
25   from_opt_str_to_opt_enum,
26   newtypes::PersonId,
27   source::{
28     local_user::{LocalUser, LocalUserForm},
29     moderator::*,
30     registration_application::{RegistrationApplication, RegistrationApplicationForm},
31     site::Site,
32   },
33   traits::{Crud, DeleteableOrRemoveable},
34   DbPool,
35   ListingType,
36   SearchType,
37   SortType,
38 };
39 use lemmy_db_views::{
40   comment_view::{CommentQueryBuilder, CommentView},
41   local_user_view::LocalUserView,
42   post_view::{PostQueryBuilder, PostView},
43   registration_application_view::{
44     RegistrationApplicationQueryBuilder,
45     RegistrationApplicationView,
46   },
47   site_view::SiteView,
48 };
49 use lemmy_db_views_actor::{
50   community_view::{CommunityQueryBuilder, CommunityView},
51   person_view::{PersonQueryBuilder, PersonViewSafe},
52 };
53 use lemmy_db_views_moderator::{
54   mod_add_community_view::ModAddCommunityView,
55   mod_add_view::ModAddView,
56   mod_ban_from_community_view::ModBanFromCommunityView,
57   mod_ban_view::ModBanView,
58   mod_lock_post_view::ModLockPostView,
59   mod_remove_comment_view::ModRemoveCommentView,
60   mod_remove_community_view::ModRemoveCommunityView,
61   mod_remove_post_view::ModRemovePostView,
62   mod_sticky_post_view::ModStickyPostView,
63   mod_transfer_community_view::ModTransferCommunityView,
64 };
65 use lemmy_utils::{location_info, settings::structs::Settings, version, ConnectionId, LemmyError};
66 use lemmy_websocket::LemmyContext;
67
68 #[async_trait::async_trait(?Send)]
69 impl Perform for GetModlog {
70   type Response = GetModlogResponse;
71
72   #[tracing::instrument(skip(context, _websocket_id))]
73   async fn perform(
74     &self,
75     context: &Data<LemmyContext>,
76     _websocket_id: Option<ConnectionId>,
77   ) -> Result<GetModlogResponse, LemmyError> {
78     let data: &GetModlog = self;
79
80     let local_user_view =
81       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
82         .await?;
83
84     check_private_instance(&local_user_view, context.pool()).await?;
85
86     let community_id = data.community_id;
87     let mod_person_id = data.mod_person_id;
88     let page = data.page;
89     let limit = data.limit;
90     let removed_posts = blocking(context.pool(), move |conn| {
91       ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
92     })
93     .await??;
94
95     let locked_posts = blocking(context.pool(), move |conn| {
96       ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
97     })
98     .await??;
99
100     let stickied_posts = blocking(context.pool(), move |conn| {
101       ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
102     })
103     .await??;
104
105     let removed_comments = blocking(context.pool(), move |conn| {
106       ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
107     })
108     .await??;
109
110     let banned_from_community = blocking(context.pool(), move |conn| {
111       ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
112     })
113     .await??;
114
115     let added_to_community = blocking(context.pool(), move |conn| {
116       ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
117     })
118     .await??;
119
120     let transferred_to_community = blocking(context.pool(), move |conn| {
121       ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit)
122     })
123     .await??;
124
125     // These arrays are only for the full modlog, when a community isn't given
126     let (removed_communities, banned, added) = if data.community_id.is_none() {
127       blocking(context.pool(), move |conn| {
128         Ok((
129           ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
130           ModBanView::list(conn, mod_person_id, page, limit)?,
131           ModAddView::list(conn, mod_person_id, page, limit)?,
132         )) as Result<_, LemmyError>
133       })
134       .await??
135     } else {
136       (Vec::new(), Vec::new(), Vec::new())
137     };
138
139     // Return the jwt
140     Ok(GetModlogResponse {
141       removed_posts,
142       locked_posts,
143       stickied_posts,
144       removed_comments,
145       removed_communities,
146       banned_from_community,
147       banned,
148       added_to_community,
149       added,
150       transferred_to_community,
151     })
152   }
153 }
154
155 #[async_trait::async_trait(?Send)]
156 impl Perform for Search {
157   type Response = SearchResponse;
158
159   #[tracing::instrument(skip(context, _websocket_id))]
160   async fn perform(
161     &self,
162     context: &Data<LemmyContext>,
163     _websocket_id: Option<ConnectionId>,
164   ) -> Result<SearchResponse, LemmyError> {
165     let data: &Search = self;
166
167     let local_user_view =
168       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
169         .await?;
170
171     check_private_instance(&local_user_view, context.pool()).await?;
172
173     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
174     let show_bot_accounts = local_user_view
175       .as_ref()
176       .map(|t| t.local_user.show_bot_accounts);
177     let show_read_posts = local_user_view
178       .as_ref()
179       .map(|t| t.local_user.show_read_posts);
180
181     let person_id = local_user_view.map(|u| u.person.id);
182
183     let mut posts = Vec::new();
184     let mut comments = Vec::new();
185     let mut communities = Vec::new();
186     let mut users = Vec::new();
187
188     // TODO no clean / non-nsfw searching rn
189
190     let q = data.q.to_owned();
191     let page = data.page;
192     let limit = data.limit;
193     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
194     let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.listing_type);
195     let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All);
196     let community_id = data.community_id;
197     let community_actor_id = if let Some(name) = &data.community_name {
198       webfinger_resolve::<ApubCommunity>(name, EndpointType::Community, context, &mut 0)
199         .await
200         .ok()
201     } else {
202       None
203     };
204     let creator_id = data.creator_id;
205     match search_type {
206       SearchType::Posts => {
207         posts = blocking(context.pool(), move |conn| {
208           PostQueryBuilder::create(conn)
209             .sort(sort)
210             .show_nsfw(show_nsfw)
211             .show_bot_accounts(show_bot_accounts)
212             .show_read_posts(show_read_posts)
213             .listing_type(listing_type)
214             .community_id(community_id)
215             .community_actor_id(community_actor_id)
216             .creator_id(creator_id)
217             .my_person_id(person_id)
218             .search_term(q)
219             .page(page)
220             .limit(limit)
221             .list()
222         })
223         .await??;
224       }
225       SearchType::Comments => {
226         comments = blocking(context.pool(), move |conn| {
227           CommentQueryBuilder::create(conn)
228             .sort(sort)
229             .listing_type(listing_type)
230             .search_term(q)
231             .show_bot_accounts(show_bot_accounts)
232             .community_id(community_id)
233             .community_actor_id(community_actor_id)
234             .creator_id(creator_id)
235             .my_person_id(person_id)
236             .page(page)
237             .limit(limit)
238             .list()
239         })
240         .await??;
241       }
242       SearchType::Communities => {
243         communities = blocking(context.pool(), move |conn| {
244           CommunityQueryBuilder::create(conn)
245             .sort(sort)
246             .listing_type(listing_type)
247             .search_term(q)
248             .my_person_id(person_id)
249             .page(page)
250             .limit(limit)
251             .list()
252         })
253         .await??;
254       }
255       SearchType::Users => {
256         users = blocking(context.pool(), move |conn| {
257           PersonQueryBuilder::create(conn)
258             .sort(sort)
259             .search_term(q)
260             .page(page)
261             .limit(limit)
262             .list()
263         })
264         .await??;
265       }
266       SearchType::All => {
267         // If the community or creator is included, dont search communities or users
268         let community_or_creator_included =
269           data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
270         let community_actor_id_2 = community_actor_id.to_owned();
271
272         posts = blocking(context.pool(), move |conn| {
273           PostQueryBuilder::create(conn)
274             .sort(sort)
275             .show_nsfw(show_nsfw)
276             .show_bot_accounts(show_bot_accounts)
277             .show_read_posts(show_read_posts)
278             .listing_type(listing_type)
279             .community_id(community_id)
280             .community_actor_id(community_actor_id_2)
281             .creator_id(creator_id)
282             .my_person_id(person_id)
283             .search_term(q)
284             .page(page)
285             .limit(limit)
286             .list()
287         })
288         .await??;
289
290         let q = data.q.to_owned();
291         let community_actor_id = community_actor_id.to_owned();
292
293         comments = blocking(context.pool(), move |conn| {
294           CommentQueryBuilder::create(conn)
295             .sort(sort)
296             .listing_type(listing_type)
297             .search_term(q)
298             .show_bot_accounts(show_bot_accounts)
299             .community_id(community_id)
300             .community_actor_id(community_actor_id)
301             .creator_id(creator_id)
302             .my_person_id(person_id)
303             .page(page)
304             .limit(limit)
305             .list()
306         })
307         .await??;
308
309         let q = data.q.to_owned();
310
311         communities = if community_or_creator_included {
312           vec![]
313         } else {
314           blocking(context.pool(), move |conn| {
315             CommunityQueryBuilder::create(conn)
316               .sort(sort)
317               .listing_type(listing_type)
318               .search_term(q)
319               .my_person_id(person_id)
320               .page(page)
321               .limit(limit)
322               .list()
323           })
324           .await??
325         };
326
327         let q = data.q.to_owned();
328
329         users = if community_or_creator_included {
330           vec![]
331         } else {
332           blocking(context.pool(), move |conn| {
333             PersonQueryBuilder::create(conn)
334               .sort(sort)
335               .search_term(q)
336               .page(page)
337               .limit(limit)
338               .list()
339           })
340           .await??
341         };
342       }
343       SearchType::Url => {
344         posts = blocking(context.pool(), move |conn| {
345           PostQueryBuilder::create(conn)
346             .sort(sort)
347             .show_nsfw(show_nsfw)
348             .show_bot_accounts(show_bot_accounts)
349             .show_read_posts(show_read_posts)
350             .listing_type(listing_type)
351             .my_person_id(person_id)
352             .community_id(community_id)
353             .community_actor_id(community_actor_id)
354             .creator_id(creator_id)
355             .url_search(q)
356             .page(page)
357             .limit(limit)
358             .list()
359         })
360         .await??;
361       }
362     };
363
364     // Blank out deleted or removed info for non logged in users
365     if person_id.is_none() {
366       for cv in communities
367         .iter_mut()
368         .filter(|cv| cv.community.deleted || cv.community.removed)
369       {
370         cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
371       }
372
373       for pv in posts
374         .iter_mut()
375         .filter(|p| p.post.deleted || p.post.removed)
376       {
377         pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
378       }
379
380       for cv in comments
381         .iter_mut()
382         .filter(|cv| cv.comment.deleted || cv.comment.removed)
383       {
384         cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
385       }
386     }
387
388     // Return the jwt
389     Ok(SearchResponse {
390       type_: search_type.to_string(),
391       comments,
392       posts,
393       communities,
394       users,
395     })
396   }
397 }
398
399 #[async_trait::async_trait(?Send)]
400 impl Perform for ResolveObject {
401   type Response = ResolveObjectResponse;
402
403   #[tracing::instrument(skip(context, _websocket_id))]
404   async fn perform(
405     &self,
406     context: &Data<LemmyContext>,
407     _websocket_id: Option<ConnectionId>,
408   ) -> Result<ResolveObjectResponse, LemmyError> {
409     let local_user_view =
410       get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
411         .await?;
412     check_private_instance(&local_user_view, context.pool()).await?;
413
414     let res = search_by_apub_id(&self.q, context)
415       .await
416       .map_err(LemmyError::from)
417       .map_err(|e| e.with_message("couldnt_find_object"))?;
418     convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
419       .await
420       .map_err(LemmyError::from)
421       .map_err(|e| e.with_message("couldnt_find_object"))
422   }
423 }
424
425 async fn convert_response(
426   object: SearchableObjects,
427   user_id: Option<PersonId>,
428   pool: &DbPool,
429 ) -> Result<ResolveObjectResponse, LemmyError> {
430   let removed_or_deleted;
431   let mut res = ResolveObjectResponse {
432     comment: None,
433     post: None,
434     community: None,
435     person: None,
436   };
437   use SearchableObjects::*;
438   match object {
439     Person(p) => {
440       removed_or_deleted = p.deleted;
441       res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
442     }
443     Community(c) => {
444       removed_or_deleted = c.deleted || c.removed;
445       res.community =
446         Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
447     }
448     Post(p) => {
449       removed_or_deleted = p.deleted || p.removed;
450       res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
451     }
452     Comment(c) => {
453       removed_or_deleted = c.deleted || c.removed;
454       res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
455     }
456   };
457   // if the object was deleted from database, dont return it
458   if removed_or_deleted {
459     return Err(NotFound {}.into());
460   }
461   Ok(res)
462 }
463
464 #[async_trait::async_trait(?Send)]
465 impl Perform for TransferSite {
466   type Response = GetSiteResponse;
467
468   #[tracing::instrument(skip(context, _websocket_id))]
469   async fn perform(
470     &self,
471     context: &Data<LemmyContext>,
472     _websocket_id: Option<ConnectionId>,
473   ) -> Result<GetSiteResponse, LemmyError> {
474     let data: &TransferSite = self;
475     let local_user_view =
476       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
477
478     is_admin(&local_user_view)?;
479
480     let read_site = blocking(context.pool(), Site::read_simple).await??;
481
482     // Make sure user is the creator
483     if read_site.creator_id != local_user_view.person.id {
484       return Err(LemmyError::from_message("not_an_admin"));
485     }
486
487     let new_creator_id = data.person_id;
488     let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
489     blocking(context.pool(), transfer_site)
490       .await?
491       .map_err(LemmyError::from)
492       .map_err(|e| e.with_message("couldnt_update_site"))?;
493
494     // Mod tables
495     let form = ModAddForm {
496       mod_person_id: local_user_view.person.id,
497       other_person_id: data.person_id,
498       removed: Some(false),
499     };
500
501     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
502
503     let site_view = blocking(context.pool(), SiteView::read).await??;
504
505     let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
506     let creator_index = admins
507       .iter()
508       .position(|r| r.person.id == site_view.creator.id)
509       .context(location_info!())?;
510     let creator_person = admins.remove(creator_index);
511     admins.insert(0, creator_person);
512
513     let federated_instances = build_federated_instances(
514       context.pool(),
515       &context.settings().federation,
516       &context.settings().hostname,
517     )
518     .await?;
519
520     Ok(GetSiteResponse {
521       site_view: Some(site_view),
522       admins,
523       online: 0,
524       version: version::VERSION.to_string(),
525       my_user: None,
526       federated_instances,
527     })
528   }
529 }
530
531 #[async_trait::async_trait(?Send)]
532 impl Perform for GetSiteConfig {
533   type Response = GetSiteConfigResponse;
534
535   #[tracing::instrument(skip(context, _websocket_id))]
536   async fn perform(
537     &self,
538     context: &Data<LemmyContext>,
539     _websocket_id: Option<ConnectionId>,
540   ) -> Result<GetSiteConfigResponse, LemmyError> {
541     let data: &GetSiteConfig = self;
542     let local_user_view =
543       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
544
545     // Only let admins read this
546     is_admin(&local_user_view)?;
547
548     let config_hjson = Settings::read_config_file()?;
549
550     Ok(GetSiteConfigResponse { config_hjson })
551   }
552 }
553
554 #[async_trait::async_trait(?Send)]
555 impl Perform for SaveSiteConfig {
556   type Response = GetSiteConfigResponse;
557
558   #[tracing::instrument(skip(context, _websocket_id))]
559   async fn perform(
560     &self,
561     context: &Data<LemmyContext>,
562     _websocket_id: Option<ConnectionId>,
563   ) -> Result<GetSiteConfigResponse, LemmyError> {
564     let data: &SaveSiteConfig = self;
565     let local_user_view =
566       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
567
568     // Only let admins read this
569     is_admin(&local_user_view)?;
570
571     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
572     let config_hjson = Settings::save_config_file(&data.config_hjson)
573       .map_err(LemmyError::from)
574       .map_err(|e| e.with_message("couldnt_update_site"))?;
575
576     Ok(GetSiteConfigResponse { config_hjson })
577   }
578 }
579
580 /// Lists registration applications, filterable by undenied only.
581 #[async_trait::async_trait(?Send)]
582 impl Perform for ListRegistrationApplications {
583   type Response = ListRegistrationApplicationsResponse;
584
585   async fn perform(
586     &self,
587     context: &Data<LemmyContext>,
588     _websocket_id: Option<ConnectionId>,
589   ) -> Result<Self::Response, LemmyError> {
590     let data = self;
591     let local_user_view =
592       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
593
594     // Make sure user is an admin
595     is_admin(&local_user_view)?;
596
597     let unread_only = data.unread_only;
598     let verified_email_only = blocking(context.pool(), Site::read_simple)
599       .await??
600       .require_email_verification;
601
602     let page = data.page;
603     let limit = data.limit;
604     let registration_applications = blocking(context.pool(), move |conn| {
605       RegistrationApplicationQueryBuilder::create(conn)
606         .unread_only(unread_only)
607         .verified_email_only(verified_email_only)
608         .page(page)
609         .limit(limit)
610         .list()
611     })
612     .await??;
613
614     let res = Self::Response {
615       registration_applications,
616     };
617
618     Ok(res)
619   }
620 }
621
622 #[async_trait::async_trait(?Send)]
623 impl Perform for ApproveRegistrationApplication {
624   type Response = RegistrationApplicationResponse;
625
626   async fn perform(
627     &self,
628     context: &Data<LemmyContext>,
629     _websocket_id: Option<ConnectionId>,
630   ) -> Result<Self::Response, LemmyError> {
631     let data = self;
632     let local_user_view =
633       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
634
635     let app_id = data.id;
636
637     // Only let admins do this
638     is_admin(&local_user_view)?;
639
640     // Update the registration with reason, admin_id
641     let deny_reason = diesel_option_overwrite(&data.deny_reason);
642     let app_form = RegistrationApplicationForm {
643       admin_id: Some(local_user_view.person.id),
644       deny_reason,
645       ..RegistrationApplicationForm::default()
646     };
647
648     let registration_application = blocking(context.pool(), move |conn| {
649       RegistrationApplication::update(conn, app_id, &app_form)
650     })
651     .await??;
652
653     // Update the local_user row
654     let local_user_form = LocalUserForm {
655       accepted_application: Some(data.approve),
656       ..LocalUserForm::default()
657     };
658
659     let approved_user_id = registration_application.local_user_id;
660     blocking(context.pool(), move |conn| {
661       LocalUser::update(conn, approved_user_id, &local_user_form)
662     })
663     .await??;
664
665     if data.approve {
666       let approved_local_user_view = blocking(context.pool(), move |conn| {
667         LocalUserView::read(conn, approved_user_id)
668       })
669       .await??;
670
671       if approved_local_user_view.local_user.email.is_some() {
672         send_application_approved_email(&approved_local_user_view, &context.settings())?;
673       }
674     }
675
676     // Read the view
677     let registration_application = blocking(context.pool(), move |conn| {
678       RegistrationApplicationView::read(conn, app_id)
679     })
680     .await??;
681
682     Ok(Self::Response {
683       registration_application,
684     })
685   }
686 }
687
688 #[async_trait::async_trait(?Send)]
689 impl Perform for GetUnreadRegistrationApplicationCount {
690   type Response = GetUnreadRegistrationApplicationCountResponse;
691
692   async fn perform(
693     &self,
694     context: &Data<LemmyContext>,
695     _websocket_id: Option<ConnectionId>,
696   ) -> Result<Self::Response, LemmyError> {
697     let data = self;
698     let local_user_view =
699       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
700
701     // Only let admins do this
702     is_admin(&local_user_view)?;
703
704     let verified_email_only = blocking(context.pool(), Site::read_simple)
705       .await??
706       .require_email_verification;
707
708     let registration_applications = blocking(context.pool(), move |conn| {
709       RegistrationApplicationView::get_unread_count(conn, verified_email_only)
710     })
711     .await??;
712
713     Ok(Self::Response {
714       registration_applications,
715     })
716   }
717 }