]> Untitled Git - lemmy.git/blob - crates/api/src/site.rs
~80% done
[lemmy.git] / crates / api / src / site.rs
1 use crate::{
2   build_federated_instances,
3   get_local_user_view_from_jwt,
4   get_local_user_view_from_jwt_opt,
5   get_local_user_settings_view_from_jwt_opt,
6   is_admin,
7   Perform,
8 };
9 use actix_web::web::Data;
10 use anyhow::Context;
11 use lemmy_api_structs::{blocking, site::*, person::Register};
12 use lemmy_apub::fetcher::search::search_by_apub_id;
13 use lemmy_db_queries::{
14   diesel_option_overwrite_to_url,
15   source::site::Site_,
16   Crud,
17   SearchType,
18   SortType,
19 };
20 use lemmy_db_schema::{
21   naive_now,
22   source::{
23     moderator::*,
24     site::{Site, *},
25   },
26 };
27 use lemmy_db_views::{
28   comment_view::CommentQueryBuilder,
29   post_view::PostQueryBuilder,
30   site_view::SiteView,
31 };
32 use lemmy_db_views_actor::{
33   community_view::CommunityQueryBuilder,
34   person_view::{PersonQueryBuilder, PersonViewSafe},
35 };
36 use lemmy_db_views_moderator::{
37   mod_add_community_view::ModAddCommunityView,
38   mod_add_view::ModAddView,
39   mod_ban_from_community_view::ModBanFromCommunityView,
40   mod_ban_view::ModBanView,
41   mod_lock_post_view::ModLockPostView,
42   mod_remove_comment_view::ModRemoveCommentView,
43   mod_remove_community_view::ModRemoveCommunityView,
44   mod_remove_post_view::ModRemovePostView,
45   mod_sticky_post_view::ModStickyPostView,
46 };
47 use lemmy_utils::{
48   location_info,
49   settings::structs::Settings,
50   utils::{check_slurs, check_slurs_opt},
51   version,
52   ApiError,
53   ConnectionId,
54   LemmyError,
55 };
56 use lemmy_websocket::{
57   messages::{GetUsersOnline, SendAllMessage},
58   LemmyContext,
59   UserOperation,
60 };
61 use log::{debug, info};
62 use std::str::FromStr;
63
64 #[async_trait::async_trait(?Send)]
65 impl Perform for GetModlog {
66   type Response = GetModlogResponse;
67
68   async fn perform(
69     &self,
70     context: &Data<LemmyContext>,
71     _websocket_id: Option<ConnectionId>,
72   ) -> Result<GetModlogResponse, LemmyError> {
73     let data: &GetModlog = &self;
74
75     let community_id = data.community_id;
76     let mod_person_id = data.mod_person_id;
77     let page = data.page;
78     let limit = data.limit;
79     let removed_posts = blocking(context.pool(), move |conn| {
80       ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
81     })
82     .await??;
83
84     let locked_posts = blocking(context.pool(), move |conn| {
85       ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
86     })
87     .await??;
88
89     let stickied_posts = blocking(context.pool(), move |conn| {
90       ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
91     })
92     .await??;
93
94     let removed_comments = blocking(context.pool(), move |conn| {
95       ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
96     })
97     .await??;
98
99     let banned_from_community = blocking(context.pool(), move |conn| {
100       ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
101     })
102     .await??;
103
104     let added_to_community = blocking(context.pool(), move |conn| {
105       ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
106     })
107     .await??;
108
109     // These arrays are only for the full modlog, when a community isn't given
110     let (removed_communities, banned, added) = if data.community_id.is_none() {
111       blocking(context.pool(), move |conn| {
112         Ok((
113           ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
114           ModBanView::list(conn, mod_person_id, page, limit)?,
115           ModAddView::list(conn, mod_person_id, page, limit)?,
116         )) as Result<_, LemmyError>
117       })
118       .await??
119     } else {
120       (Vec::new(), Vec::new(), Vec::new())
121     };
122
123     // Return the jwt
124     Ok(GetModlogResponse {
125       removed_posts,
126       locked_posts,
127       stickied_posts,
128       removed_comments,
129       removed_communities,
130       banned_from_community,
131       banned,
132       added_to_community,
133       added,
134     })
135   }
136 }
137
138 #[async_trait::async_trait(?Send)]
139 impl Perform for CreateSite {
140   type Response = SiteResponse;
141
142   async fn perform(
143     &self,
144     context: &Data<LemmyContext>,
145     _websocket_id: Option<ConnectionId>,
146   ) -> Result<SiteResponse, LemmyError> {
147     let data: &CreateSite = &self;
148
149     let read_site = move |conn: &'_ _| Site::read_simple(conn);
150     if blocking(context.pool(), read_site).await?.is_ok() {
151       return Err(ApiError::err("site_already_exists").into());
152     };
153
154     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
155
156     check_slurs(&data.name)?;
157     check_slurs_opt(&data.description)?;
158
159     // Make sure user is an admin
160     is_admin(&local_user_view)?;
161
162     let site_form = SiteForm {
163       name: data.name.to_owned(),
164       description: data.description.to_owned(),
165       icon: Some(data.icon.to_owned().map(|url| url.into())),
166       banner: Some(data.banner.to_owned().map(|url| url.into())),
167       creator_id: local_user_view.person.id,
168       enable_downvotes: data.enable_downvotes,
169       open_registration: data.open_registration,
170       enable_nsfw: data.enable_nsfw,
171       updated: None,
172     };
173
174     let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
175     if blocking(context.pool(), create_site).await?.is_err() {
176       return Err(ApiError::err("site_already_exists").into());
177     }
178
179     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
180
181     Ok(SiteResponse { site_view })
182   }
183 }
184
185 #[async_trait::async_trait(?Send)]
186 impl Perform for EditSite {
187   type Response = SiteResponse;
188   async fn perform(
189     &self,
190     context: &Data<LemmyContext>,
191     websocket_id: Option<ConnectionId>,
192   ) -> Result<SiteResponse, LemmyError> {
193     let data: &EditSite = &self;
194     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
195
196     check_slurs(&data.name)?;
197     check_slurs_opt(&data.description)?;
198
199     // Make sure user is an admin
200     is_admin(&local_user_view)?;
201
202     let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
203
204     let icon = diesel_option_overwrite_to_url(&data.icon)?;
205     let banner = diesel_option_overwrite_to_url(&data.banner)?;
206
207     let site_form = SiteForm {
208       name: data.name.to_owned(),
209       description: data.description.to_owned(),
210       icon,
211       banner,
212       creator_id: found_site.creator_id,
213       updated: Some(naive_now()),
214       enable_downvotes: data.enable_downvotes,
215       open_registration: data.open_registration,
216       enable_nsfw: data.enable_nsfw,
217     };
218
219     let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
220     if blocking(context.pool(), update_site).await?.is_err() {
221       return Err(ApiError::err("couldnt_update_site").into());
222     }
223
224     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
225
226     let res = SiteResponse { site_view };
227
228     context.chat_server().do_send(SendAllMessage {
229       op: UserOperation::EditSite,
230       response: res.clone(),
231       websocket_id,
232     });
233
234     Ok(res)
235   }
236 }
237
238 #[async_trait::async_trait(?Send)]
239 impl Perform for GetSite {
240   type Response = GetSiteResponse;
241
242   async fn perform(
243     &self,
244     context: &Data<LemmyContext>,
245     websocket_id: Option<ConnectionId>,
246   ) -> Result<GetSiteResponse, LemmyError> {
247     let data: &GetSite = &self;
248
249     let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
250       Ok(site_view) => Some(site_view),
251       // If the site isn't created yet, check the setup
252       Err(_) => {
253         if let Some(setup) = Settings::get().setup().as_ref() {
254           let register = Register {
255             username: setup.admin_username.to_owned(),
256             email: setup.admin_email.to_owned(),
257             password: setup.admin_password.to_owned(),
258             password_verify: setup.admin_password.to_owned(),
259             show_nsfw: true,
260             captcha_uuid: None,
261             captcha_answer: None,
262           };
263           let login_response = register.perform(context, websocket_id).await?;
264           info!("Admin {} created", setup.admin_username);
265
266           let create_site = CreateSite {
267             name: setup.site_name.to_owned(),
268             description: None,
269             icon: None,
270             banner: None,
271             enable_downvotes: true,
272             open_registration: true,
273             enable_nsfw: true,
274             auth: login_response.jwt,
275           };
276           create_site.perform(context, websocket_id).await?;
277           info!("Site {} created", setup.site_name);
278           Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
279         } else {
280           None
281         }
282       }
283     };
284
285     let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
286
287     // Make sure the site creator is the top admin
288     if let Some(site_view) = site_view.to_owned() {
289       let site_creator_id = site_view.creator.id;
290       // TODO investigate why this is sometimes coming back null
291       // Maybe user_.admin isn't being set to true?
292       if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
293         let creator_person = admins.remove(creator_index);
294         admins.insert(0, creator_person);
295       }
296     }
297
298     let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
299
300     let online = context
301       .chat_server()
302       .send(GetUsersOnline)
303       .await
304       .unwrap_or(1);
305
306     let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?;
307     let federated_instances = build_federated_instances(context.pool()).await?;
308
309     Ok(GetSiteResponse {
310       site_view,
311       admins,
312       banned,
313       online,
314       version: version::VERSION.to_string(),
315       my_user,
316       federated_instances,
317     })
318   }
319 }
320
321 #[async_trait::async_trait(?Send)]
322 impl Perform for Search {
323   type Response = SearchResponse;
324
325   async fn perform(
326     &self,
327     context: &Data<LemmyContext>,
328     _websocket_id: Option<ConnectionId>,
329   ) -> Result<SearchResponse, LemmyError> {
330     let data: &Search = &self;
331
332     match search_by_apub_id(&data.q, context).await {
333       Ok(r) => return Ok(r),
334       Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
335     }
336
337     let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
338     let person_id = local_user_view.map(|u| u.person.id);
339
340     let type_ = SearchType::from_str(&data.type_)?;
341
342     let mut posts = Vec::new();
343     let mut comments = Vec::new();
344     let mut communities = Vec::new();
345     let mut users = Vec::new();
346
347     // TODO no clean / non-nsfw searching rn
348
349     let q = data.q.to_owned();
350     let page = data.page;
351     let limit = data.limit;
352     let sort = SortType::from_str(&data.sort)?;
353     let community_id = data.community_id;
354     let community_name = data.community_name.to_owned();
355     match type_ {
356       SearchType::Posts => {
357         posts = blocking(context.pool(), move |conn| {
358           PostQueryBuilder::create(conn)
359             .sort(&sort)
360             .show_nsfw(true)
361             .community_id(community_id)
362             .community_name(community_name)
363             .my_person_id(person_id)
364             .search_term(q)
365             .page(page)
366             .limit(limit)
367             .list()
368         })
369         .await??;
370       }
371       SearchType::Comments => {
372         comments = blocking(context.pool(), move |conn| {
373           CommentQueryBuilder::create(&conn)
374             .sort(&sort)
375             .search_term(q)
376             .my_person_id(person_id)
377             .page(page)
378             .limit(limit)
379             .list()
380         })
381         .await??;
382       }
383       SearchType::Communities => {
384         communities = blocking(context.pool(), move |conn| {
385           CommunityQueryBuilder::create(conn)
386             .sort(&sort)
387             .search_term(q)
388             .my_person_id(person_id)
389             .page(page)
390             .limit(limit)
391             .list()
392         })
393         .await??;
394       }
395       SearchType::Users => {
396         users = blocking(context.pool(), move |conn| {
397           PersonQueryBuilder::create(conn)
398             .sort(&sort)
399             .search_term(q)
400             .page(page)
401             .limit(limit)
402             .list()
403         })
404         .await??;
405       }
406       SearchType::All => {
407         posts = blocking(context.pool(), move |conn| {
408           PostQueryBuilder::create(conn)
409             .sort(&sort)
410             .show_nsfw(true)
411             .community_id(community_id)
412             .community_name(community_name)
413             .my_person_id(person_id)
414             .search_term(q)
415             .page(page)
416             .limit(limit)
417             .list()
418         })
419         .await??;
420
421         let q = data.q.to_owned();
422         let sort = SortType::from_str(&data.sort)?;
423
424         comments = blocking(context.pool(), move |conn| {
425           CommentQueryBuilder::create(conn)
426             .sort(&sort)
427             .search_term(q)
428             .my_person_id(person_id)
429             .page(page)
430             .limit(limit)
431             .list()
432         })
433         .await??;
434
435         let q = data.q.to_owned();
436         let sort = SortType::from_str(&data.sort)?;
437
438         communities = blocking(context.pool(), move |conn| {
439           CommunityQueryBuilder::create(conn)
440             .sort(&sort)
441             .search_term(q)
442             .my_person_id(person_id)
443             .page(page)
444             .limit(limit)
445             .list()
446         })
447         .await??;
448
449         let q = data.q.to_owned();
450         let sort = SortType::from_str(&data.sort)?;
451
452         users = blocking(context.pool(), move |conn| {
453           PersonQueryBuilder::create(conn)
454             .sort(&sort)
455             .search_term(q)
456             .page(page)
457             .limit(limit)
458             .list()
459         })
460         .await??;
461       }
462       SearchType::Url => {
463         posts = blocking(context.pool(), move |conn| {
464           PostQueryBuilder::create(conn)
465             .sort(&sort)
466             .show_nsfw(true)
467             .my_person_id(person_id)
468             .community_id(community_id)
469             .community_name(community_name)
470             .url_search(q)
471             .page(page)
472             .limit(limit)
473             .list()
474         })
475         .await??;
476       }
477     };
478
479     // Return the jwt
480     Ok(SearchResponse {
481       type_: data.type_.to_owned(),
482       comments,
483       posts,
484       communities,
485       users,
486     })
487   }
488 }
489
490 #[async_trait::async_trait(?Send)]
491 impl Perform for TransferSite {
492   type Response = GetSiteResponse;
493
494   async fn perform(
495     &self,
496     context: &Data<LemmyContext>,
497     _websocket_id: Option<ConnectionId>,
498   ) -> Result<GetSiteResponse, LemmyError> {
499     let data: &TransferSite = &self;
500     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
501
502     is_admin(&local_user_view)?;
503
504     let read_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
505
506     // Make sure user is the creator
507     if read_site.creator_id != local_user_view.person.id {
508       return Err(ApiError::err("not_an_admin").into());
509     }
510
511     let new_creator_id = data.person_id;
512     let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
513     if blocking(context.pool(), transfer_site).await?.is_err() {
514       return Err(ApiError::err("couldnt_update_site").into());
515     };
516
517     // Mod tables
518     let form = ModAddForm {
519       mod_person_id: local_user_view.person.id,
520       other_person_id: data.person_id,
521       removed: Some(false),
522     };
523
524     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
525
526     let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
527
528     let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
529     let creator_index = admins
530       .iter()
531       .position(|r| r.person.id == site_view.creator.id)
532       .context(location_info!())?;
533     let creator_person = admins.remove(creator_index);
534     admins.insert(0, creator_person);
535
536     let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
537     let federated_instances = build_federated_instances(context.pool()).await?;
538
539     Ok(GetSiteResponse {
540       site_view: Some(site_view),
541       admins,
542       banned,
543       online: 0,
544       version: version::VERSION.to_string(),
545       my_user: Some(local_user_view),
546       federated_instances,
547     })
548   }
549 }
550
551 #[async_trait::async_trait(?Send)]
552 impl Perform for GetSiteConfig {
553   type Response = GetSiteConfigResponse;
554
555   async fn perform(
556     &self,
557     context: &Data<LemmyContext>,
558     _websocket_id: Option<ConnectionId>,
559   ) -> Result<GetSiteConfigResponse, LemmyError> {
560     let data: &GetSiteConfig = &self;
561     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
562
563     // Only let admins read this
564     is_admin(&local_user_view)?;
565
566     let config_hjson = Settings::read_config_file()?;
567
568     Ok(GetSiteConfigResponse { config_hjson })
569   }
570 }
571
572 #[async_trait::async_trait(?Send)]
573 impl Perform for SaveSiteConfig {
574   type Response = GetSiteConfigResponse;
575
576   async fn perform(
577     &self,
578     context: &Data<LemmyContext>,
579     _websocket_id: Option<ConnectionId>,
580   ) -> Result<GetSiteConfigResponse, LemmyError> {
581     let data: &SaveSiteConfig = &self;
582     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
583
584     // Only let admins read this
585     let person_id = local_user_view.person.id;
586     is_admin(&local_user_view)?;
587
588     // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
589     let config_hjson = match Settings::save_config_file(&data.config_hjson) {
590       Ok(config_hjson) => config_hjson,
591       Err(_e) => return Err(ApiError::err("couldnt_update_site").into()),
592     };
593
594     Ok(GetSiteConfigResponse { config_hjson })
595   }
596 }