use nostr::nips::nip44;
use nostr_sdk::prelude::*;
use crate::error::{MostroError, ServiceError};
pub const CHAT_MAX_CLOCK_SKEW_SECS: u64 = 60;
pub const CHAT_MAX_CONTENT_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub content: String,
pub sender: PublicKey,
pub created_at: Timestamp,
pub inner_event_id: EventId,
pub outer_event_id: EventId,
}
pub fn unwrap_chat_message(
conv: &Keys,
sign_pubkey: &PublicKey,
allowed_signers: &[PublicKey],
outer: &Event,
now: Timestamp,
) -> Result<ChatMessage, MostroError> {
if outer.pubkey != *sign_pubkey {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"outer event is not authored by the conversation signing key".to_string(),
),
));
}
if outer.kind != Kind::PrivateDirectMessage {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("outer event is not kind 14".to_string()),
));
}
let mut p_tags = outer.tags.iter().filter(|t| t.kind() == "p");
match (p_tags.next().and_then(|t| t.content()), p_tags.next()) {
(Some(pk), None) if pk == conv.public_key().to_hex() => {}
_ => {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"outer event must carry exactly one p tag for this conversation".to_string(),
),
));
}
}
if outer.created_at.as_secs() > now.as_secs().saturating_add(CHAT_MAX_CLOCK_SKEW_SECS) {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("outer event is dated too far in the future".to_string()),
));
}
if outer.content.len() > CHAT_MAX_CONTENT_BYTES {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"encrypted payload exceeds the accepted size".to_string(),
),
));
}
outer.verify().map_err(|e| {
MostroError::MostroInternalErr(ServiceError::NostrError(format!(
"invalid outer chat signature: {e}"
)))
})?;
let decrypted =
nip44::decrypt(conv.secret_key(), &conv.public_key(), &outer.content).map_err(|e| {
MostroError::MostroInternalErr(ServiceError::DecryptionError(format!(
"K_conv decrypt failed: {e}"
)))
})?;
let inner = Event::from_json(&decrypted).map_err(|e| {
MostroError::MostroInternalErr(ServiceError::NostrError(format!(
"malformed inner chat event: {e}"
)))
})?;
inner.verify().map_err(|e| {
MostroError::MostroInternalErr(ServiceError::NostrError(format!(
"invalid inner chat signature: {e}"
)))
})?;
if !allowed_signers.contains(&inner.pubkey) {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"inner event is signed by a key that is not a party to this conversation"
.to_string(),
),
));
}
if inner.kind != Kind::TextNote {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("inner chat event is not a TextNote".to_string()),
));
}
let skew = inner
.created_at
.as_secs()
.abs_diff(outer.created_at.as_secs());
if skew > CHAT_MAX_CLOCK_SKEW_SECS {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError(
"inner and outer timestamps disagree — stale re-wrap".to_string(),
),
));
}
Ok(ChatMessage {
content: inner.content.clone(),
sender: inner.pubkey,
created_at: inner.created_at,
inner_event_id: inner.id,
outer_event_id: outer.id,
})
}
pub async fn unwrap_giftwrap_chat_message(
shared_keys: &Keys,
event: &Event,
) -> Result<ChatMessage, MostroError> {
if event.kind != Kind::GiftWrap {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("event is not a GiftWrap".to_string()),
));
}
let decrypted = nip44::decrypt(shared_keys.secret_key(), &event.pubkey, &event.content)
.map_err(|e| {
MostroError::MostroInternalErr(ServiceError::DecryptionError(format!(
"shared-key decrypt failed: {e}"
)))
})?;
let inner = Event::from_json(&decrypted).map_err(|e| {
MostroError::MostroInternalErr(ServiceError::NostrError(format!(
"malformed inner chat event: {e}"
)))
})?;
if inner.kind != Kind::TextNote {
return Err(MostroError::MostroInternalErr(
ServiceError::UnexpectedError("inner chat event is not a TextNote".to_string()),
));
}
inner.verify().map_err(|e| {
MostroError::MostroInternalErr(ServiceError::NostrError(format!(
"invalid inner chat signature: {e}"
)))
})?;
Ok(ChatMessage {
content: inner.content.clone(),
sender: inner.pubkey,
created_at: inner.created_at,
inner_event_id: inner.id,
outer_event_id: event.id,
})
}