diff --git a/rsky-feedgen/src/apis/mod.rs b/rsky-feedgen/src/apis/mod.rs index 41439f1d..d8e58d91 100644 --- a/rsky-feedgen/src/apis/mod.rs +++ b/rsky-feedgen/src/apis/mod.rs @@ -1119,6 +1119,29 @@ pub fn add_visitor( Ok(()) } +pub fn ban_from_tv( + subject: &String, + reason_param: Option, + tags_param: Option>, +) -> Result<(), Box> { + use crate::schema::banned_from_tv::dsl::*; + + let connection = &mut establish_connection()?; + + diesel::insert_into(banned_from_tv) + .values(( + did.eq(subject.clone()), + reason.eq(reason_param), + tags.eq(tags_param), + )) + .on_conflict(did) + .do_nothing() + .execute(connection) + .expect("Error inserting into banned_from_tv"); + + Ok(()) +} + pub fn is_banned_from_tv(subject: &String) -> Result> { use crate::schema::banned_from_tv::dsl::*; @@ -1134,6 +1157,57 @@ pub fn is_banned_from_tv(subject: &String) -> Result 0 { Ok(true) } else { Ok(false) }; } +pub fn unban_from_tv(subject: &String) -> Result<(), Box> { + use crate::schema::banned_from_tv::dsl::*; + + let connection = &mut establish_connection()?; + + diesel::delete(banned_from_tv.filter(did.eq(subject.clone()))) + .execute(connection) + .expect("Error deleting from banned_from_tv"); + + Ok(()) +} + +pub fn search_banned_from_tv( + search_did: Option, + search_tag: Option, + limit: Option, + offset: Option, +) -> Result, Box> { + use crate::models::BannedFromTv; + use crate::schema::banned_from_tv::dsl::*; + + let connection = &mut establish_connection()?; + + let mut query = banned_from_tv + .select(BannedFromTv::as_select()) + .order(createdAt.desc()) + .into_boxed(); + + if let Some(search_did_val) = search_did { + query = query.filter(did.like(format!("%{}%", search_did_val))); + } + + if let Some(search_tag_val) = search_tag { + query = query.filter(tags.contains(vec![Some(search_tag_val)])); + } + + if let Some(limit_val) = limit { + query = query.limit(limit_val); + } + + if let Some(offset_val) = offset { + query = query.offset(offset_val); + } + + let results = query + .load(connection) + .expect("Error loading banned_from_tv records"); + + Ok(results) +} + pub async fn get_cursor( service_: String, connection: ReadReplicaConn, @@ -1531,4 +1605,84 @@ mod tests { ) .await; } + + #[test] + fn test_ban_from_tv() { + let test_did = "did:plc:test123".to_string(); + let test_reason = Some("Test ban reason".to_string()); + let test_tags = Some(vec!["spam".to_string(), "abuse".to_string()]); + + // Insert the banned user + let result = ban_from_tv(&test_did, test_reason.clone(), test_tags.clone()); + assert!(result.is_ok()); + + // Verify the user is banned + let is_banned = is_banned_from_tv(&test_did); + assert!(is_banned.is_ok()); + assert_eq!(is_banned.unwrap(), true); + + // Test duplicate ban (should not error due to on_conflict) + let duplicate_result = ban_from_tv(&test_did, None, None); + assert!(duplicate_result.is_ok()); + + // Unban the user + let unban_result = unban_from_tv(&test_did); + assert!(unban_result.is_ok()); + + // Verify they are no longer banned + let is_still_banned = is_banned_from_tv(&test_did); + assert_eq!(is_still_banned.unwrap(), false); + } + + #[test] + fn test_search_banned_from_tv() { + let test_did1 = "did:plc:search001".to_string(); + let test_did2 = "did:plc:search002".to_string(); + let test_did3 = "did:plc:search003".to_string(); + + // Insert multiple banned users + let _ = ban_from_tv( + &test_did1, + Some("Reason 1".to_string()), + Some(vec!["tag1".to_string()]), + ); + let _ = ban_from_tv( + &test_did2, + Some("Reason 2".to_string()), + Some(vec!["tag2".to_string()]), + ); + let _ = ban_from_tv( + &test_did3, + Some("Reason 3".to_string()), + Some(vec!["tag1".to_string()]), + ); + + // Search without filters + let all_results = search_banned_from_tv(None, None, Some(100), None); + assert!(all_results.is_ok()); + let all_banned = all_results.unwrap(); + assert!(all_banned.len() >= 3); + + // Search by DID + let did_results = search_banned_from_tv(Some("search001".to_string()), None, None, None); + assert!(did_results.is_ok()); + let did_banned = did_results.unwrap(); + assert!(did_banned.iter().any(|b| b.did.contains("search001"))); + + // Search by tag + let tag_results = search_banned_from_tv(None, Some("tag1".to_string()), None, None); + assert!(tag_results.is_ok()); + let tag_banned = tag_results.unwrap(); + assert!(tag_banned.len() >= 2); + + // Test pagination + let limited_results = search_banned_from_tv(None, None, Some(1), Some(0)); + assert!(limited_results.is_ok()); + assert_eq!(limited_results.unwrap().len(), 1); + + // Clean up + let _ = unban_from_tv(&test_did1); + let _ = unban_from_tv(&test_did2); + let _ = unban_from_tv(&test_did3); + } } diff --git a/rsky-feedgen/src/main.rs b/rsky-feedgen/src/main.rs index 9dc9911d..a772d138 100644 --- a/rsky-feedgen/src/main.rs +++ b/rsky-feedgen/src/main.rs @@ -153,6 +153,9 @@ fn rocket() -> _ { well_known, get_cursor, update_cursor, + ban_user, + unban_user, + list_banned_users, all_options ], ) diff --git a/rsky-feedgen/src/models/banned_from_tv.rs b/rsky-feedgen/src/models/banned_from_tv.rs new file mode 100644 index 00000000..820967fc --- /dev/null +++ b/rsky-feedgen/src/models/banned_from_tv.rs @@ -0,0 +1,16 @@ +use diesel::prelude::*; + +#[derive(Queryable, Selectable, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = crate::schema::banned_from_tv)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct BannedFromTv { + #[serde(rename = "did")] + pub did: String, + #[serde(rename = "reason", skip_serializing_if = "Option::is_none")] + pub reason: Option, + #[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")] + #[diesel(column_name = createdAt)] + pub created_at: Option, + #[serde(rename = "tags", skip_serializing_if = "Option::is_none")] + pub tags: Option>>, +} diff --git a/rsky-feedgen/src/models/banned_from_tv_request.rs b/rsky-feedgen/src/models/banned_from_tv_request.rs new file mode 100644 index 00000000..b4e03a1b --- /dev/null +++ b/rsky-feedgen/src/models/banned_from_tv_request.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BannedFromTvRequest { + #[serde(rename = "did")] + pub did: String, + #[serde(rename = "reason", skip_serializing_if = "Option::is_none")] + pub reason: Option, + #[serde(rename = "tags", skip_serializing_if = "Option::is_none")] + pub tags: Option>, +} diff --git a/rsky-feedgen/src/models/mod.rs b/rsky-feedgen/src/models/mod.rs index cebae973..ad29a7fe 100644 --- a/rsky-feedgen/src/models/mod.rs +++ b/rsky-feedgen/src/models/mod.rs @@ -31,3 +31,7 @@ pub mod known_service; pub use self::known_service::KnownService; pub mod jwt_parts; pub use self::jwt_parts::JwtParts; +pub mod banned_from_tv; +pub use self::banned_from_tv::BannedFromTv; +pub mod banned_from_tv_request; +pub use self::banned_from_tv_request::BannedFromTvRequest; diff --git a/rsky-feedgen/src/routes.rs b/rsky-feedgen/src/routes.rs index ffde4b7c..4365681b 100644 --- a/rsky-feedgen/src/routes.rs +++ b/rsky-feedgen/src/routes.rs @@ -539,3 +539,72 @@ pub async fn well_known() -> Result< } } } + +#[rocket::post("/admin/ban", format = "json", data = "")] +pub fn ban_user( + body: Json, + _key: ApiKey<'_>, +) -> Result<(), status::Custom>> { + match crate::apis::ban_from_tv(&body.did, body.reason.clone(), body.tags.clone()) { + Ok(_) => Ok(()), + Err(error) => { + eprintln!("Internal Error: {error}"); + let internal_error = crate::models::InternalErrorMessageResponse { + code: Some(crate::models::InternalErrorCode::InternalError), + message: Some(error.to_string()), + }; + Err(status::Custom( + Status::InternalServerError, + Json(internal_error), + )) + } + } +} + +#[rocket::delete("/admin/ban?")] +pub fn unban_user( + did: &str, + _key: ApiKey<'_>, +) -> Result<(), status::Custom>> { + match crate::apis::unban_from_tv(&did.to_string()) { + Ok(_) => Ok(()), + Err(error) => { + eprintln!("Internal Error: {error}"); + let internal_error = crate::models::InternalErrorMessageResponse { + code: Some(crate::models::InternalErrorCode::InternalError), + message: Some(error.to_string()), + }; + Err(status::Custom( + Status::InternalServerError, + Json(internal_error), + )) + } + } +} + +#[rocket::get("/admin/banned?&&&", format = "json")] +pub fn list_banned_users( + did: Option, + tag: Option, + limit: Option, + offset: Option, + _key: ApiKey<'_>, +) -> Result< + Json>, + status::Custom>, +> { + match crate::apis::search_banned_from_tv(did, tag, limit, offset) { + Ok(results) => Ok(Json(results)), + Err(error) => { + eprintln!("Internal Error: {error}"); + let internal_error = crate::models::InternalErrorMessageResponse { + code: Some(crate::models::InternalErrorCode::InternalError), + message: Some(error.to_string()), + }; + Err(status::Custom( + Status::InternalServerError, + Json(internal_error), + )) + } + } +}