feat: moderation API for pulling reported images. (#880)

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Release-As: 0.15.1
This commit is contained in:
Tom
2026-08-07 11:42:52 -07:00
committed by IAmTomahawkx
parent 4feeeb11f3
commit fe331f0dcb
3 changed files with 69 additions and 1 deletions

View File

@@ -70,6 +70,8 @@ trust_cloudflare = false
easypwned = "" easypwned = ""
# Tenor API Key # Tenor API Key
tenor_key = "" tenor_key = ""
# Admin keys for future admin api and for bonfire reported image fetching
admin_keys = []
[api.security.captcha] [api.security.captcha]
# hCaptcha configuration # hCaptcha configuration

View File

@@ -228,6 +228,7 @@ pub struct ApiSecurity {
pub trust_cloudflare: bool, pub trust_cloudflare: bool,
pub easypwned: String, pub easypwned: String,
pub tenor_key: String, pub tenor_key: String,
pub admin_keys: Vec<String>,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]

View File

@@ -5,7 +5,7 @@ use std::{
use axum::{ use axum::{
extract::{DefaultBodyLimit, Path, State}, extract::{DefaultBodyLimit, Path, State},
http::{header, Method}, http::{header, HeaderMap, Method},
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect, Response},
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
@@ -58,6 +58,7 @@ pub async fn router() -> Router<AppState> {
) )
.route("/:tag/:file_id", get(fetch_preview)) .route("/:tag/:file_id", get(fetch_preview))
.route("/:tag/:file_id/:file_name", get(fetch_file)) .route("/:tag/:file_id/:file_name", get(fetch_file))
.route("/mod/:tag/:file_id/:file_name", get(fetch_file_mod))
.layer(cors) .layer(cors)
} }
@@ -503,3 +504,67 @@ async fn fetch_file(
.into_response() .into_response()
}) })
} }
/// Fetch original file (Moderation)
///
/// This is intentionally left out of the OpenApi docs.
/// It uses the config key api.security.admin_keys to determine access. This is intended for server to server communication.
async fn fetch_file_mod(
State(db): State<Database>,
headers: HeaderMap,
Path((tag, file_id, file_name)): Path<(Tag, String, String)>,
) -> Result<Response> {
let config = revolt_config::config().await;
let token = headers
.get("X-Admin-Token")
.and_then(|value| value.to_str().ok())
.ok_or_else(|| create_error!(NotAuthenticated))?;
if !config
.api
.security
.admin_keys
.iter()
.any(|tok| tok == token)
{
return Err(create_error!(NotAuthenticated));
}
let tag: &'static str = tag.clone().into();
let file = db.fetch_attachment(tag, &file_id).await?;
// Ignore files that haven't been attached
if file.used_for.is_none() {
return Err(create_error!(NotFound));
}
// Ensure filename is correct
if file_name != file.filename {
if file_name == "original" {
let safe_filename = encode_component(&file.filename);
return Ok(
Redirect::permanent(&format!("/{tag}/{file_id}/{}", safe_filename)).into_response(),
);
}
return Err(create_error!(NotFound));
}
let hash = file.as_hash(&db).await?;
retrieve_file_by_hash(&hash).await.map(|data| {
(
[
(header::CONTENT_TYPE, hash.content_type),
(header::CONTENT_DISPOSITION, "attachment".to_owned()),
(
header::CACHE_CONTROL,
"private, max-age=300, must-revalidate".to_owned(),
),
],
data,
)
.into_response()
})
}