Something went wrong. Try again.
fork of hydrant
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159use axum::Router;
use crate::control::Hydrant;
#[cfg(feature = "indexer")]use crate::types::{DeleteBodiesReport, DeleteBodyTarget};#[cfg(feature = "indexer")]use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, routing::post,};#[cfg(feature = "indexer")]use jacquard_common::types::{did::Did, nsid::Nsid, string::Rkey};#[cfg(feature = "indexer")]use serde::Deserialize;
#[cfg(feature = "indexer")]#[derive(Deserialize, Debug, Default)]pub struct RedactionQueryParams { #[serde(default)] pub target: Option<DeleteBodyTarget>, #[serde(default)] pub exclude: Option<bool>,}
#[cfg(feature = "indexer")]#[derive(Debug, PartialEq, Eq)]pub enum RedactionTarget { Repo(Did), Record { did: Did, collection: String, rkey: String, },}
#[cfg(feature = "indexer")]pub fn parse_redaction_target(raw: &str) -> Result<RedactionTarget, String> { let s = raw.strip_prefix("at://").unwrap_or(raw); let segments: Vec<&str> = s.split('/').filter(|seg| !seg.is_empty()).collect(); match segments.as_slice() { [did_str] => { let did = Did::new_owned(*did_str).map_err(|e| format!("invalid DID: {e}"))?; Ok(RedactionTarget::Repo(did)) } [did_str, collection_str, rkey_str] => { let did = Did::new_owned(*did_str).map_err(|e| format!("invalid DID: {e}"))?; Nsid::new(*collection_str).map_err(|e| format!("invalid collection NSID: {e}"))?; Rkey::new(*rkey_str).map_err(|e| format!("invalid rkey: {e}"))?; Ok(RedactionTarget::Record { did, collection: (*collection_str).to_string(), rkey: (*rkey_str).to_string(), }) } _ => Err("invalid redaction target: expected DID (did:...) or AT-URI (at://did/.../collection/rkey)".to_string()), }}
pub fn router() -> Router<Hydrant> { #[cfg(feature = "indexer")] { Router::new().route("/redactions/{*target}", post(handle_post_redaction)) } #[cfg(not(feature = "indexer"))] { Router::new() }}
#[cfg(feature = "indexer")]pub async fn handle_post_redaction( State(hydrant): State<Hydrant>, Path(target_raw): Path<String>, Query(params): Query<RedactionQueryParams>,) -> Result<Json<DeleteBodiesReport>, (StatusCode, String)> { tracing::info!("handle_post_redaction called with target_raw: {target_raw}"); if hydrant.state.ephemeral { return Err(( StatusCode::CONFLICT, "operator body deletion is not supported in ephemeral mode".into(), )); }
let parsed = parse_redaction_target(&target_raw).map_err(|e| (StatusCode::BAD_REQUEST, e))?;
match parsed { RedactionTarget::Repo(did) => { let target = params.target.unwrap_or(DeleteBodyTarget::All); let exclude = params.exclude.unwrap_or(true); hydrant .repos .drop_repo(&did, target, exclude) .await .map(Json) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) } RedactionTarget::Record { did, collection, rkey, } => { let target = params.target.unwrap_or(DeleteBodyTarget::All); hydrant .repos .get(&did) .delete_record_bodies(&collection, &rkey, target) .await .map(Json) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) } }}
#[cfg(all(test, feature = "indexer"))]mod tests { use super::*;
#[test] fn test_parse_redaction_target_repo() { let did = Did::new_static("did:plc:testrepo").unwrap(); assert_eq!( parse_redaction_target("did:plc:testrepo").unwrap(), RedactionTarget::Repo(did.clone()) ); assert_eq!( parse_redaction_target("at://did:plc:testrepo").unwrap(), RedactionTarget::Repo(did) ); }
#[test] fn test_parse_redaction_target_record() { let did = Did::new_static("did:plc:testrepo").unwrap(); let expected = RedactionTarget::Record { did: did.clone(), collection: "app.bsky.feed.post".to_string(), rkey: "3kzbif5moe22m".to_string(), }; assert_eq!( parse_redaction_target("did:plc:testrepo/app.bsky.feed.post/3kzbif5moe22m").unwrap(), expected ); assert_eq!( parse_redaction_target("at://did:plc:testrepo/app.bsky.feed.post/3kzbif5moe22m") .unwrap(), expected ); }
#[test] fn test_parse_redaction_target_invalid() { assert!(parse_redaction_target("not-a-did").is_err()); assert!(parse_redaction_target("did:plc:test/only-two-parts").is_err()); }}