use askama::Template; use askama_web::WebTemplate; use axum::extract::State; use axum::http::HeaderMap; use axum::response::{IntoResponse, Response}; use axum::Form; use serde::Deserialize; use crate::page::PageContext; use crate::state::AppState; #[derive(Deserialize)] pub struct ReportQuery { pub url: Option, } #[derive(Template, WebTemplate)] #[template(path = "report.html")] pub struct ReportTemplate { pub page: PageContext, pub is_admin: bool, pub prefill_url: String, } #[derive(Template, WebTemplate)] #[template(path = "report_submitted.html")] pub struct ReportSubmittedTemplate { pub page: PageContext, pub is_admin: bool, pub report_id: i64, } pub async fn form( headers: HeaderMap, State(state): State, axum::extract::Query(q): axum::extract::Query, ) -> ReportTemplate { let page = PageContext::from_request(&headers, &state.announcement); ReportTemplate { page, is_admin: false, prefill_url: q.url.unwrap_or_default(), } } #[derive(Deserialize)] pub struct ReportForm { content_url: String, reason: String, description: Option, reporter_email: Option, } fn parse_content_ref(url: &str) -> Option<(&'static str, String)> { if let Some(id) = url .trim_end_matches('/') .strip_prefix("https://irc.pics/") .or_else(|| url.trim_end_matches('/').strip_prefix("http://irc.pics/")) { let id = id.split('/').next().unwrap_or(id); if !id.is_empty() && !id.starts_with("auth") && !id.starts_with("api") { return Some(("image", id.to_string())); } } if let Some(id) = url .trim_end_matches('/') .strip_prefix("https://txt.irc.now/") .or_else(|| url.trim_end_matches('/').strip_prefix("http://txt.irc.now/")) { let id = id.split('/').next().unwrap_or(id); if !id.is_empty() && !id.starts_with("auth") && !id.starts_with("api") { return Some(("paste", id.to_string())); } } None } pub async fn submit( headers: HeaderMap, State(state): State, Form(form): Form, ) -> Response { let page = PageContext::from_request(&headers, &state.announcement); if form.content_url.is_empty() || form.reason.is_empty() { return ReportTemplate { page, is_admin: false, prefill_url: form.content_url, } .into_response(); } let valid_reasons = ["csam", "illegal", "harassment", "copyright", "spam", "other"]; if !valid_reasons.contains(&form.reason.as_str()) { return ReportTemplate { page, is_admin: false, prefill_url: form.content_url, } .into_response(); } let description = form .description .as_deref() .map(|d| &d[..d.len().min(2000)]) .filter(|d| !d.trim().is_empty()); let reporter_email = form .reporter_email .as_deref() .filter(|e| !e.trim().is_empty()); let (content_type, content_id) = match parse_content_ref(&form.content_url) { Some(r) => r, None => { return ReportTemplate { page, is_admin: false, prefill_url: form.content_url, } .into_response(); } }; let result = sqlx::query_scalar::<_, i64>( "INSERT INTO abuse_reports (content_type, content_id, content_url, reporter_email, reason, description) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id", ) .bind(content_type) .bind(&content_id) .bind(&form.content_url) .bind(reporter_email) .bind(&form.reason) .bind(description) .fetch_one(&state.db) .await; match result { Ok(report_id) => { tracing::info!("abuse report #{report_id} submitted for {content_type}/{content_id}"); ReportSubmittedTemplate { page, is_admin: false, report_id }.into_response() } Err(e) => { tracing::error!("abuse report insert failed: {e}"); ReportTemplate { page, is_admin: false, prefill_url: form.content_url, } .into_response() } } }