Spaces:
Build error
Build error
File size: 5,467 Bytes
1295969 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | //! DMCA §512 notice-and-takedown. EU Copyright Directive Art. 17.
//!
//! Persistence: LMDB via persist::LmdbStore — notices survive server restarts.
//! The rand_id now uses OS entropy for unpredictable DMCA IDs.
use crate::AppState;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use serde::{Deserialize, Serialize};
use tracing::info;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NoticeStatus {
Received,
UnderReview,
ContentRemoved,
CounterReceived,
Restored,
Dismissed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TakedownNotice {
pub id: String,
pub isrc: String,
pub claimant_name: String,
pub claimant_email: String,
pub work_description: String,
pub infringing_url: String,
pub good_faith: bool,
pub accuracy: bool,
pub status: NoticeStatus,
pub submitted_at: String,
pub resolved_at: Option<String>,
pub counter_notice: Option<CounterNotice>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CounterNotice {
pub uploader_name: String,
pub uploader_email: String,
pub good_faith: bool,
pub submitted_at: String,
}
#[derive(Deserialize)]
pub struct TakedownRequest {
pub isrc: String,
pub claimant_name: String,
pub claimant_email: String,
pub work_description: String,
pub infringing_url: String,
pub good_faith: bool,
pub accuracy: bool,
}
#[derive(Deserialize)]
pub struct CounterNoticeRequest {
pub uploader_name: String,
pub uploader_email: String,
pub good_faith: bool,
}
pub struct TakedownStore {
db: crate::persist::LmdbStore,
}
impl TakedownStore {
pub fn open(path: &str) -> anyhow::Result<Self> {
Ok(Self {
db: crate::persist::LmdbStore::open(path, "dmca_notices")?,
})
}
pub fn add(&self, n: TakedownNotice) -> anyhow::Result<()> {
self.db.put(&n.id, &n)?;
Ok(())
}
pub fn get(&self, id: &str) -> Option<TakedownNotice> {
self.db.get(id).ok().flatten()
}
pub fn update_status(&self, id: &str, status: NoticeStatus) {
let _ = self.db.update::<TakedownNotice>(id, |n| {
n.status = status.clone();
n.resolved_at = Some(chrono::Utc::now().to_rfc3339());
});
}
pub fn set_counter(&self, id: &str, counter: CounterNotice) {
let _ = self.db.update::<TakedownNotice>(id, |n| {
n.counter_notice = Some(counter.clone());
n.status = NoticeStatus::CounterReceived;
});
}
}
/// Cryptographically random 8-hex-char suffix for DMCA IDs.
fn rand_id() -> String {
crate::wallet_auth::random_hex_pub(4)
}
pub async fn submit_notice(
State(state): State<AppState>,
Json(req): Json<TakedownRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if !req.good_faith || !req.accuracy {
return Err(StatusCode::BAD_REQUEST);
}
let id = format!("DMCA-{}-{}", chrono::Utc::now().format("%Y%m%d"), rand_id());
let notice = TakedownNotice {
id: id.clone(),
isrc: req.isrc.clone(),
claimant_name: req.claimant_name.clone(),
claimant_email: req.claimant_email.clone(),
work_description: req.work_description.clone(),
infringing_url: req.infringing_url.clone(),
good_faith: req.good_faith,
accuracy: req.accuracy,
status: NoticeStatus::Received,
submitted_at: chrono::Utc::now().to_rfc3339(),
resolved_at: None,
counter_notice: None,
};
state
.takedown_db
.add(notice)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
state
.audit_log
.record(&format!(
"DMCA_NOTICE id='{}' isrc='{}' claimant='{}'",
id, req.isrc, req.claimant_name
))
.ok();
state
.takedown_db
.update_status(&id, NoticeStatus::ContentRemoved);
info!(id=%id, isrc=%req.isrc, "DMCA notice received — content removed (24h SLA)");
Ok(Json(serde_json::json!({
"notice_id": id, "status": "ContentRemoved",
"message": "Notice received. Content removed within 24h per DMCA §512.",
"counter_notice_window": "10 business days",
})))
}
pub async fn submit_counter_notice(
State(state): State<AppState>,
Path(id): Path<String>,
Json(req): Json<CounterNoticeRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
if state.takedown_db.get(&id).is_none() {
return Err(StatusCode::NOT_FOUND);
}
if !req.good_faith {
return Err(StatusCode::BAD_REQUEST);
}
state.takedown_db.set_counter(
&id,
CounterNotice {
uploader_name: req.uploader_name,
uploader_email: req.uploader_email,
good_faith: req.good_faith,
submitted_at: chrono::Utc::now().to_rfc3339(),
},
);
state
.audit_log
.record(&format!("DMCA_COUNTER id='{id}'"))
.ok();
Ok(Json(
serde_json::json!({ "notice_id": id, "status": "CounterReceived",
"message": "Content restored in 10-14 business days if no lawsuit filed per §512(g)." }),
))
}
pub async fn get_notice(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<TakedownNotice>, StatusCode> {
state
.takedown_db
.get(&id)
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
|