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