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