sentrystr 0.2.0

Core library for publishing Sentry-like events to Nostr network
Documentation
use crate::{EncryptionHelper, Event, Result, SentryStrError};
use chrono::{DateTime, Utc};
use nostr::prelude::*;
use nostr_sdk::prelude::*;
use serde_json;

#[derive(Debug, Clone)]
pub struct DirectMessageConfig {
    pub recipient_pubkey: PublicKey,
    pub min_level: Option<crate::Level>,
    pub use_nip17: bool,
}

#[derive(Debug)]
pub struct MessageEvent {
    pub event: Event,
    pub author: PublicKey,
    pub nostr_event_id: EventId,
    pub received_at: DateTime<Utc>,
}

#[derive(Clone)]
pub struct DirectMessageSender {
    client: Client,
    keys: Keys,
    config: DirectMessageConfig,
}

impl DirectMessageSender {
    pub fn new(client: Client, keys: Keys, config: DirectMessageConfig) -> Self {
        Self {
            client,
            keys,
            config,
        }
    }

    pub async fn send_message_for_event(&self, event: &MessageEvent) -> Result<()> {
        if !self.should_send_for_level(&event.event.level) {
            return Ok(());
        }

        let event_json = serde_json::to_string_pretty(&event.event)?;
        let message_content = format!(
            "SentryStr Alert\n\nEvent ID: {}\nAuthor: {}\nTimestamp: {}\nLevel: {:?}\n\nEvent Data:\n{}",
            event.nostr_event_id,
            event.author,
            event.event.timestamp,
            event.event.level,
            event_json
        );

        if self.config.use_nip17 {
            self.send_nip17_message(&message_content).await
        } else {
            self.send_nip44_message(&message_content).await
        }
    }

    pub async fn send_custom_message(&self, content: &str) -> Result<()> {
        if self.config.use_nip17 {
            self.send_nip17_message(content).await
        } else {
            self.send_nip44_message(content).await
        }
    }

    fn should_send_for_level(&self, event_level: &crate::Level) -> bool {
        if let Some(ref min_level) = self.config.min_level {
            match (min_level, event_level) {
                (crate::Level::Debug, _) => true,
                (crate::Level::Info, crate::Level::Debug) => false,
                (crate::Level::Info, _) => true,
                (crate::Level::Warning, crate::Level::Debug | crate::Level::Info) => false,
                (crate::Level::Warning, _) => true,
                (
                    crate::Level::Error,
                    crate::Level::Debug | crate::Level::Info | crate::Level::Warning,
                ) => false,
                (crate::Level::Error, _) => true,
                (crate::Level::Fatal, crate::Level::Fatal) => true,
                (crate::Level::Fatal, _) => false,
            }
        } else {
            true
        }
    }

    async fn send_nip17_message(&self, content: &str) -> Result<()> {
        const MAX_RETRIES: u32 = 3;
        const BASE_DELAY_MS: u64 = 1000;

        for attempt in 0..MAX_RETRIES {
            match self
                .client
                .send_private_msg(self.config.recipient_pubkey, content, [])
                .await
            {
                Ok(_) => {
                    if attempt > 0 {
                        eprintln!("Successfully sent NIP-17 message after {} retries", attempt);
                    }
                    return Ok(());
                }
                Err(e) => {
                    eprintln!("NIP-17 send attempt {} failed: {}", attempt + 1, e);
                    if attempt < MAX_RETRIES - 1 {
                        let delay =
                            std::time::Duration::from_millis(BASE_DELAY_MS * (1 << attempt));
                        tokio::time::sleep(delay).await;
                    }
                }
            }
        }

        Err(SentryStrError::Config(
            "Failed to send NIP-17 message after retries".to_string(),
        ))
    }

    async fn send_nip44_message(&self, content: &str) -> Result<()> {
        const MAX_RETRIES: u32 = 3;
        const BASE_DELAY_MS: u64 = 1000;

        for attempt in 0..MAX_RETRIES {
            let encrypted_content = EncryptionHelper::encrypt_nip44(
                self.keys.secret_key(),
                &self.config.recipient_pubkey,
                content,
            )?;

            let dm_event = EventBuilder::new(Kind::EncryptedDirectMessage, encrypted_content)
                .tag(Tag::public_key(self.config.recipient_pubkey))
                .sign_with_keys(&self.keys)?;

            match self.client.send_event(&dm_event).await {
                Ok(_) => {
                    if attempt > 0 {
                        eprintln!("Successfully sent NIP-44 message after {} retries", attempt);
                    }
                    return Ok(());
                }
                Err(e) => {
                    eprintln!("NIP-44 send attempt {} failed: {}", attempt + 1, e);
                    if attempt < MAX_RETRIES - 1 {
                        let delay =
                            std::time::Duration::from_millis(BASE_DELAY_MS * (1 << attempt));
                        tokio::time::sleep(delay).await;
                    }
                }
            }
        }

        Err(SentryStrError::Config(
            "Failed to send NIP-44 message after retries".to_string(),
        ))
    }
}

pub struct DirectMessageBuilder {
    client: Option<Client>,
    keys: Option<Keys>,
    recipient_pubkey: Option<PublicKey>,
    min_level: Option<crate::Level>,
    use_nip17: bool,
}

impl DirectMessageBuilder {
    pub fn new() -> Self {
        Self {
            client: None,
            keys: None,
            recipient_pubkey: None,
            min_level: None,
            use_nip17: false,
        }
    }

    pub fn with_client(mut self, client: Client) -> Self {
        self.client = Some(client);
        self
    }

    pub fn with_keys(mut self, keys: Keys) -> Self {
        self.keys = Some(keys);
        self
    }

    pub fn with_recipient(mut self, pubkey: PublicKey) -> Self {
        self.recipient_pubkey = Some(pubkey);
        self
    }

    pub fn with_min_level(mut self, level: crate::Level) -> Self {
        self.min_level = Some(level);
        self
    }

    pub fn with_nip17(mut self, use_nip17: bool) -> Self {
        self.use_nip17 = use_nip17;
        self
    }

    pub fn build(self) -> Result<DirectMessageSender> {
        let client = self.client.ok_or_else(|| {
            SentryStrError::Config("Client is required for DirectMessageSender".to_string())
        })?;

        let keys = self.keys.ok_or_else(|| {
            SentryStrError::Config("Keys are required for DirectMessageSender".to_string())
        })?;

        let recipient_pubkey = self.recipient_pubkey.ok_or_else(|| {
            SentryStrError::Config(
                "Recipient pubkey is required for DirectMessageSender".to_string(),
            )
        })?;

        let config = DirectMessageConfig {
            recipient_pubkey,
            min_level: self.min_level,
            use_nip17: self.use_nip17,
        };

        Ok(DirectMessageSender::new(client, keys, config))
    }
}

impl Default for DirectMessageBuilder {
    fn default() -> Self {
        Self::new()
    }
}