use nostr_sdk::prelude::*;
use crate::rumor::{RumorProcessingResult, RumorEvent, RumorContext, ConversationType, process_rumor};
use crate::types::Message;
use crate::state::WRAPPER_ID_CACHE;
pub trait InboundEventHandler: Send + Sync {
fn on_dm_received(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
fn on_file_received(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
fn on_reaction_received(&self, _chat_id: &str, _msg: &Message) {}
fn on_message_deleted(&self, _chat_id: &str, _message_id: &str) {}
fn on_community_invite(&self, _community_id: &str) {}
fn on_community_message(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
fn on_community_update(&self, _chat_id: &str, _target_id: &str, _msg: &Message) {}
fn on_community_removed(&self, _chat_id: &str, _target_id: &str) {}
#[allow(clippy::too_many_arguments)]
fn on_community_presence(
&self,
_chat_id: &str,
_npub: &str,
_joined: bool,
_event_id: &str,
_created_at: u64,
_invited_by: Option<&str>,
_invited_label: Option<&str>,
) {}
fn on_community_typing(&self, _chat_id: &str, _npub: &str, _until: u64) {}
#[allow(clippy::too_many_arguments)]
fn on_community_webxdc(
&self,
_chat_id: &str,
_npub: &str,
_topic_id: &str,
_node_addr: Option<&str>,
_event_id: &str,
_created_at: u64,
) {}
fn on_community_self_removed(&self, _community_id: &str) {}
fn on_community_refreshed(&self, _community_id: &str) {}
fn on_community_dissolved(&self, _community_id: &str) {}
}
pub struct NoOpEventHandler;
impl InboundEventHandler for NoOpEventHandler {}
pub enum PreparedEvent {
Processed {
result: RumorProcessingResult,
contact: String,
sender: PublicKey,
is_mine: bool,
wrapper_event_id: String,
wrapper_event_id_bytes: [u8; 32],
wrapper_created_at: u64,
unwrap_ns: u64,
parse_ns: u64,
},
CommunityInvite {
invite: crate::community::invite::CommunityInvite,
inviter: String,
is_mine: bool,
wrapper_event_id_bytes: [u8; 32],
wrapper_created_at: u64,
},
CommunityInviteV2 {
bundle_json: String,
community_id: String,
inviter: String,
is_mine: bool,
wrapper_event_id_bytes: [u8; 32],
wrapper_created_at: u64,
},
DedupSkip {
wrapper_id_bytes: [u8; 32],
wrapper_created_at: u64,
},
ErrorSkip {
wrapper_id_bytes: [u8; 32],
wrapper_created_at: u64,
},
}
pub async fn prepare_event(
event: Event,
client: &Client,
my_public_key: PublicKey,
) -> PreparedEvent {
let wrapper_created_at = event.created_at.as_secs();
let wrapper_event_id_bytes: [u8; 32] = event.id.to_bytes();
let wrapper_event_id = event.id.to_hex();
{
let cache = WRAPPER_ID_CACHE.lock().await;
if cache.contains(&wrapper_event_id_bytes) {
return PreparedEvent::DedupSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at };
}
}
if let Ok(true) = crate::db::events::wrapper_event_exists(&wrapper_event_id) {
return PreparedEvent::DedupSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at };
}
let unwrap_start = std::time::Instant::now();
let (rumor, sender) = match client.unwrap_gift_wrap(&event).await {
Ok(UnwrappedGift { rumor, sender }) => (rumor, sender),
Err(_) => return PreparedEvent::ErrorSkip {
wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
},
};
let unwrap_ns = unwrap_start.elapsed().as_nanos() as u64;
let is_mine = sender == my_public_key;
let contact = if is_mine {
rumor.tags.public_keys().next()
.and_then(|pk| pk.to_bech32().ok())
.unwrap_or_else(|| sender.to_bech32().unwrap_or_default())
} else {
sender.to_bech32().unwrap_or_default()
};
if rumor.tags.public_keys().count() > 1 {
return PreparedEvent::ErrorSkip {
wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
};
}
if rumor.kind == Kind::Custom(crate::stored_event::event_kind::COMMUNITY_INVITE_BUNDLE) {
return match crate::community::invite::parse_invite_rumor(rumor.kind, &rumor.content) {
Some(invite) => PreparedEvent::CommunityInvite {
invite, inviter: contact.clone(), is_mine, wrapper_event_id_bytes, wrapper_created_at,
},
None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
};
}
if rumor.kind == Kind::Custom(crate::community::v2::kind::DIRECT_INVITE) {
return match crate::community::v2::invite::CommunityInvite::from_bundle_json(&rumor.content)
.ok()
.and_then(|b| serde_json::to_string(&b).ok().map(|j| (b.community_id, j)))
{
Some((community_id, bundle_json)) => PreparedEvent::CommunityInviteV2 {
community_id,
bundle_json,
inviter: contact.clone(), is_mine,
wrapper_event_id_bytes,
wrapper_created_at,
},
None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
};
}
let Some(rumor_id) = rumor.id else {
return PreparedEvent::ErrorSkip {
wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
};
};
let rumor_event = RumorEvent {
id: rumor_id,
kind: rumor.kind,
content: rumor.content,
tags: rumor.tags,
created_at: rumor.created_at,
pubkey: rumor.pubkey,
};
let rumor_context = RumorContext {
sender,
is_mine,
conversation_id: contact.clone(),
conversation_type: ConversationType::DirectMessage,
};
let parse_start = std::time::Instant::now();
let download_dir = crate::db::get_download_dir();
match process_rumor(rumor_event, rumor_context, &download_dir) {
Ok(result) => {
let parse_ns = parse_start.elapsed().as_nanos() as u64;
PreparedEvent::Processed {
result, contact, sender, is_mine,
wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at,
unwrap_ns, parse_ns,
}
}
Err(e) => {
log_warn!("[EventHandler] Failed to process rumor: {}", e);
PreparedEvent::ErrorSkip {
wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
}
}
}
}
pub async fn commit_prepared_event(
prepared: PreparedEvent,
is_new: bool,
handler: &dyn InboundEventHandler,
) -> bool {
let session = crate::state::SessionGuard::capture();
if !session.is_valid() {
return false;
}
match prepared {
PreparedEvent::Processed { result, contact, sender, is_mine, wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at, .. } => {
{
let mut cache = WRAPPER_ID_CACHE.lock().await;
cache.insert(wrapper_event_id_bytes);
}
let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
if !is_mine {
let state = crate::state::STATE.lock().await;
if state.get_profile(&contact).map_or(false, |p| p.flags.is_blocked()) {
return false;
}
}
match result {
RumorProcessingResult::TextMessage(mut msg) => {
msg.wrapper_event_id = Some(wrapper_event_id.clone());
commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, handler, false).await
}
RumorProcessingResult::FileAttachment(mut msg) => {
msg.wrapper_event_id = Some(wrapper_event_id.clone());
if !is_mine {
for att in &mut msg.attachments {
if att.size == 0
&& (att.url.starts_with("https://") || att.url.starts_with("http://"))
{
if let Ok(Some(size)) = tokio::time::timeout(
std::time::Duration::from_secs(3),
crate::net::get_remote_file_size(&att.url),
).await {
att.size = size;
}
}
}
}
commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, handler, true).await
}
RumorProcessingResult::Reaction(reaction) => {
commit_reaction(reaction, &contact, is_mine, &wrapper_event_id, handler).await
}
RumorProcessingResult::Edit { message_id, new_content, edited_at, emoji_tags, mut event } => {
commit_edit(&mut event, &contact, &message_id, &new_content, edited_at, emoji_tags, &wrapper_event_id).await
}
RumorProcessingResult::TypingIndicator { profile_id, until } => {
let active_typers = {
let mut state = crate::state::STATE.lock().await;
state.update_typing_and_get_active(&contact, &profile_id, until)
};
crate::traits::emit_event("typing-update", &serde_json::json!({
"conversation_id": contact,
"typers": active_typers,
}));
false
}
RumorProcessingResult::PivxPayment { gift_code, amount_piv, address, message_id, mut event } => {
if crate::db::events::event_exists(&event.id).unwrap_or(false) {
return false;
}
event.wrapper_event_id = Some(wrapper_event_id.clone());
let ts = event.created_at;
let _ = crate::db::events::save_pivx_payment_event(&contact, event).await;
crate::traits::emit_event("pivx_payment_received", &serde_json::json!({
"conversation_id": contact,
"gift_code": gift_code, "amount_piv": amount_piv,
"address": address, "message_id": message_id,
"sender": sender.to_hex(), "is_mine": is_mine,
"at": ts * 1000,
}));
true
}
RumorProcessingResult::UnknownEvent(mut event) => {
event.wrapper_event_id = Some(wrapper_event_id.clone());
if let Ok(chat_id) = crate::db::id_cache::get_or_create_chat_id(&contact) {
event.chat_id = chat_id;
}
let _ = crate::db::events::save_event(&event).await;
false
}
RumorProcessingResult::LeaveRequest { .. } => false,
RumorProcessingResult::WebxdcPeerAdvertisement { .. } |
RumorProcessingResult::WebxdcPeerLeft { .. } => {
false
}
RumorProcessingResult::WallpaperChanged {
sender_npub, created_at, url, decryption_key, decryption_nonce,
plaintext_hash, mime, blur, dim, event_id,
} => {
let _ = crate::wallpaper::apply_received_wallpaper(
&contact, &sender_npub, created_at, &url,
&decryption_key, &decryption_nonce,
plaintext_hash.as_deref(), mime.as_deref(),
blur, dim,
&event_id,
).await;
true
}
RumorProcessingResult::DeletionRequest { target_event_id } => {
if commit_deletion(&target_event_id, &contact, &sender, handler).await {
true
} else {
commit_reaction_deletion(&target_event_id, &sender).await
}
}
RumorProcessingResult::Ignored => false,
}
}
PreparedEvent::CommunityInvite { invite, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at } => {
{
let mut cache = WRAPPER_ID_CACHE.lock().await;
cache.insert(wrapper_event_id_bytes);
}
let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
if is_mine {
return false;
}
if let Err(e) = invite.validate() {
log_warn!("[community] invite rejected: {}", e);
return false;
}
let community_id = invite.community_id.clone();
let already_held = crate::community::CommunityId(
match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
Some(b) => b,
None => { log_warn!("[community] invite has malformed id"); return false; }
},
);
if crate::db::community::community_exists(&already_held).unwrap_or(false) {
return false;
}
if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
return false;
}
if crate::community::list::tombstone_suppresses(&community_id, wrapper_created_at) {
return false;
}
let bundle_json = match invite.to_json() {
Ok(j) => j,
Err(e) => { log_warn!("[community] invite re-serialize failed: {}", e); return false; }
};
match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter) {
Ok(true) => {
handler.on_community_invite(&community_id);
let invite_warm = invite.clone();
let bg = crate::state::SessionGuard::capture();
tokio::spawn(async move {
if !bg.is_valid() {
return;
}
crate::community::service::preload_community(&invite_warm).await;
});
}
Ok(false) => {} Err(e) => log_warn!("[community] invite park failed: {}", e),
}
false
}
PreparedEvent::CommunityInviteV2 { bundle_json, community_id, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at } => {
{
let mut cache = WRAPPER_ID_CACHE.lock().await;
cache.insert(wrapper_event_id_bytes);
}
let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
if is_mine {
return false;
}
let held = match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
Some(b) => crate::community::CommunityId(b),
None => return false,
};
if crate::db::community::community_exists(&held).unwrap_or(false) {
return false;
}
if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
return false;
}
if crate::community::list::tombstone_suppresses(&community_id, wrapper_created_at) {
return false;
}
match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter) {
Ok(true) => handler.on_community_invite(&community_id),
Ok(false) => {} Err(e) => log_warn!("[community] v2 invite park failed: {}", e),
}
false
}
PreparedEvent::DedupSkip { wrapper_id_bytes, wrapper_created_at } => {
if wrapper_created_at > 0 {
let _ = crate::db::wrappers::update_wrapper_timestamp(&wrapper_id_bytes, wrapper_created_at);
}
false
}
PreparedEvent::ErrorSkip { wrapper_id_bytes, wrapper_created_at } => {
let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
false
}
}
}
async fn commit_dm_message(
mut msg: Message,
contact: &str,
_is_mine: bool,
is_new: bool,
wrapper_event_id: &str,
wrapper_event_id_bytes: [u8; 32],
handler: &dyn InboundEventHandler,
is_file: bool,
) -> bool {
if let Ok(true) = crate::db::events::message_exists_in_db(&msg.id) {
if let Ok(updated) = crate::db::events::update_wrapper_event_id(&msg.id, wrapper_event_id) {
if !updated {
let mut cache = WRAPPER_ID_CACHE.lock().await;
cache.insert(wrapper_event_id_bytes);
}
}
return false;
}
if !msg.replied_to.is_empty() {
let _ = crate::db::events::populate_reply_context(&mut msg).await;
}
let added = {
let mut state = crate::state::STATE.lock().await;
let added = state.add_message_to_participant(contact, msg.clone());
if is_file && added {
state.update_typing_and_get_active(contact, contact, 0);
}
added
};
if added {
crate::traits::emit_event("message_new", &serde_json::json!({
"message": &msg,
"chat_id": contact
}));
if is_file {
handler.on_file_received(contact, &msg, is_new);
} else {
handler.on_dm_received(contact, &msg, is_new);
}
let _ = crate::db::events::save_message(contact, &msg).await;
}
added
}
async fn commit_reaction(
reaction: crate::types::Reaction,
contact: &str,
is_mine: bool,
wrapper_event_id: &str,
handler: &dyn InboundEventHandler,
) -> bool {
let msg_for_emit = {
let mut state = crate::state::STATE.lock().await;
if let Some((chat_id, was_added)) = state.add_reaction_to_message(&reaction.reference_id, reaction.clone()) {
if was_added {
state.find_message(&reaction.reference_id)
.map(|(_, msg)| (chat_id, msg))
} else { None }
} else { None }
};
if let Some((chat_id, msg)) = msg_for_emit {
crate::traits::emit_event("message_update", &serde_json::json!({
"old_id": &reaction.reference_id,
"message": &msg,
"chat_id": &chat_id
}));
let _ = crate::db::events::save_message(&chat_id, &msg).await;
handler.on_reaction_received(&chat_id, &msg);
}
if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
let _ = crate::db::events::save_reaction_event(
&reaction, chat_id, None, is_mine, Some(wrapper_event_id.to_string())
).await;
}
true
}
async fn commit_edit(
event: &mut crate::stored_event::StoredEvent,
contact: &str,
message_id: &str,
new_content: &str,
edited_at: u64,
emoji_tags: Vec<crate::types::EmojiTag>,
wrapper_event_id: &str,
) -> bool {
if crate::db::events::event_exists(&event.id).unwrap_or(false) {
return false;
}
if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
event.chat_id = chat_id;
}
event.wrapper_event_id = Some(wrapper_event_id.to_string());
let _ = crate::db::events::save_event(event).await;
let msg_for_emit = {
let mut state = crate::state::STATE.lock().await;
state.update_message_in_chat(contact, message_id, |msg| {
msg.apply_edit(new_content.to_string(), edited_at, emoji_tags.clone());
})
};
if let Some(msg) = msg_for_emit {
crate::traits::emit_event("message_update", &serde_json::json!({
"old_id": message_id,
"message": msg,
"chat_id": contact
}));
}
true
}
async fn commit_deletion(
target_event_id: &str,
contact: &str,
sender: &PublicKey,
handler: &dyn InboundEventHandler,
) -> bool {
let (mine, chat_id) = {
let state = crate::state::STATE.lock().await;
match state.find_message(target_event_id) {
Some((chat, msg)) => (msg.mine, chat.id.clone()),
None => return false,
}
};
let authorized = if mine {
match crate::state::my_public_key() {
Some(my_pk) => *sender == my_pk,
None => false,
}
} else {
match nostr_sdk::PublicKey::from_bech32(&chat_id) {
Ok(counterpart) => sender == &counterpart,
Err(_) => false, }
};
if !authorized {
eprintln!(
"[NIP-17 cooperative-delete] unauthorized: sender {} not the author of target {} (mine={}, chat={})",
sender.to_hex(), target_event_id, mine, chat_id
);
return false;
}
let removed = {
let mut state = crate::state::STATE.lock().await;
state.remove_message(target_event_id)
};
let removed_msg = match removed {
Some((_chat_id, msg)) => msg,
None => return false,
};
let unique = crate::deletion::filter_unreferenced_attachments(
target_event_id,
removed_msg.attachments,
).await;
crate::deletion::delete_cached_attachment_files_pub(&unique);
if let Err(e) = crate::db::events::delete_event(target_event_id).await {
eprintln!(
"[NIP-17 cooperative-delete] DB delete failed for {}: {}",
target_event_id, e
);
}
crate::traits::emit_event(
"message_removed",
&serde_json::json!({
"id": target_event_id,
"chat_id": &chat_id,
"reason": "deleted-by-sender",
}),
);
handler.on_message_deleted(&chat_id, target_event_id);
let _ = contact;
true
}
async fn commit_reaction_deletion(target_reaction_id: &str, sender: &PublicKey) -> bool {
let found = {
let state = crate::state::STATE.lock().await;
state.find_reaction(target_reaction_id)
};
let (chat_id, message_id, author_npub, _is_community) = match found {
Some(v) => v,
None => return false,
};
let authorized = nostr_sdk::PublicKey::parse(&author_npub)
.map(|pk| pk == *sender)
.unwrap_or(false);
if !authorized {
eprintln!(
"[reaction-delete] unauthorized: sender {} is not the author of reaction {}",
sender.to_hex(), target_reaction_id
);
return false;
}
let updated = {
let mut state = crate::state::STATE.lock().await;
state.remove_reaction_from_message(&message_id, target_reaction_id)
};
let message = match updated {
Some((_cid, msg)) => msg,
None => return false,
};
if let Err(e) = crate::db::events::delete_event(target_reaction_id).await {
eprintln!("[reaction-delete] DB delete failed for {}: {}", target_reaction_id, e);
}
crate::traits::emit_event(
"message_update",
&serde_json::json!({
"old_id": &message_id,
"message": &message,
"chat_id": &chat_id,
}),
);
true
}
pub async fn process_event(
event: Event,
is_new: bool,
handler: &dyn InboundEventHandler,
) -> std::result::Result<bool, String> {
let client = crate::state::nostr_client()
.ok_or_else(|| "Nostr client not initialized".to_string())?;
let my_pk = crate::state::my_public_key()
.ok_or_else(|| "Public key not initialized".to_string())?;
let prepared = prepare_event(event, &client, my_pk).await;
Ok(commit_prepared_event(prepared, is_new, handler).await)
}