diff --git a/crates/nostr/CHANGELOG.md b/crates/nostr/CHANGELOG.md index 831c0dddc..4046520b3 100644 --- a/crates/nostr/CHANGELOG.md +++ b/crates/nostr/CHANGELOG.md @@ -68,6 +68,9 @@ - Add `MachineReadablePrefix::Custom` variant (https://github.com/rust-nostr/nostr/pull/1258) - Add `RelayMetadata::{is_read, is_write}` functions (https://github.com/rust-nostr/nostr/pull/1290) - Add the kind number in the kind doc (https://github.com/rust-nostr/nostr/pull/1293) +- Add `EventBuilder::private_file_msg` and `EventBuilder::file_msg` +- Add `Kind::FileMessage` +- Add `TagStandard::FileType`, `TagStandard::EncryptionAlgorithm`, `TagStandard::DecryptionKey` and `TagStandard::DecryptionNonce` ### Removed diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index 0ccc55730..8175ab964 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -1556,6 +1556,30 @@ impl EventBuilder { Self::gift_wrap(signer, &receiver, rumor, []).await } + /// Private Direct file message + /// + /// + #[inline] + #[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))] + pub async fn private_file_msg( + signer: &T, + receiver: PublicKey, + encrypted_file: EncryptedFile, + rumor_extra_tags: I, + ) -> Result + where + T: NostrSigner, + I: IntoIterator, + { + let public_key: PublicKey = signer.get_public_key().await?; + let rumor = encrypted_file + .to_event_builder()? + .tag(Tag::public_key(receiver)) + .tags(rumor_extra_tags) + .build(public_key); + Self::gift_wrap(signer, &receiver, rumor, []).await + } + /// Mute list /// /// diff --git a/crates/nostr/src/event/kind.rs b/crates/nostr/src/event/kind.rs index de3a2b8bd..f62bbfdc7 100644 --- a/crates/nostr/src/event/kind.rs +++ b/crates/nostr/src/event/kind.rs @@ -138,6 +138,7 @@ kind_variants! { Seal => 13, "Seal", "", GiftWrap => 1059, "Gift Wrap", "", PrivateDirectMessage => 14, "Private Direct message", "", + FileMessage => 15, "File message", "", SetStall => 30017, "Set stall", "", SetProduct => 30018, "Set product", "", JobFeedback => 7000, "Job Feedback", "", diff --git a/crates/nostr/src/event/tag/kind.rs b/crates/nostr/src/event/tag/kind.rs index d0a77b5c1..ed524e978 100644 --- a/crates/nostr/src/event/tag/kind.rs +++ b/crates/nostr/src/event/tag/kind.rs @@ -63,12 +63,24 @@ pub enum TagKind<'a> { Dependency, /// Description Description, + /// Decryption key + /// + /// + DecryptionKey, + /// Decryption nonce + /// + /// + DecryptionNonce, /// Size of the file in pixels Dim, /// Emoji Emoji, /// Encrypted Encrypted, + /// Encryption algorithm + /// + /// + EncryptionAlgorithm, /// Ends Ends, /// Expiration @@ -81,6 +93,10 @@ pub enum TagKind<'a> { Extension, /// File File, + /// File type + /// + /// + FileType, /// Image Image, /// License of the shared content @@ -333,13 +349,17 @@ impl<'a> TagKind<'a> { Self::CurrentParticipants => "current_participants", Self::Dependency => "dep", Self::Description => "description", + Self::DecryptionKey => "decryption-key", + Self::DecryptionNonce => "decryption-nonce", Self::Dim => "dim", Self::Emoji => "emoji", Self::Encrypted => "encrypted", + Self::EncryptionAlgorithm => "encryption-algorithm", Self::Ends => "ends", Self::Expiration => "expiration", Self::Extension => "extension", Self::File => "file", + Self::FileType => "file-type", Self::Head => "HEAD", Self::Image => "image", Self::License => "license", diff --git a/crates/nostr/src/event/tag/standard.rs b/crates/nostr/src/event/tag/standard.rs index 7d00c4213..2ebfa6ee1 100644 --- a/crates/nostr/src/event/tag/standard.rs +++ b/crates/nostr/src/event/tag/standard.rs @@ -306,6 +306,22 @@ pub enum TagStandard { /// List of web URLs Web(Vec), Word(String), + /// File type + /// + /// + FileType(String), + /// Encryption algorithm. Note: NIP17 specifies that the encryption algorithm is aes-gcm + /// + /// + EncryptionAlgorithm, + /// Decryption key + /// + /// + DecryptionKey(String), + /// Decryption nonce + /// + /// + DecryptionNonce(u128), } impl TagStandard { @@ -750,6 +766,10 @@ impl TagStandard { Self::Protected => TagKind::Protected, Self::Alt(..) => TagKind::Alt, Self::Web(..) => TagKind::Web, + Self::FileType(..) => TagKind::FileType, + Self::EncryptionAlgorithm => TagKind::EncryptionAlgorithm, + Self::DecryptionKey(..) => TagKind::DecryptionKey, + Self::DecryptionNonce(..) => TagKind::DecryptionNonce, } } @@ -1083,6 +1103,15 @@ impl From for Vec { tag.extend(urls.into_iter().map(|url| url.to_string())); tag } + TagStandard::FileType(mime) => vec![tag_kind, mime], + TagStandard::EncryptionAlgorithm => { + // NIP17 specifies that the encryption algorithm is aes-gcm + // + // + vec![tag_kind, "aes-gcm".to_string()] + } + TagStandard::DecryptionKey(key) => vec![tag_kind, key], + TagStandard::DecryptionNonce(nonce) => vec![tag_kind, nonce.to_string()], }; // Tag can't be empty, require at least 1 value diff --git a/crates/nostr/src/nips/nip17.rs b/crates/nostr/src/nips/nip17.rs index 5b260f0da..190449a55 100644 --- a/crates/nostr/src/nips/nip17.rs +++ b/crates/nostr/src/nips/nip17.rs @@ -6,7 +6,65 @@ //! //! -use crate::{Event, RelayUrl, TagStandard}; +#![allow(clippy::wrong_self_convention)] + +use url::Url; + +use crate::event::builder::{Error, EventBuilder}; +use crate::{Event, Kind, RelayUrl, Tag, TagStandard}; + +/// Encrypted file +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EncryptedFile { + /// URL of the encrypted file + pub url: Url, + /// File type (e.g. "image/png", "application/pdf") + pub file_type: String, + /// Decryption key + pub decryption_key: String, + /// Decryption nonce + pub decryption_nonce: u128, + /// SHA256 hash of the file + pub hash: hashes::sha256::Hash, +} + +impl EncryptedFile { + pub(crate) fn to_event_builder(self) -> Result { + let mut tags: Vec = Vec::with_capacity(5); + + // Add file type + if !self.file_type.is_empty() { + tags.push(Tag::from_standardized_without_cell(TagStandard::FileType( + self.file_type, + ))); + } + + // Add decryption key + if !self.decryption_key.is_empty() { + tags.push(Tag::from_standardized_without_cell( + TagStandard::DecryptionKey(self.decryption_key), + )); + } + + // Add decryption nonce + tags.push(Tag::from_standardized_without_cell( + TagStandard::DecryptionNonce(self.decryption_nonce), + )); + + // Add hash + tags.push(Tag::from_standardized_without_cell(TagStandard::Sha256( + self.hash, + ))); + + // Add encryption algorithm + tags.push(Tag::from_standardized_without_cell( + TagStandard::EncryptionAlgorithm, + )); + + // Build + Ok(EventBuilder::new(Kind::FileMessage, self.url).tags(tags)) + } +} /// Extracts the relay list ///