rb/routing/api/
responses.rs

1use crate::database::Database;
2use crate::model::{DatabaseError, ResponseCreate, ResponseEdit, ResponseEditTags, ResponseEditContext};
3use crate::routing::pages::PaginatedQuery;
4use axum::extract::Query;
5use axum::http::{HeaderMap, HeaderValue};
6use hcaptcha_no_wasm::Hcaptcha;
7use authbeam::model::NotificationCreate;
8use databeam::prelude::DefaultReturn;
9
10use axum::response::{IntoResponse, Redirect};
11use axum::{
12    extract::{Path, State},
13    routing::{delete, get, post},
14    Json, Router,
15};
16
17use axum_extra::extract::cookie::CookieJar;
18use rainbeam::model::{ResponseDeleteMultiple, ResponseEditTagsMultiple, ResponseEditWarning};
19
20pub fn routes(database: Database) -> Router {
21    Router::new()
22        .route("/", post(create_request))
23        .route("/{id}", get(get_request))
24        .route("/{id}", post(edit_request))
25        .route("/{id}/tags", post(edit_tags_request))
26        .route("/{id}/context", post(edit_context_request))
27        .route("/{id}/context/warning", post(edit_warning_request))
28        .route("/{id}", delete(delete_request))
29        .route("/{id}/unsend", post(unsend_request))
30        .route("/{id}/report", post(report_request))
31        .route("/mass/tags", post(edit_tags_multiple_request))
32        .route("/mass/delete", post(delete_multiple_request))
33        // timelines
34        .route("/timeline/home", get(home_timeline_request))
35        // ...
36        .with_state(database)
37}
38
39// routes
40
41/// [`Database::create_response`]
42pub async fn create_request(
43    jar: CookieJar,
44    State(database): State<Database>,
45    Json(req): Json<ResponseCreate>,
46) -> impl IntoResponse {
47    // get user from token
48    let auth_user = match jar.get("__Secure-Token") {
49        Some(c) => match database
50            .auth
51            .get_profile_by_unhashed(c.value_trimmed())
52            .await
53        {
54            Ok(ua) => ua.id,
55            Err(_) => return Json(DatabaseError::NotAllowed.into()),
56        },
57        None => return Json(DatabaseError::NotAllowed.into()),
58    };
59
60    // ...
61    Json(match database.create_response(req, auth_user).await {
62        Ok(r) => DefaultReturn {
63            success: true,
64            message: String::new(),
65            payload: Some(r),
66        },
67        Err(e) => e.into(),
68    })
69}
70
71/// [`Database::get_response`]
72pub async fn get_request(
73    Path(id): Path<String>,
74    State(database): State<Database>,
75) -> impl IntoResponse {
76    Json(match database.get_response(id).await {
77        Ok(mut r) => DefaultReturn {
78            success: true,
79            message: String::new(),
80            payload: {
81                // hide anonymous author id
82                if r.0.author.id.starts_with("anonymous#") {
83                    r.0.author.id = "anonymous".to_string()
84                }
85
86                // hide tokens, password, salt, and metadata
87                r.0.author.clean();
88                r.0.recipient.clean();
89                r.1.author.clean();
90
91                // return
92                Some(r)
93            },
94        },
95        Err(e) => e.into(),
96    })
97}
98
99/// Redirect to the full ID of a response through its short ID
100pub async fn expand_request(
101    Path(id): Path<String>,
102    State(database): State<Database>,
103) -> impl IntoResponse {
104    match database.get_response(id).await {
105        Ok(r) => Redirect::to(&format!("/@{}/r/{}", r.1.author.username, r.1.id)),
106        Err(_) => Redirect::to("/"),
107    }
108}
109
110/// [`Database::update_response_content`]
111pub async fn edit_request(
112    jar: CookieJar,
113    Path(id): Path<String>,
114    State(database): State<Database>,
115    Json(req): Json<ResponseEdit>,
116) -> impl IntoResponse {
117    // get user from token
118    let auth_user = match jar.get("__Secure-Token") {
119        Some(c) => match database
120            .auth
121            .get_profile_by_unhashed(c.value_trimmed())
122            .await
123        {
124            Ok(ua) => ua,
125            Err(_) => {
126                return Json(DatabaseError::NotAllowed.into());
127            }
128        },
129        None => {
130            return Json(DatabaseError::NotAllowed.into());
131        }
132    };
133
134    // ...
135    Json(
136        match database
137            .update_response_content(id, req.content, auth_user)
138            .await
139        {
140            Ok(r) => DefaultReturn {
141                success: true,
142                message: String::new(),
143                payload: Some(r),
144            },
145            Err(e) => e.into(),
146        },
147    )
148}
149
150/// [`Database::update_response_tags`]
151pub async fn edit_tags_request(
152    jar: CookieJar,
153    Path(id): Path<String>,
154    State(database): State<Database>,
155    Json(req): Json<ResponseEditTags>,
156) -> impl IntoResponse {
157    // get user from token
158    let auth_user = match jar.get("__Secure-Token") {
159        Some(c) => match database
160            .auth
161            .get_profile_by_unhashed(c.value_trimmed())
162            .await
163        {
164            Ok(ua) => ua,
165            Err(_) => {
166                return Json(DatabaseError::NotAllowed.into());
167            }
168        },
169        None => {
170            return Json(DatabaseError::NotAllowed.into());
171        }
172    };
173
174    // ...
175    Json(
176        match database.update_response_tags(id, req.tags, auth_user).await {
177            Ok(_) => DefaultReturn {
178                success: true,
179                message: "Response updated!".to_string(),
180                payload: (),
181            },
182            Err(e) => e.into(),
183        },
184    )
185}
186
187/// [`Database::update_response_tags_multiple`]
188pub async fn edit_tags_multiple_request(
189    jar: CookieJar,
190    State(database): State<Database>,
191    Json(req): Json<ResponseEditTagsMultiple>,
192) -> impl IntoResponse {
193    // get user from token
194    let auth_user = match jar.get("__Secure-Token") {
195        Some(c) => match database
196            .auth
197            .get_profile_by_unhashed(c.value_trimmed())
198            .await
199        {
200            Ok(ua) => ua,
201            Err(_) => {
202                return Json(DatabaseError::NotAllowed.into());
203            }
204        },
205        None => {
206            return Json(DatabaseError::NotAllowed.into());
207        }
208    };
209
210    // ...
211    Json(
212        match database
213            .update_response_tags_multiple(req.ids, req.tags, auth_user)
214            .await
215        {
216            Ok(_) => DefaultReturn {
217                success: true,
218                message: "Responses updated!".to_string(),
219                payload: (),
220            },
221            Err(e) => e.into(),
222        },
223    )
224}
225
226/// [`Database::update_response_context`]
227pub async fn edit_context_request(
228    jar: CookieJar,
229    Path(id): Path<String>,
230    State(database): State<Database>,
231    Json(req): Json<ResponseEditContext>,
232) -> impl IntoResponse {
233    // get user from token
234    let auth_user = match jar.get("__Secure-Token") {
235        Some(c) => match database
236            .auth
237            .get_profile_by_unhashed(c.value_trimmed())
238            .await
239        {
240            Ok(ua) => ua,
241            Err(_) => {
242                return Json(DatabaseError::NotAllowed.into());
243            }
244        },
245        None => {
246            return Json(DatabaseError::NotAllowed.into());
247        }
248    };
249
250    // ...
251    Json(
252        match database
253            .update_response_context(id, req.context, auth_user)
254            .await
255        {
256            Ok(r) => DefaultReturn {
257                success: true,
258                message: String::new(),
259                payload: Some(r),
260            },
261            Err(e) => e.into(),
262        },
263    )
264}
265
266/// [`Database::update_response_context`]
267pub async fn edit_warning_request(
268    jar: CookieJar,
269    Path(id): Path<String>,
270    State(database): State<Database>,
271    Json(req): Json<ResponseEditWarning>,
272) -> impl IntoResponse {
273    // get user from token
274    let auth_user = match jar.get("__Secure-Token") {
275        Some(c) => match database
276            .auth
277            .get_profile_by_unhashed(c.value_trimmed())
278            .await
279        {
280            Ok(ua) => ua,
281            Err(_) => {
282                return Json(DatabaseError::NotAllowed.into());
283            }
284        },
285        None => {
286            return Json(DatabaseError::NotAllowed.into());
287        }
288    };
289
290    // make sure the response exists
291    let response = match database.get_response_short(id.clone()).await {
292        Ok(q) => q,
293        Err(e) => return Json(e.into()),
294    };
295
296    let mut context = response.context;
297    context.warning = req.warning;
298
299    // ...
300    Json(
301        match database
302            .update_response_context(id, context, auth_user)
303            .await
304        {
305            Ok(r) => DefaultReturn {
306                success: true,
307                message: String::new(),
308                payload: Some(r),
309            },
310            Err(e) => e.into(),
311        },
312    )
313}
314
315/// [`Database::delete_response`]
316pub async fn delete_request(
317    jar: CookieJar,
318    Path(id): Path<String>,
319    State(database): State<Database>,
320) -> impl IntoResponse {
321    // get user from token
322    let auth_user = match jar.get("__Secure-Token") {
323        Some(c) => match database
324            .auth
325            .get_profile_by_unhashed(c.value_trimmed())
326            .await
327        {
328            Ok(ua) => ua,
329            Err(_) => {
330                return Json(DatabaseError::NotAllowed.into());
331            }
332        },
333        None => {
334            return Json(DatabaseError::NotAllowed.into());
335        }
336    };
337
338    // ...
339    Json(match database.delete_response(id, auth_user, false).await {
340        Ok(r) => DefaultReturn {
341            success: true,
342            message: String::new(),
343            payload: Some(r),
344        },
345        Err(e) => e.into(),
346    })
347}
348
349/// [`Database::delete_response_multiple`]
350pub async fn delete_multiple_request(
351    jar: CookieJar,
352    State(database): State<Database>,
353    Json(req): Json<ResponseDeleteMultiple>,
354) -> impl IntoResponse {
355    // get user from token
356    let auth_user = match jar.get("__Secure-Token") {
357        Some(c) => match database
358            .auth
359            .get_profile_by_unhashed(c.value_trimmed())
360            .await
361        {
362            Ok(ua) => ua,
363            Err(_) => {
364                return Json(DatabaseError::NotAllowed.into());
365            }
366        },
367        None => {
368            return Json(DatabaseError::NotAllowed.into());
369        }
370    };
371
372    // ...
373    Json(
374        match database.delete_response_multiple(req.ids, auth_user).await {
375            Ok(_) => DefaultReturn {
376                success: true,
377                message: "Responses delete!".to_string(),
378                payload: (),
379            },
380            Err(e) => e.into(),
381        },
382    )
383}
384
385/// [`Database::unsend_response`]
386pub async fn unsend_request(
387    jar: CookieJar,
388    Path(id): Path<String>,
389    State(database): State<Database>,
390) -> impl IntoResponse {
391    // get user from token
392    let auth_user = match jar.get("__Secure-Token") {
393        Some(c) => match database
394            .auth
395            .get_profile_by_unhashed(c.value_trimmed())
396            .await
397        {
398            Ok(ua) => ua,
399            Err(_) => {
400                return Json(DatabaseError::NotAllowed.into());
401            }
402        },
403        None => {
404            return Json(DatabaseError::NotAllowed.into());
405        }
406    };
407
408    // ...
409    Json(match database.unsend_response(id, auth_user).await {
410        Ok(r) => DefaultReturn {
411            success: true,
412            message: String::new(),
413            payload: Some(r),
414        },
415        Err(e) => e.into(),
416    })
417}
418
419/// Report a response
420pub async fn report_request(
421    headers: HeaderMap,
422    Path(id): Path<String>,
423    State(database): State<Database>,
424    Json(req): Json<super::CreateReport>,
425) -> impl IntoResponse {
426    // check hcaptcha
427    if let Err(e) = req
428        .valid_response(&database.config.captcha.secret, None)
429        .await
430    {
431        return Json(DefaultReturn {
432            success: false,
433            message: e.to_string(),
434            payload: (),
435        });
436    }
437
438    // get response
439    if let Err(_) = database.get_response(id.clone()).await {
440        return Json(DefaultReturn {
441            success: false,
442            message: DatabaseError::NotFound.to_string(),
443            payload: (),
444        });
445    };
446
447    // get real ip
448    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
449        headers
450            .get(real_ip_header.to_owned())
451            .unwrap_or(&HeaderValue::from_static(""))
452            .to_str()
453            .unwrap_or("")
454            .to_string()
455    } else {
456        String::new()
457    };
458
459    // check ip
460    if database.auth.get_ipban_by_ip(&real_ip).await.is_ok() {
461        return Json(DefaultReturn {
462            success: false,
463            message: DatabaseError::Banned.to_string(),
464            payload: (),
465        });
466    }
467
468    // report
469    match database
470        .auth
471        .create_notification(
472            NotificationCreate {
473                title: format!("**RESPONSE REPORT**: {id}"),
474                content: format!("{}\n\n***\n\n[{real_ip}](/+i/{real_ip})", req.content),
475                address: format!("/response/{id}"),
476                recipient: "*".to_string(), // all staff
477            },
478            None,
479        )
480        .await
481    {
482        Ok(_) => {
483            return Json(DefaultReturn {
484                success: true,
485                message: "Response reported!".to_string(),
486                payload: (),
487            })
488        }
489        Err(_) => Json(DefaultReturn {
490            success: false,
491            message: DatabaseError::NotFound.to_string(),
492            payload: (),
493        }),
494    }
495}
496
497/// Home timeline request ("/")
498pub async fn home_timeline_request(
499    jar: CookieJar,
500    State(database): State<Database>,
501    Query(props): Query<PaginatedQuery>,
502) -> impl IntoResponse {
503    // get user from token
504    let auth_user = match jar.get("__Secure-Token") {
505        Some(c) => match database
506            .auth
507            .get_profile_by_unhashed(c.value_trimmed())
508            .await
509        {
510            Ok(ua) => ua,
511            Err(_) => {
512                return Json(DatabaseError::NotAllowed.into());
513            }
514        },
515        None => {
516            return Json(DatabaseError::NotAllowed.into());
517        }
518    };
519
520    // ...
521    Json(
522        match database
523            .get_responses_by_following_paginated(&auth_user.id, props.page)
524            .await
525        {
526            Ok(mut r) => {
527                for response in &mut r {
528                    response.1.author.clean();
529                    response.0.recipient.clean();
530                    response.0.author.clean();
531                }
532
533                DefaultReturn {
534                    success: true,
535                    message: String::new(),
536                    payload: Some(r),
537                }
538            }
539            Err(e) => e.into(),
540        },
541    )
542}