]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
Make webfinger standard compliant
[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   get_local_user_view_from_jwt,
9   get_local_user_view_from_jwt_opt,
10   is_admin,
11   site::*,
12 };
13 use lemmy_apub::{
14   fetcher::{
15     search::{search_by_apub_id, SearchableObjects},
16     webfinger::webfinger_resolve,
17   },
18   objects::community::ApubCommunity,
19   EndpointType,
20 };
21 use lemmy_db_schema::{
22   from_opt_str_to_opt_enum,
23   newtypes::PersonId,
24   source::{moderator::*, site::Site},
25   traits::{Crud, DeleteableOrRemoveable},
26   DbPool,
27   ListingType,
28   SearchType,
29   SortType,
30 };
31 use lemmy_db_views::{
32   comment_view::{CommentQueryBuilder, CommentView},
33   post_view::{PostQueryBuilder, PostView},
34   site_view::SiteView,
35 };
36 use lemmy_db_views_actor::{
37   community_view::{CommunityQueryBuilder, CommunityView},
38   person_view::{PersonQueryBuilder, PersonViewSafe},
39 };
40 use lemmy_db_views_moderator::{
41   mod_add_community_view::ModAddCommunityView,
42   mod_add_view::ModAddView,
43   mod_ban_from_community_view::ModBanFromCommunityView,
44   mod_ban_view::ModBanView,
45   mod_lock_post_view::ModLockPostView,
46   mod_remove_comment_view::ModRemoveCommentView,
47   mod_remove_community_view::ModRemoveCommunityView,
48   mod_remove_post_view::ModRemovePostView,
49   mod_sticky_post_view::ModStickyPostView,
50   mod_transfer_community_view::ModTransferCommunityView,
51 };
52 use lemmy_utils::{
53   location_info,
54   settings::structs::Settings,
55   version,
56   ApiError,
57   ConnectionId,
58   LemmyError,
59 };
60 use lemmy_websocket::LemmyContext;
61
62 #[async_trait::async_trait(?Send)]
63 impl Perform for GetModlog {
64   type Response = GetModlogResponse;
65
66   async fn perform(
67     &self,
68     context: &Data<LemmyContext>,
69     _websocket_id: Option<ConnectionId>,
70   ) -> Result<GetModlogResponse, LemmyError> {
71     let data: &GetModlog = self;
72
73     let community_id = data.community_id;
74     let mod_person_id = data.mod_person_id;
75     let page = data.page;
76     let limit = data.limit;
77     let removed_posts = blocking(context.pool(), move |conn| {
78       ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
79     })
80     .await??;
81
82     let locked_posts = blocking(context.pool(), move |conn| {
83       ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
84     })
85     .await??;
86
87     let stickied_posts = blocking(context.pool(), move |conn| {
88       ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
89     })
90     .await??;
91
92     let removed_comments = blocking(context.pool(), move |conn| {
93       ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
94     })
95     .await??;
96
97     let banned_from_community = blocking(context.pool(), move |conn| {
98       ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
99     })
100     .await??;
101
102     let added_to_community = blocking(context.pool(), move |conn| {
103       ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
104     })
105     .await??;
106
107     let transferred_to_community = blocking(context.pool(), move |conn| {
108       ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit)
109     })
110     .await??;
111
112     // These arrays are only for the full modlog, when a community isn't given
113     let (removed_communities, banned, added) = if data.community_id.is_none() {
114       blocking(context.pool(), move |conn| {
115         Ok((
116           ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
117           ModBanView::list(conn, mod_person_id, page, limit)?,
118           ModAddView::list(conn, mod_person_id, page, limit)?,
119         )) as Result<_, LemmyError>
120       })
121       .await??
122     } else {
123       (Vec::new(), Vec::new(), Vec::new())
124     };
125
126     // Return the jwt
127     Ok(GetModlogResponse {
128       removed_posts,
129       locked_posts,
130       stickied_posts,
131       removed_comments,
132       removed_communities,
133       banned_from_community,
134       banned,
135       added_to_community,
136       added,
137       transferred_to_community,
138     })
139   }
140 }
141
142 #[async_trait::async_trait(?Send)]
143 impl Perform for Search {
144   type Response = SearchResponse;
145
146   async fn perform(
147     &self,
148     context: &Data<LemmyContext>,
149     _websocket_id: Option<ConnectionId>,
150   ) -> Result<SearchResponse, LemmyError> {
151     let data: &Search = self;
152
153     let local_user_view =
154       get_local_user_view_from_jwt_opt(&data.auth, context.pool(), context.secret()).await?;
155
156     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
157     let show_bot_accounts = local_user_view
158       .as_ref()
159       .map(|t| t.local_user.show_bot_accounts);
160     let show_read_posts = local_user_view
161       .as_ref()
162       .map(|t| t.local_user.show_read_posts);
163
164     let person_id = local_user_view.map(|u| u.person.id);
165
166     let mut posts = Vec::new();
167     let mut comments = Vec::new();
168     let mut communities = Vec::new();
169     let mut users = Vec::new();
170
171     // TODO no clean / non-nsfw searching rn
172
173     let q = data.q.to_owned();
174     let page = data.page;
175     let limit = data.limit;
176     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
177     let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.listing_type);
178     let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All);
179     let community_id = data.community_id;
180     let community_actor_id = if let Some(name) = &data.community_name {
181       webfinger_resolve::<ApubCommunity>(name, EndpointType::Community, context, &mut 0)
182         .await
183         .ok()
184     } else {
185       None
186     };
187     let creator_id = data.creator_id;
188     match search_type {
189       SearchType::Posts => {
190         posts = blocking(context.pool(), move |conn| {
191           PostQueryBuilder::create(conn)
192             .sort(sort)
193             .show_nsfw(show_nsfw)
194             .show_bot_accounts(show_bot_accounts)
195             .show_read_posts(show_read_posts)
196             .listing_type(listing_type)
197             .community_id(community_id)
198             .community_actor_id(community_actor_id)
199             .creator_id(creator_id)
200             .my_person_id(person_id)
201             .search_term(q)
202             .page(page)
203             .limit(limit)
204             .list()
205         })
206         .await??;
207       }
208       SearchType::Comments => {
209         comments = blocking(context.pool(), move |conn| {
210           CommentQueryBuilder::create(conn)
211             .sort(sort)
212             .listing_type(listing_type)
213             .search_term(q)
214             .show_bot_accounts(show_bot_accounts)
215             .community_id(community_id)
216             .community_actor_id(community_actor_id)
217             .creator_id(creator_id)
218             .my_person_id(person_id)
219             .page(page)
220             .limit(limit)
221             .list()
222         })
223         .await??;
224       }
225       SearchType::Communities => {
226         communities = blocking(context.pool(), move |conn| {
227           CommunityQueryBuilder::create(conn)
228             .sort(sort)
229             .listing_type(listing_type)
230             .search_term(q)
231             .my_person_id(person_id)
232             .page(page)
233             .limit(limit)
234             .list()
235         })
236         .await??;
237       }
238       SearchType::Users => {
239         users = blocking(context.pool(), move |conn| {
240           PersonQueryBuilder::create(conn)
241             .sort(sort)
242             .search_term(q)
243             .page(page)
244             .limit(limit)
245             .list()
246         })
247         .await??;
248       }
249       SearchType::All => {
250         // If the community or creator is included, dont search communities or users
251         let community_or_creator_included =
252           data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
253         let community_actor_id_2 = community_actor_id.to_owned();
254
255         posts = blocking(context.pool(), move |conn| {
256           PostQueryBuilder::create(conn)
257             .sort(sort)
258             .show_nsfw(show_nsfw)
259             .show_bot_accounts(show_bot_accounts)
260             .show_read_posts(show_read_posts)
261             .listing_type(listing_type)
262             .community_id(community_id)
263             .community_actor_id(community_actor_id_2)
264             .creator_id(creator_id)
265             .my_person_id(person_id)
266             .search_term(q)
267             .page(page)
268             .limit(limit)
269             .list()
270         })
271         .await??;
272
273         let q = data.q.to_owned();
274         let community_actor_id = community_actor_id.to_owned();
275
276         comments = blocking(context.pool(), move |conn| {
277           CommentQueryBuilder::create(conn)
278             .sort(sort)
279             .listing_type(listing_type)
280             .search_term(q)
281             .show_bot_accounts(show_bot_accounts)
282             .community_id(community_id)
283             .community_actor_id(community_actor_id)
284             .creator_id(creator_id)
285             .my_person_id(person_id)
286             .page(page)
287             .limit(limit)
288             .list()
289         })
290         .await??;
291
292         let q = data.q.to_owned();
293
294         communities = if community_or_creator_included {
295           vec![]
296         } else {
297           blocking(context.pool(), move |conn| {
298             CommunityQueryBuilder::create(conn)
299               .sort(sort)
300               .listing_type(listing_type)
301               .search_term(q)
302               .my_person_id(person_id)
303               .page(page)
304               .limit(limit)
305               .list()
306           })
307           .await??
308         };
309
310         let q = data.q.to_owned();
311
312         users = if community_or_creator_included {
313           vec![]
314         } else {
315           blocking(context.pool(), move |conn| {
316             PersonQueryBuilder::create(conn)
317               .sort(sort)
318               .search_term(q)
319               .page(page)
320               .limit(limit)
321               .list()
322           })
323           .await??
324         };
325       }
326       SearchType::Url => {
327         posts = blocking(context.pool(), move |conn| {
328           PostQueryBuilder::create(conn)
329             .sort(sort)
330             .show_nsfw(show_nsfw)
331             .show_bot_accounts(show_bot_accounts)
332             .show_read_posts(show_read_posts)
333             .listing_type(listing_type)
334             .my_person_id(person_id)
335             .community_id(community_id)
336             .community_actor_id(community_actor_id)
337             .creator_id(creator_id)
338             .url_search(q)
339             .page(page)
340             .limit(limit)
341             .list()
342         })
343         .await??;
344       }
345     };
346
347     // Blank out deleted or removed info for non logged in users
348     if person_id.is_none() {
349       for cv in communities
350         .iter_mut()
351         .filter(|cv| cv.community.deleted || cv.community.removed)
352       {
353         cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
354       }
355
356       for pv in posts
357         .iter_mut()
358         .filter(|p| p.post.deleted || p.post.removed)
359       {
360         pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
361       }
362
363       for cv in comments
364         .iter_mut()
365         .filter(|cv| cv.comment.deleted || cv.comment.removed)
366       {
367         cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
368       }
369     }
370
371     // Return the jwt
372     Ok(SearchResponse {
373       type_: search_type.to_string(),
374       comments,
375       posts,
376       communities,
377       users,
378     })
379   }
380 }
381
382 #[async_trait::async_trait(?Send)]
383 impl Perform for ResolveObject {
384   type Response = ResolveObjectResponse;
385
386   async fn perform(
387     &self,
388     context: &Data<LemmyContext>,
389     _websocket_id: Option<ConnectionId>,
390   ) -> Result<ResolveObjectResponse, LemmyError> {
391     let local_user_view =
392       get_local_user_view_from_jwt_opt(&self.auth, context.pool(), context.secret()).await?;
393     let res = search_by_apub_id(&self.q, context)
394       .await
395       .map_err(|e| ApiError::err("couldnt_find_object", e))?;
396     convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
397       .await
398       .map_err(|e| ApiError::err("couldnt_find_object", e).into())
399   }
400 }
401
402 async fn convert_response(
403   object: SearchableObjects,
404   user_id: Option<PersonId>,
405   pool: &DbPool,
406 ) -> Result<ResolveObjectResponse, LemmyError> {
407   let removed_or_deleted;
408   let mut res = ResolveObjectResponse {
409     comment: None,
410     post: None,
411     community: None,
412     person: None,
413   };
414   use SearchableObjects::*;
415   match object {
416     Person(p) => {
417       removed_or_deleted = p.deleted;
418       res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
419     }
420     Community(c) => {
421       removed_or_deleted = c.deleted || c.removed;
422       res.community =
423         Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
424     }
425     Post(p) => {
426       removed_or_deleted = p.deleted || p.removed;
427       res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
428     }
429     Comment(c) => {
430       removed_or_deleted = c.deleted || c.removed;
431       res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
432     }
433   };
434   // if the object was deleted from database, dont return it
435   if removed_or_deleted {
436     return Err(NotFound {}.into());
437   }
438   Ok(res)
439 }
440
441 #[async_trait::async_trait(?Send)]
442 impl Perform for TransferSite {
443   type Response = GetSiteResponse;
444
445   async fn perform(
446     &self,
447     context: &Data<LemmyContext>,
448     _websocket_id: Option<ConnectionId>,
449   ) -> Result<GetSiteResponse, LemmyError> {
450     let data: &TransferSite = self;
451     let local_user_view =
452       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
453
454     is_admin(&local_user_view)?;
455
456     let read_site = blocking(context.pool(), Site::read_simple).await??;
457
458     // Make sure user is the creator
459     if read_site.creator_id != local_user_view.person.id {
460       return Err(ApiError::err_plain("not_an_admin").into());
461     }
462
463     let new_creator_id = data.person_id;
464     let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
465     blocking(context.pool(), transfer_site)
466       .await?
467       .map_err(|e| ApiError::err("couldnt_update_site", e))?;
468
469     // Mod tables
470     let form = ModAddForm {
471       mod_person_id: local_user_view.person.id,
472       other_person_id: data.person_id,
473       removed: Some(false),
474     };
475
476     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
477
478     let site_view = blocking(context.pool(), SiteView::read).await??;
479
480     let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
481     let creator_index = admins
482       .iter()
483       .position(|r| r.person.id == site_view.creator.id)
484       .context(location_info!())?;
485     let creator_person = admins.remove(creator_index);
486     admins.insert(0, creator_person);
487
488     let banned = blocking(context.pool(), PersonViewSafe::banned).await??;
489     let federated_instances = build_federated_instances(
490       context.pool(),
491       &context.settings().federation,
492       &context.settings().hostname,
493     )
494     .await?;
495
496     Ok(GetSiteResponse {
497       site_view: Some(site_view),
498       admins,
499       banned,
500       online: 0,
501       version: version::VERSION.to_string(),
502       my_user: None,
503       federated_instances,
504     })
505   }
506 }
507
508 #[async_trait::async_trait(?Send)]
509 impl Perform for GetSiteConfig {
510   type Response = GetSiteConfigResponse;
511
512   async fn perform(
513     &self,
514     context: &Data<LemmyContext>,
515     _websocket_id: Option<ConnectionId>,
516   ) -> Result<GetSiteConfigResponse, LemmyError> {
517     let data: &GetSiteConfig = self;
518     let local_user_view =
519       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
520
521     // Only let admins read this
522     is_admin(&local_user_view)?;
523
524     let config_hjson = Settings::read_config_file()?;
525
526     Ok(GetSiteConfigResponse { config_hjson })
527   }
528 }
529
530 #[async_trait::async_trait(?Send)]
531 impl Perform for SaveSiteConfig {
532   type Response = GetSiteConfigResponse;
533
534   async fn perform(
535     &self,
536     context: &Data<LemmyContext>,
537     _websocket_id: Option<ConnectionId>,
538   ) -> Result<GetSiteConfigResponse, LemmyError> {
539     let data: &SaveSiteConfig = self;
540     let local_user_view =
541       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
542
543     // Only let admins read this
544     is_admin(&local_user_view)?;
545
546     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
547     let config_hjson = Settings::save_config_file(&data.config_hjson)
548       .map_err(|e| ApiError::err("couldnt_update_site", e))?;
549
550     Ok(GetSiteConfigResponse { config_hjson })
551   }
552 }