use crate::tags::NostrTags;
use bourne::{JsonWrite, ToJson};
use nostro2_traits::hex::{FromHex as _, Hexable};
bourne::json! {
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct NostrNote {
pub pubkey: String,
pub created_at: i64,
pub kind: u32,
#[bourne(default)]
pub tags: NostrTags,
pub content: String,
#[bourne(skip_if_none)]
pub id: Option<String>,
#[bourne(skip_if_none)]
pub sig: Option<String>,
}
}
impl NostrNote {
#[must_use]
pub fn new() -> Self {
Self {
created_at: Self::now(),
..Self::default()
}
}
#[must_use]
pub fn builder() -> NostrNoteBuilder {
NostrNoteBuilder { note: Self::new() }
}
#[must_use]
pub fn text_note(content: impl Into<String>) -> Self {
Self {
content: content.into(),
kind: 1,
..Self::new()
}
}
#[must_use]
pub fn metadata(content: impl Into<String>) -> Self {
Self {
content: content.into(),
kind: 0,
..Self::new()
}
}
#[must_use]
pub fn with_kind(kind: u32) -> Self {
Self {
kind,
..Self::new()
}
}
#[must_use]
pub fn now() -> i64 {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.and_then(|d| i64::try_from(d.as_secs()).ok())
.unwrap_or(0)
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::cast_possible_truncation)]
{
(js_sys::Date::now() / 1000.0) as i64
}
}
#[must_use]
pub const fn with_timestamp(mut self, timestamp: i64) -> Self {
self.created_at = timestamp;
self
}
#[must_use]
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
#[must_use]
#[inline]
pub fn id_bytes(&self) -> Option<[u8; 32]> {
let mut out = [0_u8; 32];
self.id.as_ref()?.decode_hex_to_slice(&mut out).ok()?;
Some(out)
}
#[cfg(any(feature = "k256", feature = "secp256k1"))]
#[inline]
fn sig_bytes(&self) -> Option<[u8; 64]> {
let mut out = [0_u8; 64];
self.sig.as_ref()?.decode_hex_to_slice(&mut out).ok()?;
Some(out)
}
#[cfg(any(feature = "k256", feature = "secp256k1"))]
#[inline]
fn pubkey_bytes(&self) -> Option<[u8; 32]> {
let mut out = [0_u8; 32];
self.pubkey.decode_hex_to_slice(&mut out).ok()?;
Some(out)
}
#[inline]
fn compute_id_bytes(&self) -> [u8; 32] {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
let mut sink = Sha256Sink(&mut hasher);
let _: Result<(), core::convert::Infallible> = (|| {
sink.write_byte(b'[')?;
sink.write_int_i64(0)?;
sink.write_byte(b',')?;
sink.write_escaped_str(&self.pubkey)?;
sink.write_byte(b',')?;
sink.write_int_i64(self.created_at)?;
sink.write_byte(b',')?;
sink.write_int_u64(u64::from(self.kind))?;
sink.write_byte(b',')?;
self.tags.write_json(&mut sink)?;
sink.write_byte(b',')?;
sink.write_escaped_str(&self.content)?;
sink.write_byte(b']')
})();
hasher.finalize().into()
}
pub fn serialize_id(&mut self) -> Result<(), crate::errors::NostrErrors> {
let hash = self.compute_id_bytes();
self.id = Some(Hexable::to_hex(&hash));
Ok(())
}
pub fn sign_with<S: nostro2_traits::NostrSigner + ?Sized>(
&mut self,
signer: &S,
) -> Result<(), crate::errors::NostrErrors> {
self.pubkey = signer.public_key();
let id = self.serialize_id_raw();
let sig = signer.sign_prehash(&id)?;
self.sig = Some(Hexable::to_hex(&sig));
Ok(())
}
pub fn serialize_id_raw(&mut self) -> [u8; 32] {
let hash = self.compute_id_bytes();
self.id = Some(Hexable::to_hex(&hash));
hash
}
#[cfg(feature = "k256")]
fn verify_signature(&self) -> Result<bool, crate::errors::NostrErrors> {
use k256::schnorr::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
let id = self
.id_bytes()
.ok_or(crate::errors::NostrErrors::MissingId)?;
let sig = self
.sig_bytes()
.ok_or(crate::errors::NostrErrors::MissingSignature)?;
let pubkey = self
.pubkey_bytes()
.ok_or(crate::errors::NostrErrors::InvalidPublicKey)?;
let verifying_key = VerifyingKey::from_bytes((&pubkey).into())?;
let signature = Signature::try_from(sig.as_slice())?;
Ok(verifying_key.verify_prehash(&id, &signature).is_ok())
}
#[cfg(feature = "secp256k1")]
#[allow(unknown_lints, crappy)]
fn verify_signature(&self) -> Result<bool, crate::errors::NostrErrors> {
use secp256k1::{schnorr::Signature, Message, XOnlyPublicKey, SECP256K1};
let id = self
.id_bytes()
.ok_or(crate::errors::NostrErrors::MissingId)?;
let sig_bytes = self
.sig_bytes()
.ok_or(crate::errors::NostrErrors::MissingSignature)?;
let pubkey = self
.pubkey_bytes()
.ok_or(crate::errors::NostrErrors::InvalidPublicKey)?;
let xonly = XOnlyPublicKey::from_slice(&pubkey)?;
let sig = Signature::from_slice(sig_bytes.as_slice())?;
let msg = Message::from_digest(id);
Ok(SECP256K1.verify_schnorr(&sig, &msg, &xonly).is_ok())
}
#[cfg(any(feature = "k256", feature = "secp256k1"))]
#[inline]
fn verify_content(&self) -> bool {
let Some(stored_id) = self.id_bytes() else {
return false;
};
stored_id == self.compute_id_bytes()
}
#[cfg(any(feature = "k256", feature = "secp256k1"))]
#[must_use]
#[inline]
pub fn verify(&self) -> bool {
self.verify_content() && self.verify_signature().is_ok_and(|t| t)
}
pub fn serialize(&self) -> Result<String, crate::errors::NostrErrors> {
Ok(bourne::to_string(self)?)
}
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) struct Sha256Sink<'a>(pub(crate) &'a mut sha2::Sha256);
impl JsonWrite for Sha256Sink<'_> {
type Error = core::convert::Infallible;
#[inline]
fn write_byte(&mut self, b: u8) -> Result<(), Self::Error> {
use sha2::Digest as _;
self.0.update([b]);
Ok(())
}
#[inline]
fn write_str_raw(&mut self, s: &str) -> Result<(), Self::Error> {
use sha2::Digest as _;
self.0.update(s.as_bytes());
Ok(())
}
#[inline]
fn write_float_f64(&mut self, f: f64) -> Result<(), Self::Error> {
use sha2::Digest as _;
use std::io::Write as _;
let mut buf = [0_u8; 24];
let n = write!(&mut buf[..], "{f}").map_or(0, |()| {
buf.iter().position(|&b| b == 0).unwrap_or(buf.len())
});
self.0.update(&buf[..n]);
Ok(())
}
}
impl core::str::FromStr for NostrNote {
type Err = crate::errors::NostrErrors;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(bourne::parse_str(s)?)
}
}
#[derive(Debug)]
pub struct NostrNoteBuilder {
note: NostrNote,
}
impl NostrNoteBuilder {
#[must_use]
pub fn content(mut self, content: impl Into<String>) -> Self {
self.note.content = content.into();
self
}
#[must_use]
pub const fn kind(mut self, kind: u32) -> Self {
self.note.kind = kind;
self
}
#[must_use]
pub const fn timestamp(mut self, timestamp: i64) -> Self {
self.note.created_at = timestamp;
self
}
#[must_use]
pub fn tag_pubkey(mut self, pubkey: &str) -> Self {
self.note.tags.add_pubkey_tag(pubkey, None);
self
}
#[must_use]
pub fn tag_pubkey_with_relay(mut self, pubkey: &str, relay: &str) -> Self {
self.note.tags.add_pubkey_tag(pubkey, Some(relay));
self
}
#[must_use]
pub fn tag_event(mut self, event_id: &str) -> Self {
self.note.tags.add_event_tag(event_id);
self
}
#[must_use]
pub fn tag_parameter(mut self, parameter: &str) -> Self {
self.note.tags.add_parameter_tag(parameter);
self
}
#[must_use]
pub fn tag(mut self, tag_type: &str, value: &str) -> Self {
self.note.tags.add_custom_tag(tag_type, value);
self
}
#[must_use]
pub fn tag_relay(mut self, url: &str) -> Self {
self.note.tags.add_relay_tag(url);
self
}
#[must_use]
pub fn build(self) -> NostrNote {
self.note
}
}