rb/routing/api/
comments.rs

1use crate::database::Database;
2use crate::model::{anonymous_profile, CommentCreate, DatabaseError, ResponseEdit};
3use axum::http::{HeaderMap, HeaderValue};
4use hcaptcha_no_wasm::Hcaptcha;
5use authbeam::model::{IpBlockCreate, NotificationCreate};
6use databeam::prelude::DefaultReturn;
7
8use axum::response::{IntoResponse, Redirect};
9use axum::{
10    extract::{Path, State},
11    routing::{delete, get, post, put},
12    Json, Router,
13};
14
15use axum_extra::extract::cookie::CookieJar;
16
17pub fn routes(database: Database) -> Router {
18    Router::new()
19        .route("/", post(create_request))
20        .route("/{id}", get(get_request))
21        .route("/{id}", put(edit_request))
22        .route("/{id}", delete(delete_request))
23        .route("/{id}/report", post(report_request))
24        .route("/{id}/ipblock", post(ipblock_request))
25        // ...
26        .with_state(database)
27}
28
29// routes
30
31/// [`Database::create_comment`]
32pub async fn create_request(
33    jar: CookieJar,
34    headers: HeaderMap,
35    State(database): State<Database>,
36    Json(req): Json<CommentCreate>,
37) -> impl IntoResponse {
38    // get user from token
39    let mut was_not_anonymous = false;
40
41    let auth_user = match jar.get("__Secure-Token") {
42        Some(c) => match database
43            .auth
44            .get_profile_by_unhashed(c.value_trimmed())
45            .await
46        {
47            Ok(ua) => {
48                was_not_anonymous = true;
49                ua
50            }
51            Err(_) => anonymous_profile(database.create_anonymous().0),
52        },
53        None => anonymous_profile(database.create_anonymous().0),
54    };
55
56    let existing_tag = match jar.get("__Secure-Question-Tag") {
57        Some(c) => c.value_trimmed().to_string(),
58        None => String::new(),
59    };
60
61    // get real ip
62    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
63        headers
64            .get(real_ip_header.to_owned())
65            .unwrap_or(&HeaderValue::from_static(""))
66            .to_str()
67            .unwrap_or("")
68            .to_string()
69    } else {
70        String::new()
71    };
72
73    // check ip
74    if database.auth.get_ipban_by_ip(&real_ip).await.is_ok() {
75        return (
76            [
77                ("Content-Type".to_string(), "text/plain".to_string()),
78                ("Set-Cookie".to_string(), String::new()),
79            ],
80            Json(DefaultReturn {
81                success: false,
82                message: DatabaseError::Banned.to_string(),
83                payload: None,
84            }),
85        );
86    }
87
88    // get correct username
89    let use_anonymous_anyways = req.anonymous; // this is the "Hide your name" field
90
91    if (auth_user.username == "anonymous") | use_anonymous_anyways {
92        let tag = if was_not_anonymous && use_anonymous_anyways {
93            // use real username as tag
94            format!("anonymous#{}", auth_user.id)
95        } else if !existing_tag.is_empty() {
96            // use existing tag
97            existing_tag
98        } else if !was_not_anonymous {
99            // use id as tag
100            auth_user.id
101        } else {
102            // use id as tag
103            if auth_user.username == "anonymous" {
104                auth_user.id
105            } else {
106                auth_user.id
107            }
108        };
109
110        // create as anonymous
111        return (
112            [
113                ("Content-Type".to_string(), "text/plain".to_string()),
114                (
115                    "Set-Cookie".to_string(),
116                    format!(
117                        "__Secure-Question-Tag={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
118                        tag,
119                        60 * 60 * 24 * 365
120                    ),
121                ),
122            ],
123            Json(match database.create_comment(req, tag, real_ip).await {
124                Ok(r) => DefaultReturn {
125                    success: true,
126                    message: String::new(),
127                    payload: Some(r),
128                },
129                Err(e) => e.into(),
130            }),
131        );
132    }
133
134    // ...
135    (
136        [
137            ("Content-Type".to_string(), "text/plain".to_string()),
138            (
139                "Set-Cookie".to_string(),
140                format!(
141                    "__Secure-Question-Tag={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}",
142                    auth_user.username.replace("anonymous#", ""),
143                    60 * 60 * 24 * 365
144                ),
145            ),
146        ],
147        Json(match database.create_comment(req, auth_user.id, real_ip).await {
148            Ok(r) => DefaultReturn {
149                success: true,
150                message: String::new(),
151                payload: Some(r),
152            },
153            Err(e) => e.into(),
154        })
155    )
156}
157
158/// [`Database::get_comment`]
159pub async fn get_request(
160    Path(id): Path<String>,
161    State(database): State<Database>,
162) -> impl IntoResponse {
163    Json(match database.get_comment(id, true).await {
164        Ok(mut r) => DefaultReturn {
165            success: true,
166            message: String::new(),
167            payload: {
168                r.0.author.clean();
169                Some(r)
170            },
171        },
172        Err(e) => e.into(),
173    })
174}
175
176/// Redirect to the full ID of a comment through its short ID
177pub async fn expand_request(
178    Path(id): Path<String>,
179    State(database): State<Database>,
180) -> impl IntoResponse {
181    match database.get_comment(id, false).await {
182        Ok(c) => Redirect::to(&format!("/@{}/c/{}", c.0.author.username, c.0.id)),
183        Err(_) => Redirect::to("/"),
184    }
185}
186
187/// [`Database::update_comment_content`]
188pub async fn edit_request(
189    jar: CookieJar,
190    Path(id): Path<String>,
191    State(database): State<Database>,
192    Json(req): Json<ResponseEdit>,
193) -> impl IntoResponse {
194    // get user from token
195    let auth_user = match jar.get("__Secure-Token") {
196        Some(c) => match database
197            .auth
198            .get_profile_by_unhashed(c.value_trimmed())
199            .await
200        {
201            Ok(ua) => ua,
202            Err(_) => {
203                return Json(DatabaseError::NotAllowed.into());
204            }
205        },
206        None => {
207            return Json(DatabaseError::NotAllowed.into());
208        }
209    };
210
211    // ...
212    Json(
213        match database
214            .update_comment_content(id, req.content, auth_user)
215            .await
216        {
217            Ok(r) => DefaultReturn {
218                success: true,
219                message: String::new(),
220                payload: Some(r),
221            },
222            Err(e) => e.into(),
223        },
224    )
225}
226
227/// [`Database::delete_comment`]
228pub async fn delete_request(
229    jar: CookieJar,
230    Path(id): Path<String>,
231    State(database): State<Database>,
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(match database.delete_comment(id, auth_user).await {
252        Ok(r) => DefaultReturn {
253            success: true,
254            message: String::new(),
255            payload: Some(r),
256        },
257        Err(e) => e.into(),
258    })
259}
260
261/// Report a comment
262pub async fn report_request(
263    headers: HeaderMap,
264    Path(id): Path<String>,
265    State(database): State<Database>,
266    Json(req): Json<super::CreateReport>,
267) -> impl IntoResponse {
268    // check hcaptcha
269    if let Err(e) = req
270        .valid_response(&database.config.captcha.secret, None)
271        .await
272    {
273        return Json(DefaultReturn {
274            success: false,
275            message: e.to_string(),
276            payload: (),
277        });
278    }
279
280    // get comment
281    if let Err(_) = database.get_comment(id.clone(), false).await {
282        return Json(DefaultReturn {
283            success: false,
284            message: DatabaseError::NotFound.to_string(),
285            payload: (),
286        });
287    };
288
289    // get real ip
290    let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header {
291        headers
292            .get(real_ip_header.to_owned())
293            .unwrap_or(&HeaderValue::from_static(""))
294            .to_str()
295            .unwrap_or("")
296            .to_string()
297    } else {
298        String::new()
299    };
300
301    // check ip
302    if database.auth.get_ipban_by_ip(&real_ip).await.is_ok() {
303        return Json(DefaultReturn {
304            success: false,
305            message: DatabaseError::Banned.to_string(),
306            payload: (),
307        });
308    }
309
310    // report
311    match database
312        .auth
313        .create_notification(
314            NotificationCreate {
315                title: format!("**COMMENT REPORT**: {id}"),
316                content: format!("{}\n\n***\n\n[{real_ip}](/+i/{real_ip})", req.content),
317                address: format!("/comment/{id}"),
318                recipient: "*".to_string(), // all staff
319            },
320            None,
321        )
322        .await
323    {
324        Ok(_) => {
325            return Json(DefaultReturn {
326                success: true,
327                message: "Comment reported!".to_string(),
328                payload: (),
329            })
330        }
331        Err(_) => Json(DefaultReturn {
332            success: false,
333            message: DatabaseError::NotFound.to_string(),
334            payload: (),
335        }),
336    }
337}
338
339/// IP block a comment's author
340pub async fn ipblock_request(
341    jar: CookieJar,
342    Path(id): Path<String>,
343    State(database): State<Database>,
344) -> impl IntoResponse {
345    // get user from token
346    let auth_user = match jar.get("__Secure-Token") {
347        Some(c) => match database
348            .auth
349            .get_profile_by_unhashed(c.value_trimmed())
350            .await
351        {
352            Ok(ua) => ua,
353            Err(_) => {
354                return Json(DatabaseError::NotAllowed.into());
355            }
356        },
357        None => {
358            return Json(DatabaseError::NotAllowed.into());
359        }
360    };
361
362    // get comment
363    let comment = match database.get_comment(id.clone(), false).await {
364        Ok(q) => q.0,
365        Err(e) => return Json(e.to_json()),
366    };
367
368    // block
369    match database
370        .auth
371        .create_ipblock(
372            IpBlockCreate {
373                ip: comment.ip,
374                context: comment.content,
375            },
376            auth_user,
377        )
378        .await
379    {
380        Ok(_) => {
381            return Json(DefaultReturn {
382                success: true,
383                message: "IP blocked!".to_string(),
384                payload: (),
385            })
386        }
387        Err(_) => Json(DefaultReturn {
388            success: false,
389            message: DatabaseError::Other.to_string(),
390            payload: (),
391        }),
392    }
393}