diff --git a/Cargo.lock b/Cargo.lock index 1c2a0db..02f2de9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,17 +206,17 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c38341e6ee670913fb9dc3aba40c22d616261da4dc0928326d3168ebf576fb0" +checksum = "3445f8f330db8e5f3be7912f170f32e43fec90d995c71ced1ec3b8394b4a873c" dependencies = [ "async-utility", "futures-util", "thiserror", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-socks", - "tokio-tungstenite", + "tokio-tungstenite 0.23.1", "url", "wasm-ws", "webpki-roots", @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "atomic-destructor" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4653a42bf04120a1d4e92452e006b4e3af4ab4afff8fb4af0f1bbb98418adf3e" +checksum = "7d919cb60ba95c87ba42777e9e246c4e8d658057299b437b7512531ce0a09a23" dependencies = [ "tracing", ] @@ -1673,9 +1673,8 @@ dependencies = [ [[package]] name = "nostr" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4f065c7357937f9149fa6fb04c89c3041a8a01ca472b887baded59b65cfe2c" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "aes", "base64 0.21.7", @@ -1703,9 +1702,8 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89506f743a5441695ab727794db41d8df1c1365ff96c25272985adf08f816b3" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-trait", "lru", @@ -1717,9 +1715,8 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751acc8bbb1329718d673470c7c3a18cddd33963dd91b97bccc92037113d254" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-utility", "async-wsocket", @@ -1733,11 +1730,11 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e65cd9f4f26f3f8e10253c518aff9e61a9204f600dfe4c3c241b0230471c67f" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-utility", + "atomic-destructor", "lnurl-pay", "nostr", "nostr-database", @@ -1752,9 +1749,8 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be1878e91a0b4a95cfd8142349b6124b037b287375d76db9638ccc4b4cdf271" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-utility", "nostr", @@ -1766,9 +1762,8 @@ dependencies = [ [[package]] name = "nostr-zapper" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5558bb031cff46e5b580847f26617d516ded4c0f8fd27fb568ec875bcd8fb99c" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-trait", "nostr", @@ -1831,9 +1826,8 @@ dependencies = [ [[package]] name = "nwc" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd88cc13a04ae41037c182489893c2f421ba0c12a028564ec339882e7f96d61" +version = "0.32.0" +source = "git+https://github.com/rust-nostr/nostr.git#d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" dependencies = [ "async-utility", "nostr", @@ -2194,8 +2188,7 @@ dependencies = [ [[package]] name = "ractor" version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11e9a53fcabb680bb70a3ce495e744676ee7e1800a188e01a68d18916a1b48e1" +source = "git+https://github.com/planetary-social/ractor.git?branch=output_ports#87af8b584d2293cee1dd8a376b0640e36dc9dd85" dependencies = [ "async-trait", "dashmap", @@ -2507,6 +2500,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -2935,7 +2942,7 @@ dependencies = [ "signal-hook-tokio", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.21.0", "tower", "tracing", "url", @@ -3212,6 +3219,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.10", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -3248,7 +3266,22 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", - "tungstenite", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tungstenite 0.23.0", "webpki-roots", ] @@ -3449,6 +3482,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.23.10", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3645,9 +3698,9 @@ dependencies = [ [[package]] name = "wasm-ws" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5b3a482e27ff54809c0848629d9033179705c5ea2f58e26cf45dc77c34c4984" +checksum = "688c5806d1b06b4f3d90d015e23364dc5d3af412ee64abba6dde8fdc01637e33" dependencies = [ "async_io_stream", "futures", diff --git a/Cargo.toml b/Cargo.toml index b2227de..ab38d34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ libc = "0.2.155" log = "0.4.21" metrics = "0.23.0" metrics-exporter-prometheus = "0.15.0" -nostr-sdk = "0.31.0" -ractor = "0.10.3" +nostr-sdk = { git = "https://github.com/rust-nostr/nostr.git", ref = "d244d10f53bf0ad2a1e84fffdf658c84d7bcce0c" } +ractor = { git = "https://github.com/planetary-social/ractor.git", branch = "output_ports" } regex = "1.10.4" reqwest = "0.12.4" serde = { version = "1.0.203", features = ["derive"] } diff --git a/src/adapters/http_server/slack_interactions_route.rs b/src/adapters/http_server/slack_interactions_route.rs index aca44a4..214d3df 100644 --- a/src/adapters/http_server/slack_interactions_route.rs +++ b/src/adapters/http_server/slack_interactions_route.rs @@ -2,7 +2,7 @@ use super::app_errors::AppError; use super::WebAppState; use crate::actors::messages::SupervisorMessage; use crate::adapters::njump_or_pubkey; -use crate::domain_objects::{ModerationCategory, ReportRequest, ReportTarget}; +use crate::domain_objects::{ReportRequest, ReportTarget}; use anyhow::{anyhow, Context, Result}; use axum::{extract::State, routing::post, Extension, Router}; use nostr_sdk::prelude::*; @@ -77,7 +77,7 @@ async fn slack_interaction_handler( async fn slack_message( message_dispatcher: ActorRef, report_request: ReportRequest, - maybe_category: Option, + maybe_category: Option, slack_username: String, ) -> Result { let reporter_nip05_markdown = njump_or_pubkey( @@ -89,7 +89,7 @@ async fn slack_message( let reported_nip05_markdown = njump_or_pubkey(message_dispatcher.clone(), report_request.target().pubkey()).await; - if let Some(moderated_report) = report_request.report(maybe_category.as_ref())? { + if let Some(moderated_report) = report_request.report(maybe_category.clone())? { let report_id = moderated_report.id(); cast!( message_dispatcher, @@ -117,7 +117,7 @@ async fn slack_message( fn slack_processed_message( slack_username: String, - category: ModerationCategory, + category: Report, report_id: EventId, reporter_nip05_markdown: String, report_request: ReportRequest, @@ -143,6 +143,19 @@ fn slack_processed_message( ), }; + let reason = match report_request.reporter_text() { + Some(text) => format!( + r#" + *Reporter Reason:* + ``` + {} + ``` + "#, + text + ), + None => "".to_string(), + }; + let message = format!( r#" 🚩 *New Moderation Report* 🚩 @@ -152,20 +165,11 @@ fn slack_processed_message( *Report Id:* `{}` *Requested By*: {} - *Reporter Reason:* - - ``` {} - ``` {} "#, - slack_username, - category, - report_id, - reporter_nip05_markdown, - report_request.reporter_text().unwrap_or(&"".to_string()), - target_message, + slack_username, category, report_id, reporter_nip05_markdown, reason, target_message, ); let trimmed_string = message @@ -203,6 +207,19 @@ fn slack_skipped_message( ), }; + let reason = match report_request.reporter_text() { + Some(text) => format!( + r#" + *Reporter Reason:* + ``` + {} + ``` + "#, + text + ), + None => "".to_string(), + }; + let message = format!( r#" ⏭️ *Moderation Report Skipped* ⏭️ @@ -210,17 +227,11 @@ fn slack_skipped_message( *Report Skipped By:* {} *Requested By*: {} - *Reporter Reason:* - ``` {} - ``` {} "#, - slack_username, - reporter_nip05_markdown, - report_request.reporter_text().unwrap_or(&"".to_string()), - target_message, + slack_username, reporter_nip05_markdown, reason, target_message, ); let trimmed_string = message @@ -234,7 +245,7 @@ fn slack_skipped_message( fn parse_slack_action( block_actions_event: SlackInteractionBlockActionsEvent, -) -> Result<(Url, String, ReportRequest, Option), AppError> { +) -> Result<(Url, String, ReportRequest, Option), AppError> { let event_value = serde_json::to_value(block_actions_event) .map_err(|e| anyhow!("Failed to convert block_actions_event to Value: {:?}", e))?; @@ -284,7 +295,7 @@ fn parse_slack_action( .map_err(|_| AppError::slack_parsing_error("reporter_pubkey"))?; let report_request = ReportRequest::new(target, reporter_pubkey, reporter_text); - let maybe_category = ModerationCategory::from_str(action_id).ok(); + let maybe_category = Report::from_str(action_id).ok(); Ok(( response_url, @@ -403,15 +414,12 @@ mod tests { fn test_parse_slack_action_with_hateful() { let reporter_pubkey = Keys::generate().public_key(); let slack_username = "daniel"; - let category_name = "hate"; + let category_name = "nudity"; let reporter_text = Some("This is wrong, report it!".to_string()); - let reported_event = EventBuilder::text_note( - "This is a hateful comment, will someone report me? I hate everything!", - [], - ) - .to_event(&Keys::generate()) - .unwrap(); + let reported_event = EventBuilder::text_note("I'm so nude I'm freezing", []) + .to_event(&Keys::generate()) + .unwrap(); let slack_actions_event = create_slack_actions_event( slack_username, diff --git a/src/adapters/nostr_service.rs b/src/adapters/nostr_service.rs index bb65d19..489b5bd 100644 --- a/src/adapters/nostr_service.rs +++ b/src/adapters/nostr_service.rs @@ -55,11 +55,16 @@ impl NostrPort for NostrService { }; if let Some(nip05_value) = metadata.nip05 { - let Ok(()) = nip05::verify(&public_key, &nip05_value, None).await else { + let Ok(verified) = nip05::verify(&public_key, &nip05_value, None).await else { error!("Failed to verify Nip05 for public key: {}", public_key); return None; }; + if !verified { + error!("Nip05 for public key: {} is not verified", public_key); + return None; + } + info!("Nip05 for public key: {} is: {}", public_key, nip05_value); return Some(nip05_value); } @@ -78,8 +83,8 @@ impl NostrPort for NostrService { tokio::spawn(async move { token_clone.cancelled().await; debug!("Cancelling relay subscription worker"); - if let Err(e) = client_clone.stop().await { - error!("Failed to stop client: {}", e); + if let Err(e) = client_clone.shutdown().await { + error!("Failed to shutdown client: {}", e); } }); @@ -109,7 +114,7 @@ impl NostrPort for NostrService { // If we ever have different type of subscriptions, we should separate // creation from handling. We can have a single handler for all subs. // See: https://github.com/rust-nostr/nostr/issues/345#issuecomment-1985925161 - self.client.subscribe(self.filters.clone(), None).await; + self.client.subscribe(self.filters.clone(), None).await?; self.client .handle_notifications(|notification| async { if cancellation_token.is_cancelled() { diff --git a/src/adapters/slack_client_adapter.rs b/src/adapters/slack_client_adapter.rs index 0caccea..d6bcc55 100644 --- a/src/adapters/slack_client_adapter.rs +++ b/src/adapters/slack_client_adapter.rs @@ -1,14 +1,14 @@ use crate::actors::messages::SupervisorMessage; use crate::actors::{SlackClientPort, SlackClientPortBuilder}; use crate::adapters::njump_or_pubkey; -use crate::domain_objects::{ModerationCategory, ReportRequest}; +use crate::domain_objects::ReportRequest; use anyhow::Result; use hyper_rustls::HttpsConnector; use hyper_util::client::legacy::connect::HttpConnector; +use nostr_sdk::nips::nip56::Report; use ractor::ActorRef; use slack_morphism::prelude::*; use std::env; -use tracing::info; #[derive(Clone)] pub struct SlackClientAdapter { @@ -35,8 +35,7 @@ impl SlackClientAdapter { let token: SlackApiToken = SlackApiToken::new(slack_token.into()); let session = self.client.open_session(&token); - let post_chat_resp = session.chat_post_message(&message).await; - info!("post chat resp: {:#?}", &post_chat_resp); + session.chat_post_message(&message).await?; Ok(()) } @@ -92,49 +91,13 @@ impl<'a> PubkeyReportRequestMessage<'a> { .with_style("danger".to_string()) .with_value(pubkey.clone()) ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::Hate).with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::HateThreatening) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::Harassment) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::HarassmentThreatening) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::SelfHarm) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::SelfHarmIntent) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::SelfHarmInstructions) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::Sexual) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::SexualMinors) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::Violence) - .with_value(pubkey.clone()) - ), - some_into( - SlackBlockButtonElement::from(ModerationCategory::ViolenceGraphic) - .with_value(pubkey.clone()) - ) + some_into(report_to_button(Report::Nudity).with_value(pubkey.clone())), + some_into(report_to_button(Report::Malware).with_value(pubkey.clone())), + some_into(report_to_button(Report::Profanity).with_value(pubkey.clone())), + some_into(report_to_button(Report::Illegal).with_value(pubkey.clone())), + some_into(report_to_button(Report::Spam).with_value(pubkey.clone())), + some_into(report_to_button(Report::Impersonation).with_value(pubkey.clone())), + some_into(report_to_button(Report::Other).with_value(pubkey.clone())) ] } } @@ -173,8 +136,6 @@ impl<'a> SlackMessageTemplate for PubkeyReportRequestMessage<'a> { } } -impl From for SlackBlockButtonElement { - fn from(category: ModerationCategory) -> Self { - SlackBlockButtonElement::new(category.to_string().into(), pt!(category.to_string())) - } +fn report_to_button(report: Report) -> SlackBlockButtonElement { + SlackBlockButtonElement::new(report.to_string().into(), pt!(report.to_string())) } diff --git a/src/domain_objects.rs b/src/domain_objects.rs index 6f97f40..39d1177 100644 --- a/src/domain_objects.rs +++ b/src/domain_objects.rs @@ -7,8 +7,5 @@ pub use report_request::ReportTarget; pub mod as_gift_wrap; -pub mod moderation_category; -pub use moderation_category::ModerationCategory; - pub mod moderated_report; pub use moderated_report::ModeratedReport; diff --git a/src/domain_objects/moderated_report.rs b/src/domain_objects/moderated_report.rs index 19942e2..f12779b 100644 --- a/src/domain_objects/moderated_report.rs +++ b/src/domain_objects/moderated_report.rs @@ -1,4 +1,4 @@ -use crate::domain_objects::{ModerationCategory, ReportRequest, ReportTarget}; +use crate::domain_objects::{ReportRequest, ReportTarget}; use anyhow::Result; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -12,10 +12,7 @@ pub struct ModeratedReport { } impl ModeratedReport { - pub(super) fn create( - reported_request: &ReportRequest, - category: &ModerationCategory, - ) -> Result { + pub(super) fn create(reported_request: &ReportRequest, category: Report) -> Result { let Ok(reportinator_secret) = env::var("REPORTINATOR_SECRET") else { return Err(anyhow::anyhow!("REPORTINATOR_SECRET env variable not set")); }; @@ -24,8 +21,8 @@ impl ModeratedReport { ReportTarget::Event(event) => (event.pubkey, Some(event.id)), ReportTarget::Pubkey(pubkey) => (*pubkey, None), }; - let tags = Self::set_tags(reported_pubkey, reported_event_id, category); - let report_event = EventBuilder::new(Kind::Reporting, category.description(), tags) + let tags = Self::set_tags(reported_pubkey, reported_event_id, category.clone()); + let report_event = EventBuilder::new(Kind::Reporting, report_description(category), tags) .to_event(&reportinator_keys)?; Ok(Self { @@ -36,13 +33,12 @@ impl ModeratedReport { fn set_tags( reported_pubkey: PublicKey, reported_event_id: Option, - category: &ModerationCategory, + category: Report, ) -> impl IntoIterator { - let pubkey_tag = Tag::public_key_report(reported_pubkey, category.nip56_report_type()); + let pubkey_tag = Tag::public_key_report(reported_pubkey, category.clone()); let mut tags = vec![pubkey_tag]; - reported_event_id - .inspect(|id| tags.push(Tag::event_report(*id, category.nip56_report_type()))); + reported_event_id.inspect(|id| tags.push(Tag::event_report(*id, category))); let label_namespace_tag = Tag::custom( TagKind::SingleLetter(SingleLetterTag::uppercase(Alphabet::L)), @@ -50,12 +46,6 @@ impl ModeratedReport { ); tags.push(label_namespace_tag); - let label_tag = Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)), - vec![format!("MOD>{}", category.nip69()), "MOD".to_string()], - ); - tags.push(label_tag); - tags } @@ -68,6 +58,18 @@ impl ModeratedReport { } } +fn report_description(report: Report) -> &'static str { + match report { + Report::Nudity => "Depictions of nudity, porn, or sexually explicit content.", + Report::Malware => "Virus, trojan horse, worm, robot, spyware, adware, back door, ransomware, rootkit, kidnapper, etc.", + Report::Profanity => "Profanity, hateful speech, or other offensive content.", + Report::Illegal => "Content that may be illegal in some jurisdictions.", + Report::Spam => "Spam.", + Report::Impersonation => "Someone pretending to be someone else.", + Report::Other => "For reports that don't fit in the above categories.", + } +} + impl Display for ModeratedReport { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", serde_json::to_string_pretty(&self.event).unwrap()) diff --git a/src/domain_objects/moderation_category.rs b/src/domain_objects/moderation_category.rs deleted file mode 100644 index 0dfaa88..0000000 --- a/src/domain_objects/moderation_category.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anyhow::anyhow; -use nostr_sdk::nips::nip56::Report; - -use std::fmt::{self, Display, Formatter}; -use std::str::FromStr; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ModerationCategory { - Hate, - HateThreatening, - Harassment, - HarassmentThreatening, - SelfHarm, - SelfHarmIntent, - SelfHarmInstructions, - Sexual, - SexualMinors, - Violence, - ViolenceGraphic, -} - -impl ModerationCategory { - pub fn description(&self) -> &'static str { - match self { - ModerationCategory::Hate => "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment.", - ModerationCategory::HateThreatening => "Hateful content that also includes violence or serious harm towards the targeted group based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste.", - ModerationCategory::Harassment => "Content that expresses, incites, or promotes harassing language towards any target.", - ModerationCategory::HarassmentThreatening => "Harassment content that also includes violence or serious harm towards any target.", - ModerationCategory::SelfHarm => "Content that promotes, encourages, or depicts acts of self-harm, such as suicide, cutting, and eating disorders.", - ModerationCategory::SelfHarmIntent => "Content where the speaker expresses that they are engaging or intend to engage in acts of self-harm, such as suicide, cutting, and eating disorders.", - ModerationCategory::SelfHarmInstructions => "Content that encourages performing acts of self-harm, such as suicide, cutting, and eating disorders, or that gives instructions or advice on how to commit such acts.", - ModerationCategory::Sexual => "Content meant to arouse sexual excitement, such as the description of sexual activity, or that promotes sexual services (excluding sex education and wellness).", - ModerationCategory::SexualMinors => "Sexual content that includes an individual who is under 18 years old.", - ModerationCategory::Violence => "Content that depicts death, violence, or physical injury.", - ModerationCategory::ViolenceGraphic => "Content that depicts death, violence, or physical injury in graphic detail.", - } - } - - pub fn nip56_report_type(&self) -> Report { - match self { - ModerationCategory::Hate - | ModerationCategory::HateThreatening - | ModerationCategory::Harassment - | ModerationCategory::HarassmentThreatening - | ModerationCategory::SelfHarm - | ModerationCategory::SelfHarmIntent - | ModerationCategory::SelfHarmInstructions - | ModerationCategory::Violence - | ModerationCategory::ViolenceGraphic => Report::Other, - - ModerationCategory::Sexual => Report::Nudity, - - ModerationCategory::SexualMinors => Report::Illegal, - } - } - - pub fn nip69(&self) -> &'static str { - match self { - ModerationCategory::Hate => "IH", - ModerationCategory::HateThreatening => "HC-bhd", - ModerationCategory::Harassment => "IL-har", - ModerationCategory::HarassmentThreatening => "HC-bhd", - ModerationCategory::SelfHarm => "HC-bhd", - ModerationCategory::SelfHarmIntent => "HC-bhd", - ModerationCategory::SelfHarmInstructions => "HC-bhd", - ModerationCategory::Sexual => "NS", - ModerationCategory::SexualMinors => "IL-csa", - ModerationCategory::Violence => "VI", - ModerationCategory::ViolenceGraphic => "VI", - } - } -} - -impl FromStr for ModerationCategory { - type Err = anyhow::Error; - - fn from_str(input: &str) -> Result { - match input { - "hate" => Ok(ModerationCategory::Hate), - "hate/threatening" => Ok(ModerationCategory::HateThreatening), - "harassment" => Ok(ModerationCategory::Harassment), - "harassment/threatening" => Ok(ModerationCategory::HarassmentThreatening), - "self-harm" => Ok(ModerationCategory::SelfHarm), - "self-harm/intent" => Ok(ModerationCategory::SelfHarmIntent), - "self-harm/instructions" => Ok(ModerationCategory::SelfHarmInstructions), - "sexual" => Ok(ModerationCategory::Sexual), - "sexual/minors" => Ok(ModerationCategory::SexualMinors), - "violence" => Ok(ModerationCategory::Violence), - "violence/graphic" => Ok(ModerationCategory::ViolenceGraphic), - _ => Err(anyhow!("Invalid moderation category {}", input)), - } - } -} - -impl Display for ModerationCategory { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!( - f, - "{}", - match self { - ModerationCategory::Hate => "hate", - ModerationCategory::HateThreatening => "hate/threatening", - ModerationCategory::Harassment => "harassment", - ModerationCategory::HarassmentThreatening => "harassment/threatening", - ModerationCategory::SelfHarm => "self-harm", - ModerationCategory::SelfHarmIntent => "self-harm/intent", - ModerationCategory::SelfHarmInstructions => "self-harm/instructions", - ModerationCategory::Sexual => "sexual", - ModerationCategory::SexualMinors => "sexual/minors", - ModerationCategory::Violence => "violence", - ModerationCategory::ViolenceGraphic => "violence/graphic", - } - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_from_str() { - assert_eq!( - ModerationCategory::from_str("hate").unwrap(), - ModerationCategory::Hate - ); - assert_eq!( - ModerationCategory::from_str("harassment").unwrap(), - ModerationCategory::Harassment - ); - - assert!(ModerationCategory::from_str("non-existent").is_err()); - } - - #[test] - fn test_description() { - let hate = ModerationCategory::Hate; - assert_eq!(hate.description(), "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment."); - } - - #[test] - fn test_nip56_report_type() { - let harassment = ModerationCategory::Harassment; - assert_eq!(harassment.nip56_report_type(), Report::Other); - } - - #[test] - fn test_nip69() { - let violence = ModerationCategory::Violence; - assert_eq!(violence.nip69(), "VI"); - } - - #[test] - fn test_display() { - let sexual = ModerationCategory::Sexual; - assert_eq!(format!("{}", sexual), "sexual"); - } -} diff --git a/src/domain_objects/report_request.rs b/src/domain_objects/report_request.rs index c32620e..1672755 100644 --- a/src/domain_objects/report_request.rs +++ b/src/domain_objects/report_request.rs @@ -1,4 +1,4 @@ -use super::{ModeratedReport, ModerationCategory}; +use super::ModeratedReport; use anyhow::Result; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -111,7 +111,7 @@ impl ReportRequest { pub fn report( &self, - maybe_moderation_category: Option<&ModerationCategory>, + maybe_moderation_category: Option, ) -> Result> { let Some(moderation_category) = maybe_moderation_category else { return Ok(None); @@ -131,7 +131,7 @@ impl Display for ReportRequest { #[cfg(test)] mod tests { use super::*; - use crate::domain_objects::ModerationCategory; + use nostr_sdk::nips::nip56::Report; use serde_json::json; use std::str::FromStr; @@ -186,8 +186,8 @@ mod tests { let (report_request, reported_target, _reporter_pubkey, _reporter_text) = setup_test_environment(true); - let category = ModerationCategory::from_str("hate").unwrap(); - let maybe_report_event = report_request.report(Some(&category)).unwrap(); + let category = Report::from_str("malware").unwrap(); + let maybe_report_event = report_request.report(Some(category)).unwrap(); let report_event = maybe_report_event.unwrap().event(); let report_event_value = serde_json::to_value(report_event).unwrap(); @@ -196,17 +196,15 @@ mod tests { "2ddc92121b9e67172cc0d40b959c416173a3533636144ebc002b7719d8d1c4e3".to_string() ); assert_eq!(report_event_value["kind"], 1984); - assert_eq!(report_event_value["content"], "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment."); + assert_eq!(report_event_value["content"], "Virus, trojan horse, worm, robot, spyware, adware, back door, ransomware, rootkit, kidnapper, etc."); let ReportTarget::Event(reported_event) = reported_target else { panic!("Expected ReportedTarget::Event, got {:?}", reported_target); }; let expected_tags = vec![ - json!(["p", reported_event.pubkey, "other"]), - json!(["e", reported_event.id, "other"]), - json!(["L", "MOD"]), - json!(["l", "MOD>IH", "MOD"]), + json!(["p", reported_event.pubkey, "malware"]), + json!(["e", reported_event.id, "malware"]), ]; for (i, expected_tag) in expected_tags.iter().enumerate() { @@ -219,8 +217,8 @@ mod tests { let (report_request, reported_target, _reporter_pubkey, _reporter_text) = setup_test_environment(false); - let category = ModerationCategory::from_str("hate").unwrap(); - let maybe_report_event = report_request.report(Some(&category)).unwrap(); + let category = Report::from_str("other").unwrap(); + let maybe_report_event = report_request.report(Some(category)).unwrap(); let report_event = maybe_report_event.unwrap().event(); let report_event_value = serde_json::to_value(report_event).unwrap(); @@ -229,7 +227,10 @@ mod tests { "2ddc92121b9e67172cc0d40b959c416173a3533636144ebc002b7719d8d1c4e3".to_string() ); assert_eq!(report_event_value["kind"], 1984); - assert_eq!(report_event_value["content"], "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment."); + assert_eq!( + report_event_value["content"], + "For reports that don't fit in the above categories." + ); let ReportTarget::Pubkey(reported_pubkey) = reported_target else { panic!("Expected ReportedTarget::Pubkey, got {:?}", reported_target); @@ -237,11 +238,7 @@ mod tests { assert!(matches!(reported_target, ReportTarget::Pubkey { .. })); - let expected_tags = vec![ - json!(["p", reported_pubkey, "other"]), - json!(["L", "MOD"]), - json!(["l", "MOD>IH", "MOD"]), - ]; + let expected_tags = vec![json!(["p", reported_pubkey, "other"])]; for (i, expected_tag) in expected_tags.iter().enumerate() { assert_eq!(&report_event_value["tags"][i], expected_tag);