From 368a1d34fb3aefa6368378d14dd1c25c18ace63a Mon Sep 17 00:00:00 2001 From: Daniel Cadenas Date: Fri, 29 Mar 2024 19:17:57 -0300 Subject: [PATCH] Start with ModeratedReport and category parsing --- Cargo.lock | 23 ++++ Cargo.toml | 3 + src/actors/event_enqueuer.rs | 2 +- src/actors/gift_unwrapper.rs | 48 +------ src/actors/messages.rs | 10 +- src/actors/relay_event_dispatcher.rs | 20 ++- .../http_server/slack_interactions_route.rs | 34 +++-- src/adapters/nostr_subscriber.rs | 5 +- src/bin/giftwrapper.rs | 15 +-- src/domain_objects.rs | 52 ++------ src/domain_objects/gift_wrap.rs | 44 ++++++ src/domain_objects/moderated_report.rs | 23 ++++ src/domain_objects/moderation_category.rs | 126 ++++++++++++++++++ src/domain_objects/report_request.rs | 103 ++++++++++++++ 14 files changed, 386 insertions(+), 122 deletions(-) create mode 100644 src/domain_objects/gift_wrap.rs create mode 100644 src/domain_objects/moderated_report.rs create mode 100644 src/domain_objects/moderation_category.rs create mode 100644 src/domain_objects/report_request.rs diff --git a/Cargo.lock b/Cargo.lock index a45f769..656d334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,6 +681,12 @@ dependencies = [ "serde", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1986,6 +1992,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -2148,6 +2164,7 @@ dependencies = [ "libc", "log", "nostr-sdk", + "pretty_assertions", "ractor", "regex", "reqwest 0.12.2", @@ -3655,6 +3672,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index c108799..2d1d4fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,6 @@ path = "src/main.rs" [[bin]] name = "giftwrapper" path = "src/bin/giftwrapper.rs" + +[dev-dependencies] +pretty_assertions = "1.4.0" diff --git a/src/actors/event_enqueuer.rs b/src/actors/event_enqueuer.rs index d7d4604..eee0261 100644 --- a/src/actors/event_enqueuer.rs +++ b/src/actors/event_enqueuer.rs @@ -58,7 +58,7 @@ where info!( "Event {} enqueued for moderation", - report_request.reported_event.id() + report_request.reported_event().id() ); } } diff --git a/src/actors/gift_unwrapper.rs b/src/actors/gift_unwrapper.rs index f945f68..ada90d4 100644 --- a/src/actors/gift_unwrapper.rs +++ b/src/actors/gift_unwrapper.rs @@ -52,8 +52,8 @@ impl Actor for GiftUnwrapper { info!( "Request from {} to moderate event {}", - report_request.reporter_pubkey, - report_request.reported_event.id() + report_request.reporter_pubkey(), + report_request.reported_event().id() ); state.message_parsed_output_port.send(report_request) @@ -68,45 +68,10 @@ impl Actor for GiftUnwrapper { } } -// NOTE: This roughly creates a message as described by nip 17 but it's still -// not ready, just for testing purposes. There are more details to consider to -// properly implement the nip like created_at treatment. The nip itself is not -// finished at this time so hopefully in the future this can be done through the -// nostr crate. -#[allow(dead_code)] // Besides the tests, it's used from the giftwrapper utility binary -pub async fn create_private_dm_message( - report_request: &ReportRequest, - reporter_keys: &Keys, - receiver_pubkey: &PublicKey, -) -> Result { - if report_request.reporter_pubkey != reporter_keys.public_key() { - return Err(anyhow::anyhow!( - "Reporter public key doesn't match the provided keys" - )); - } - // Compose rumor - let kind_14_rumor = - EventBuilder::sealed_direct(receiver_pubkey.clone(), report_request.as_json()) - .to_unsigned_event(reporter_keys.public_key()); - - // Compose seal - let content: String = NostrSigner::Keys(reporter_keys.clone()) - .nip44_encrypt(receiver_pubkey.clone(), kind_14_rumor.as_json()) - .await?; - let kind_13_seal = EventBuilder::new(Kind::Seal, content, []).to_event(&reporter_keys)?; - - // Compose gift wrap - let kind_1059_gift_wrap: Event = - EventBuilder::gift_wrap_from_seal(&receiver_pubkey, &kind_13_seal, None)?; - - Ok(kind_1059_gift_wrap) -} - #[cfg(test)] mod tests { use super::*; use crate::actors::TestActor; - use crate::domain_objects::GiftWrap; use ractor::{cast, Actor}; use serde_json::json; use std::sync::Arc; @@ -138,11 +103,10 @@ mod tests { .to_string(); let report_request: ReportRequest = serde_json::from_str(&report_request_string).unwrap(); - let gift_wrapped_event = GiftWrap::new( - create_private_dm_message(&report_request, &sender_keys, &receiver_pubkey) - .await - .unwrap(), - ); + let gift_wrapped_event = report_request + .as_gift_wrap(&sender_keys, &receiver_pubkey) + .await + .unwrap(); let messages_received = Arc::new(Mutex::new(Vec::::new())); let (receiver_actor_ref, receiver_actor_handle) = diff --git a/src/actors/messages.rs b/src/actors/messages.rs index 053d966..0ae8111 100644 --- a/src/actors/messages.rs +++ b/src/actors/messages.rs @@ -6,19 +6,19 @@ use std::fmt::Debug; pub enum RelayEventDispatcherMessage { Connect, Reconnect, - SubscribeToEventReceived(OutputPortSubscriber), - EventReceived(GiftWrap), + SubscribeToEventReceived(OutputPortSubscriber), + EventReceived(GiftWrappedReportRequest), } #[derive(Debug)] pub enum GiftUnwrapperMessage { - UnwrapEvent(GiftWrap), + UnwrapEvent(GiftWrappedReportRequest), SubscribeToEventUnwrapped(OutputPortSubscriber), } // How to subscribe to actors that publish DM messages like RelayEventDispatcher -impl From for GiftUnwrapperMessage { - fn from(gift_wrap: GiftWrap) -> Self { +impl From for GiftUnwrapperMessage { + fn from(gift_wrap: GiftWrappedReportRequest) -> Self { GiftUnwrapperMessage::UnwrapEvent(gift_wrap) } } diff --git a/src/actors/relay_event_dispatcher.rs b/src/actors/relay_event_dispatcher.rs index a89b461..aa79ae8 100644 --- a/src/actors/relay_event_dispatcher.rs +++ b/src/actors/relay_event_dispatcher.rs @@ -1,5 +1,5 @@ use crate::actors::messages::RelayEventDispatcherMessage; -use crate::domain_objects::GiftWrap; +use crate::domain_objects::GiftWrappedReportRequest; use crate::service_manager::ServiceManager; use anyhow::Result; use nostr_sdk::prelude::*; @@ -19,7 +19,7 @@ impl Default for RelayEventDispatcher { } } pub struct State { - event_received_output_port: OutputPort, + event_received_output_port: OutputPort, subscription_task_manager: Option, nostr_client: T, } @@ -195,6 +195,7 @@ where mod tests { use super::*; use crate::actors::TestActor; + use pretty_assertions::assert_eq; use ractor::{cast, concurrency::Duration}; use std::sync::Arc; use tokio::sync::mpsc; @@ -245,7 +246,9 @@ mod tests { while let Some(Some(event)) = self.event_receiver.lock().await.recv().await { cast!( dispatcher_actor, - RelayEventDispatcherMessage::EventReceived(GiftWrap::new(event)) + RelayEventDispatcherMessage::EventReceived(GiftWrappedReportRequest::try_from( + event + )?) ) .expect("Failed to cast event to dispatcher"); } @@ -256,10 +259,10 @@ mod tests { #[tokio::test] async fn test_relay_event_dispatcher() { - let first_event = EventBuilder::text_note("First event", []) + let first_event = EventBuilder::new(Kind::GiftWrap, "First event", []) .to_event(&Keys::generate()) .unwrap(); - let second_event = EventBuilder::text_note("Second event", []) + let second_event = EventBuilder::new(Kind::GiftWrap, "Second event", []) .to_event(&Keys::generate()) .unwrap(); @@ -275,7 +278,7 @@ mod tests { .await .unwrap(); - let received_messages = Arc::new(Mutex::new(Vec::::new())); + let received_messages = Arc::new(Mutex::new(Vec::::new())); let (receiver_ref, receiver_handle) = Actor::spawn(None, TestActor::default(), received_messages.clone()) @@ -304,7 +307,10 @@ mod tests { assert_eq!( received_messages.lock().await.as_ref(), - [GiftWrap::new(first_event), GiftWrap::new(second_event)] + [ + GiftWrappedReportRequest::try_from(first_event).unwrap(), + GiftWrappedReportRequest::try_from(second_event).unwrap() + ] ); } } diff --git a/src/adapters/http_server/slack_interactions_route.rs b/src/adapters/http_server/slack_interactions_route.rs index bbba0ba..deb0451 100644 --- a/src/adapters/http_server/slack_interactions_route.rs +++ b/src/adapters/http_server/slack_interactions_route.rs @@ -2,12 +2,14 @@ use super::app_errors::AppError; use super::WebAppState; use anyhow::{anyhow, Context, Result}; use axum::{extract::State, routing::post, Extension, Router}; +use gcloud_sdk::tonic::IntoRequest; use nostr_sdk::prelude::*; +use reportinator_server::domain_objects::{ModeratedReport, ModerationCategory, ReportRequest}; use reqwest::Client; use serde_json::{json, Value}; use slack_morphism::prelude::*; -use std::env; use std::sync::Arc; +use std::{env, str::FromStr}; use tracing::{error, info}; pub fn slack_interactions_route() -> Result> { @@ -112,12 +114,27 @@ async fn slack_interaction_handler( ) })?; - info!("Reported Event Block: {:?}", event); + // The slack payload is the category id in the action_id, and the reporter pubkey in the value + let reporter_pubkey = Keys::from_str(&value)?.public_key(); + let report_request = ReportRequest::new(event, reporter_pubkey, None); + let category = ModerationCategory::from_str(&action_id).ok(); + let moderated_report = report_request.moderate(category); + info!( "Received interaction from {}. Action: {}, Value: {}", username, action_id, value ); - respond_with_replace(&response_url.to_string(), username, text, event.id).await?; + + let response_text = match moderated_report { + Some(moderated_report) => { + format!( + "Event reported by {} has been moderated with category: {}", + username, moderated_report + ) + } + None => format!("{} skipped moderation for {}", username, report_request), + }; + respond_with_replace(&response_url.to_string(), &response_text).await?; } _ => {} } @@ -125,17 +142,8 @@ async fn slack_interaction_handler( Ok(()) } -async fn respond_with_replace( - response_url: &str, - username: &str, - text: &str, - event_id: EventId, -) -> Result<()> { +async fn respond_with_replace(response_url: &str, response_text: &str) -> Result<()> { let client = Client::new(); - let response_text = format!( - "`{}` selected `{}` for event id `{}`", - username, text, event_id - ); let res = client .post(response_url) diff --git a/src/adapters/nostr_subscriber.rs b/src/adapters/nostr_subscriber.rs index cc6fdd6..8e34ecc 100644 --- a/src/adapters/nostr_subscriber.rs +++ b/src/adapters/nostr_subscriber.rs @@ -1,6 +1,6 @@ use crate::actors::messages::RelayEventDispatcherMessage; use crate::actors::Subscribe; -use crate::domain_objects::GiftWrap; +use crate::domain_objects::GiftWrappedReportRequest; use nostr_sdk::prelude::*; use ractor::{cast, concurrency::Duration, ActorRef}; use tokio_util::sync::CancellationToken; @@ -58,9 +58,10 @@ impl Subscribe for NostrSubscriber { } if let RelayPoolNotification::Event { event, .. } = notification { + let gift_wrapped_report_request = GiftWrappedReportRequest::try_from(*event)?; cast!( dispatcher_actor, - RelayEventDispatcherMessage::EventReceived(GiftWrap::new(*event)) + RelayEventDispatcherMessage::EventReceived(gift_wrapped_report_request) ) .expect("Failed to cast event to dispatcher"); } diff --git a/src/bin/giftwrapper.rs b/src/bin/giftwrapper.rs index 859f8ad..e6ba42d 100644 --- a/src/bin/giftwrapper.rs +++ b/src/bin/giftwrapper.rs @@ -1,6 +1,5 @@ use anyhow::Result; use nostr_sdk::prelude::*; -use reportinator_server::actors::gift_unwrapper::create_private_dm_message; use reportinator_server::domain_objects::ReportRequest; use std::env; use std::io::{self, BufRead}; @@ -30,13 +29,13 @@ async fn main() -> Result<()> { .expect("Failed to read line"); let sender_keys = Keys::generate(); - let report_request = ReportRequest { - reported_event: EventBuilder::text_note(&message, []).to_event(&sender_keys)?, - reporter_pubkey: sender_keys.public_key(), - reporter_text: Some("This is wrong, report it!".to_string()), - }; - let event_result = - create_private_dm_message(&report_request, &sender_keys, &receiver_pubkey).await; + let reported_event = EventBuilder::text_note(&message, []).to_event(&sender_keys)?; + let reporter_pubkey = sender_keys.public_key(); + let reporter_text = Some("This is wrong, report it!".to_string()); + let report_request = ReportRequest::new(reported_event, reporter_pubkey, reporter_text); + let event_result = report_request + .as_gift_wrap(&sender_keys, &receiver_pubkey) + .await; match event_result { Ok(event) => { diff --git a/src/domain_objects.rs b/src/domain_objects.rs index 6d5f57a..8da3ecd 100644 --- a/src/domain_objects.rs +++ b/src/domain_objects.rs @@ -1,47 +1,11 @@ -use anyhow::{bail, Context, Result}; -use nostr_sdk::prelude::*; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +pub mod gift_wrap; +pub use gift_wrap::GiftWrappedReportRequest; -//Newtype -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GiftWrap(Event); -impl GiftWrap { - pub fn new(event: Event) -> Self { - GiftWrap(event) - } +pub mod report_request; +pub use report_request::ReportRequest; - pub fn extract_report_request(&self, keys: &Keys) -> Result { - let unwrapped_gift = self.extract_rumor(keys)?; - let report_request = serde_json::from_str::(&unwrapped_gift.rumor.content) - .context("Failed to parse report request")?; +pub mod moderation_category; +pub use moderation_category::ModerationCategory; - if !report_request.valid() { - bail!("Invalid report request"); - } - - Ok(report_request) - } - - fn extract_rumor(&self, keys: &Keys) -> Result { - extract_rumor(keys, &self.0).context("Couldn't extract rumor") - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ReportRequest { - pub reported_event: Event, - pub reporter_pubkey: PublicKey, - pub reporter_text: Option, -} - -impl ReportRequest { - pub fn as_json(&self) -> String { - serde_json::to_string(self).expect("Failed to serialize ReportRequest to JSON") - } - - pub fn valid(&self) -> bool { - self.reported_event.verify().is_ok() - } -} +pub mod moderated_report; +pub use moderated_report::ModeratedReport; diff --git a/src/domain_objects/gift_wrap.rs b/src/domain_objects/gift_wrap.rs new file mode 100644 index 0000000..ab29dc2 --- /dev/null +++ b/src/domain_objects/gift_wrap.rs @@ -0,0 +1,44 @@ +use crate::domain_objects::ReportRequest; +use anyhow::{bail, Context, Result}; +use nostr_sdk::prelude::*; +use std::convert::TryFrom; +use std::fmt::Debug; + +//Newtype +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GiftWrappedReportRequest(Event); +impl GiftWrappedReportRequest { + fn new(event: Event) -> Self { + GiftWrappedReportRequest(event) + } + + pub fn as_json(&self) -> String { + self.0.as_json() + } + + pub fn extract_report_request(&self, keys: &Keys) -> Result { + let unwrapped_gift = extract_rumor(keys, &self.0).context("Couldn't extract rumor")?; + + let report_request = serde_json::from_str::(&unwrapped_gift.rumor.content) + .context("Failed to parse report request")?; + + if !report_request.valid() { + bail!("{} is not a valid gift wrapped report request", self.0.id()); + } + + Ok(report_request) + } +} + +impl TryFrom for GiftWrappedReportRequest { + // TODO: We should have better custom errors at some point + type Error = anyhow::Error; + + fn try_from(event: Event) -> Result { + if event.kind == Kind::GiftWrap { + Ok(GiftWrappedReportRequest::new(event)) + } else { + bail!("Event kind is not 1059") + } + } +} diff --git a/src/domain_objects/moderated_report.rs b/src/domain_objects/moderated_report.rs new file mode 100644 index 0000000..b0bbe16 --- /dev/null +++ b/src/domain_objects/moderated_report.rs @@ -0,0 +1,23 @@ +use crate::domain_objects::{ModerationCategory, ReportRequest}; +use std::fmt::{self, Display, Formatter}; + +pub struct ModeratedReport { + pub request: ReportRequest, + pub category: Option, +} + +impl ModeratedReport { + pub(super) fn new(request: ReportRequest, category: Option) -> Self { + ModeratedReport { request, category } + } +} + +impl Display for ModeratedReport { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "ModeratedReport {{ request: {}, category: {:?} }}", + self.request, self.category + ) + } +} diff --git a/src/domain_objects/moderation_category.rs b/src/domain_objects/moderation_category.rs new file mode 100644 index 0000000..4dc71c7 --- /dev/null +++ b/src/domain_objects/moderation_category.rs @@ -0,0 +1,126 @@ +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 { + 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.", + } + } + + fn nip56_report_type(&self) -> &'static str { + match self { + ModerationCategory::Hate + | ModerationCategory::HateThreatening + | ModerationCategory::Harassment + | ModerationCategory::HarassmentThreatening + | ModerationCategory::SelfHarm + | ModerationCategory::SelfHarmIntent + | ModerationCategory::SelfHarmInstructions + | ModerationCategory::Violence + | ModerationCategory::ViolenceGraphic => "other", + + ModerationCategory::Sexual => "nudity", + + ModerationCategory::SexualMinors => "illegal", + } + } + + 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 = (); + + 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(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_str() { + assert_eq!( + ModerationCategory::from_str("hate"), + Ok(ModerationCategory::Hate) + ); + assert_eq!( + ModerationCategory::from_str("harassment"), + Ok(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(), "other"); + } + + #[test] + fn test_nip69() { + let violence = ModerationCategory::Violence; + assert_eq!(violence.nip69(), "VI"); + } +} diff --git a/src/domain_objects/report_request.rs b/src/domain_objects/report_request.rs new file mode 100644 index 0000000..ab5f9a1 --- /dev/null +++ b/src/domain_objects/report_request.rs @@ -0,0 +1,103 @@ +use super::{ModeratedReport, ModerationCategory}; +use crate::domain_objects::GiftWrappedReportRequest; +use anyhow::Result; +use nostr_sdk::prelude::*; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportRequest { + reported_event: Event, + reporter_pubkey: PublicKey, + reporter_text: Option, +} + +impl ReportRequest { + #[allow(unused)] + pub fn new( + reported_event: Event, + reporter_pubkey: PublicKey, + reporter_text: Option, + ) -> Self { + ReportRequest { + reported_event, + reporter_pubkey, + reporter_text, + } + } + + pub fn reported_event(&self) -> &Event { + &self.reported_event + } + + pub fn reporter_pubkey(&self) -> &PublicKey { + &self.reporter_pubkey + } + + #[allow(unused)] + pub fn reporter_text(&self) -> Option<&String> { + self.reporter_text.as_ref() + } + + pub fn as_json(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize ReportRequest to JSON") + } + + pub fn valid(&self) -> bool { + self.reported_event.verify().is_ok() + } + + pub fn moderate( + &self, + moderation_category: Option, + ) -> Option { + moderation_category.map(|category| ModeratedReport::new(self.clone(), Some(category))) + } + + // NOTE: This roughly creates a message as described by nip 17 but it's still + // not ready, just for testing purposes. There are more details to consider to + // properly implement the nip like created_at treatment. The nip itself is not + // finished at this time so hopefully in the future this can be done through the + // nostr crate. + pub async fn as_gift_wrap( + &self, + reporter_keys: &Keys, + receiver_pubkey: &PublicKey, + ) -> Result { + if self.reporter_pubkey() != &reporter_keys.public_key() { + return Err(anyhow::anyhow!( + "Reporter public key doesn't match the provided keys" + )); + } + // Compose rumor + let kind_14_rumor = EventBuilder::sealed_direct(receiver_pubkey.clone(), self.as_json()) + .to_unsigned_event(reporter_keys.public_key()); + + // Compose seal + let content: String = NostrSigner::Keys(reporter_keys.clone()) + .nip44_encrypt(receiver_pubkey.clone(), kind_14_rumor.as_json()) + .await?; + let kind_13_seal = EventBuilder::new(Kind::Seal, content, []).to_event(&reporter_keys)?; + + // Compose gift wrap + let kind_1059_gift_wrap: Event = + EventBuilder::gift_wrap_from_seal(&receiver_pubkey, &kind_13_seal, None)?; + + let gift_wrap = GiftWrappedReportRequest::try_from(kind_1059_gift_wrap)?; + Ok(gift_wrap) + } +} + +impl Display for ReportRequest { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "ReportRequest {{ reported_event: {}, reporter_pubkey: {}, reporter_text: {:?} }}", + self.reported_event.as_json(), + self.reporter_pubkey, + self.reporter_text + ) + } +}