1use nostr_sdk::prelude::*;
10
11use crate::rumor::{RumorProcessingResult, RumorEvent, RumorContext, ConversationType, process_rumor};
12use crate::types::Message;
13use crate::state::WRAPPER_ID_CACHE;
14
15pub trait InboundEventHandler: Send + Sync {
20 fn on_dm_received(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
22
23 fn on_file_received(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
25
26 fn on_reaction_received(&self, _chat_id: &str, _msg: &Message) {}
28
29 fn on_message_deleted(&self, _chat_id: &str, _message_id: &str) {}
32
33 fn on_community_invite(&self, _community_id: &str) {}
37
38 fn on_community_message(&self, _chat_id: &str, _msg: &Message, _is_new: bool) {}
42
43 fn on_community_update(&self, _chat_id: &str, _target_id: &str, _msg: &Message) {}
46
47 fn on_community_removed(&self, _chat_id: &str, _target_id: &str) {}
49
50 #[allow(clippy::too_many_arguments)]
53 fn on_community_presence(
54 &self,
55 _chat_id: &str,
56 _npub: &str,
57 _joined: bool,
58 _event_id: &str,
59 _created_at: u64,
60 _invited_by: Option<&str>,
61 _invited_label: Option<&str>,
62 ) {}
63
64 fn on_community_typing(&self, _chat_id: &str, _npub: &str, _until: u64) {}
66
67 #[allow(clippy::too_many_arguments)]
69 fn on_community_webxdc(
70 &self,
71 _chat_id: &str,
72 _npub: &str,
73 _topic_id: &str,
74 _node_addr: Option<&str>,
75 _event_id: &str,
76 _created_at: u64,
77 ) {}
78
79 fn on_community_self_removed(&self, _community_id: &str) {}
82
83 fn on_community_refreshed(&self, _community_id: &str) {}
86
87 fn on_community_dissolved(&self, _community_id: &str) {}
90}
91
92pub struct NoOpEventHandler;
94impl InboundEventHandler for NoOpEventHandler {}
95
96pub enum PreparedEvent {
98 Processed {
100 result: RumorProcessingResult,
101 contact: String,
102 sender: PublicKey,
103 is_mine: bool,
104 wrapper_event_id: String,
105 wrapper_event_id_bytes: [u8; 32],
106 wrapper_created_at: u64,
107 unwrap_ns: u64,
109 parse_ns: u64,
111 },
112 CommunityInvite {
114 invite: crate::community::invite::CommunityInvite,
115 inviter: String,
117 is_mine: bool,
118 wrapper_event_id_bytes: [u8; 32],
119 wrapper_created_at: u64,
120 },
121 CommunityInviteV2 {
124 bundle_json: String,
125 community_id: String,
126 inviter: String,
128 is_mine: bool,
129 wrapper_event_id_bytes: [u8; 32],
130 wrapper_created_at: u64,
131 },
132 DedupSkip {
134 wrapper_id_bytes: [u8; 32],
135 wrapper_created_at: u64,
136 },
137 ErrorSkip {
139 wrapper_id_bytes: [u8; 32],
140 wrapper_created_at: u64,
141 },
142}
143
144pub async fn prepare_event(
149 event: Event,
150 client: &Client,
151 my_public_key: PublicKey,
152) -> PreparedEvent {
153 let wrapper_created_at = event.created_at.as_secs();
154 let wrapper_event_id_bytes: [u8; 32] = event.id.to_bytes();
155 let wrapper_event_id = event.id.to_hex();
156
157 {
159 let cache = WRAPPER_ID_CACHE.lock().await;
160 if cache.contains(&wrapper_event_id_bytes) {
161 return PreparedEvent::DedupSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at };
162 }
163 }
164
165 if let Ok(true) = crate::db::events::wrapper_event_exists(&wrapper_event_id) {
166 return PreparedEvent::DedupSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at };
167 }
168
169 let unwrap_start = std::time::Instant::now();
171 let (rumor, sender) = match client.unwrap_gift_wrap(&event).await {
172 Ok(UnwrappedGift { rumor, sender }) => (rumor, sender),
173 Err(_) => return PreparedEvent::ErrorSkip {
174 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
175 },
176 };
177
178 let unwrap_ns = unwrap_start.elapsed().as_nanos() as u64;
179
180 let is_mine = sender == my_public_key;
181 let contact = if is_mine {
182 rumor.tags.public_keys().next()
183 .and_then(|pk| pk.to_bech32().ok())
184 .unwrap_or_else(|| sender.to_bech32().unwrap_or_default())
185 } else {
186 sender.to_bech32().unwrap_or_default()
187 };
188
189 if rumor.tags.public_keys().count() > 1 {
191 return PreparedEvent::ErrorSkip {
192 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
193 };
194 }
195
196 if rumor.kind == Kind::Custom(crate::stored_event::event_kind::COMMUNITY_INVITE_BUNDLE) {
199 return match crate::community::invite::parse_invite_rumor(rumor.kind, &rumor.content) {
200 Some(invite) => PreparedEvent::CommunityInvite {
201 invite, inviter: contact.clone(), is_mine, wrapper_event_id_bytes, wrapper_created_at,
202 },
203 None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
204 };
205 }
206
207 if rumor.kind == Kind::Custom(crate::community::v2::kind::DIRECT_INVITE) {
210 return match crate::community::v2::invite::CommunityInvite::from_bundle_json(&rumor.content)
211 .ok()
212 .and_then(|b| serde_json::to_string(&b).ok().map(|j| (b.community_id, j)))
213 {
214 Some((community_id, bundle_json)) => PreparedEvent::CommunityInviteV2 {
215 community_id,
216 bundle_json,
217 inviter: contact.clone(), is_mine,
219 wrapper_event_id_bytes,
220 wrapper_created_at,
221 },
222 None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
223 };
224 }
225
226 let Some(rumor_id) = rumor.id else {
228 return PreparedEvent::ErrorSkip {
229 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
230 };
231 };
232
233 let rumor_event = RumorEvent {
234 id: rumor_id,
235 kind: rumor.kind,
236 content: rumor.content,
237 tags: rumor.tags,
238 created_at: rumor.created_at,
239 pubkey: rumor.pubkey,
240 };
241 let rumor_context = RumorContext {
242 sender,
243 is_mine,
244 conversation_id: contact.clone(),
245 conversation_type: ConversationType::DirectMessage,
246 };
247
248 let parse_start = std::time::Instant::now();
249 let download_dir = crate::db::get_download_dir();
250 match process_rumor(rumor_event, rumor_context, &download_dir) {
251 Ok(result) => {
252 let parse_ns = parse_start.elapsed().as_nanos() as u64;
253 PreparedEvent::Processed {
254 result, contact, sender, is_mine,
255 wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at,
256 unwrap_ns, parse_ns,
257 }
258 }
259 Err(e) => {
260 log_warn!("[EventHandler] Failed to process rumor: {}", e);
261 PreparedEvent::ErrorSkip {
262 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
263 }
264 }
265 }
266}
267
268pub async fn commit_prepared_event(
282 prepared: PreparedEvent,
283 is_new: bool,
284 handler: &dyn InboundEventHandler,
285) -> bool {
286 let session = crate::state::SessionGuard::capture();
287 if !session.is_valid() {
288 return false;
289 }
290 match prepared {
291 PreparedEvent::Processed { result, contact, sender, is_mine, wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at, .. } => {
292 {
294 let mut cache = WRAPPER_ID_CACHE.lock().await;
295 cache.insert(wrapper_event_id_bytes);
296 }
297 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
299
300 if !is_mine {
302 let state = crate::state::STATE.lock().await;
303 if state.get_profile(&contact).map_or(false, |p| p.flags.is_blocked()) {
304 return false;
305 }
306 }
307
308 match result {
309 RumorProcessingResult::TextMessage(mut msg) => {
310 msg.wrapper_event_id = Some(wrapper_event_id.clone());
311 commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, handler, false).await
312 }
313 RumorProcessingResult::FileAttachment(mut msg) => {
314 msg.wrapper_event_id = Some(wrapper_event_id.clone());
315 if !is_mine {
330 for att in &mut msg.attachments {
331 if att.size == 0
332 && (att.url.starts_with("https://") || att.url.starts_with("http://"))
333 {
334 if let Ok(Some(size)) = tokio::time::timeout(
335 std::time::Duration::from_secs(3),
336 crate::net::get_remote_file_size(&att.url),
337 ).await {
338 att.size = size;
339 }
340 }
341 }
342 }
343 commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, handler, true).await
344 }
345 RumorProcessingResult::Reaction(reaction) => {
346 commit_reaction(reaction, &contact, is_mine, &wrapper_event_id, handler).await
347 }
348 RumorProcessingResult::Edit { message_id, new_content, edited_at, emoji_tags, mut event } => {
349 commit_edit(&mut event, &contact, &message_id, &new_content, edited_at, emoji_tags, &wrapper_event_id).await
350 }
351 RumorProcessingResult::TypingIndicator { profile_id, until } => {
352 let active_typers = {
353 let mut state = crate::state::STATE.lock().await;
354 state.update_typing_and_get_active(&contact, &profile_id, until)
355 };
356 crate::traits::emit_event("typing-update", &serde_json::json!({
357 "conversation_id": contact,
358 "typers": active_typers,
359 }));
360 false
361 }
362 RumorProcessingResult::PivxPayment { gift_code, amount_piv, address, message_id, mut event } => {
363 if crate::db::events::event_exists(&event.id).unwrap_or(false) {
364 return false;
365 }
366 event.wrapper_event_id = Some(wrapper_event_id.clone());
367 let ts = event.created_at;
368 let _ = crate::db::events::save_pivx_payment_event(&contact, event).await;
369 crate::traits::emit_event("pivx_payment_received", &serde_json::json!({
370 "conversation_id": contact,
371 "gift_code": gift_code, "amount_piv": amount_piv,
372 "address": address, "message_id": message_id,
373 "sender": sender.to_hex(), "is_mine": is_mine,
374 "at": ts * 1000,
375 }));
376 true
377 }
378 RumorProcessingResult::UnknownEvent(mut event) => {
379 event.wrapper_event_id = Some(wrapper_event_id.clone());
380 if let Ok(chat_id) = crate::db::id_cache::get_or_create_chat_id(&contact) {
382 event.chat_id = chat_id;
383 }
384 let _ = crate::db::events::save_event(&event).await;
385 false
386 }
387 RumorProcessingResult::LeaveRequest { .. } => false,
388 RumorProcessingResult::WebxdcPeerAdvertisement { .. } |
389 RumorProcessingResult::WebxdcPeerLeft { .. } => {
390 false
392 }
393 RumorProcessingResult::WallpaperChanged {
394 sender_npub, created_at, url, decryption_key, decryption_nonce,
395 plaintext_hash, mime, blur, dim, event_id,
396 } => {
397 let _ = crate::wallpaper::apply_received_wallpaper(
398 &contact, &sender_npub, created_at, &url,
399 &decryption_key, &decryption_nonce,
400 plaintext_hash.as_deref(), mime.as_deref(),
401 blur, dim,
402 &event_id,
403 ).await;
404 true
407 }
408 RumorProcessingResult::DeletionRequest { target_event_id } => {
409 if commit_deletion(&target_event_id, &contact, &sender, handler).await {
413 true
414 } else {
415 commit_reaction_deletion(&target_event_id, &sender).await
416 }
417 }
418 RumorProcessingResult::Ignored => false,
419 }
420 }
421 PreparedEvent::CommunityInvite { invite, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at } => {
422 {
425 let mut cache = WRAPPER_ID_CACHE.lock().await;
426 cache.insert(wrapper_event_id_bytes);
427 }
428 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
429
430 if is_mine {
432 return false;
433 }
434
435 if let Err(e) = invite.validate() {
438 log_warn!("[community] invite rejected: {}", e);
439 return false;
440 }
441
442 let community_id = invite.community_id.clone();
447 let already_held = crate::community::CommunityId(
450 match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
451 Some(b) => b,
452 None => { log_warn!("[community] invite has malformed id"); return false; }
453 },
454 );
455 if crate::db::community::community_exists(&already_held).unwrap_or(false) {
456 return false;
457 }
458 if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
459 return false;
460 }
461
462 if crate::community::list::tombstone_suppresses(&community_id, wrapper_created_at) {
467 return false;
468 }
469
470 let bundle_json = match invite.to_json() {
473 Ok(j) => j,
474 Err(e) => { log_warn!("[community] invite re-serialize failed: {}", e); return false; }
475 };
476 match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter) {
477 Ok(true) => {
478 handler.on_community_invite(&community_id);
479 let invite_warm = invite.clone();
483 let bg = crate::state::SessionGuard::capture();
484 tokio::spawn(async move {
485 if !bg.is_valid() {
486 return;
487 }
488 crate::community::service::preload_community(&invite_warm).await;
489 });
490 }
491 Ok(false) => {} Err(e) => log_warn!("[community] invite park failed: {}", e),
493 }
494 false
495 }
496 PreparedEvent::CommunityInviteV2 { bundle_json, community_id, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at } => {
497 {
498 let mut cache = WRAPPER_ID_CACHE.lock().await;
499 cache.insert(wrapper_event_id_bytes);
500 }
501 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
502
503 if is_mine {
505 return false;
506 }
507 let held = match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
510 Some(b) => crate::community::CommunityId(b),
511 None => return false,
512 };
513 if crate::db::community::community_exists(&held).unwrap_or(false) {
514 return false;
515 }
516 if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
517 return false;
518 }
519 if crate::community::list::tombstone_suppresses(&community_id, wrapper_created_at) {
523 return false;
524 }
525 match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter) {
527 Ok(true) => handler.on_community_invite(&community_id),
528 Ok(false) => {} Err(e) => log_warn!("[community] v2 invite park failed: {}", e),
530 }
531 false
532 }
533 PreparedEvent::DedupSkip { wrapper_id_bytes, wrapper_created_at } => {
534 if wrapper_created_at > 0 {
536 let _ = crate::db::wrappers::update_wrapper_timestamp(&wrapper_id_bytes, wrapper_created_at);
537 }
538 false
539 }
540 PreparedEvent::ErrorSkip { wrapper_id_bytes, wrapper_created_at } => {
541 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
542 false
543 }
544 }
545}
546
547async fn commit_dm_message(
549 mut msg: Message,
550 contact: &str,
551 _is_mine: bool,
552 is_new: bool,
553 wrapper_event_id: &str,
554 wrapper_event_id_bytes: [u8; 32],
555 handler: &dyn InboundEventHandler,
556 is_file: bool,
557) -> bool {
558 if let Ok(true) = crate::db::events::message_exists_in_db(&msg.id) {
560 if let Ok(updated) = crate::db::events::update_wrapper_event_id(&msg.id, wrapper_event_id) {
562 if !updated {
563 let mut cache = WRAPPER_ID_CACHE.lock().await;
564 cache.insert(wrapper_event_id_bytes);
565 }
566 }
567 return false;
568 }
569
570 if !msg.replied_to.is_empty() {
572 let _ = crate::db::events::populate_reply_context(&mut msg).await;
573 }
574
575 let added = {
577 let mut state = crate::state::STATE.lock().await;
578 let added = state.add_message_to_participant(contact, msg.clone());
579 if is_file && added {
580 state.update_typing_and_get_active(contact, contact, 0);
581 }
582 added
583 };
584
585 if added {
586 crate::traits::emit_event("message_new", &serde_json::json!({
588 "message": &msg,
589 "chat_id": contact
590 }));
591
592 if is_file {
594 handler.on_file_received(contact, &msg, is_new);
595 } else {
596 handler.on_dm_received(contact, &msg, is_new);
597 }
598
599 let _ = crate::db::events::save_message(contact, &msg).await;
601 }
602
603 added
604}
605
606async fn commit_reaction(
608 reaction: crate::types::Reaction,
609 contact: &str,
610 is_mine: bool,
611 wrapper_event_id: &str,
612 handler: &dyn InboundEventHandler,
613) -> bool {
614 let msg_for_emit = {
616 let mut state = crate::state::STATE.lock().await;
617 if let Some((chat_id, was_added)) = state.add_reaction_to_message(&reaction.reference_id, reaction.clone()) {
618 if was_added {
619 state.find_message(&reaction.reference_id)
620 .map(|(_, msg)| (chat_id, msg))
621 } else { None }
622 } else { None }
623 };
624
625 if let Some((chat_id, msg)) = msg_for_emit {
626 crate::traits::emit_event("message_update", &serde_json::json!({
627 "old_id": &reaction.reference_id,
628 "message": &msg,
629 "chat_id": &chat_id
630 }));
631 let _ = crate::db::events::save_message(&chat_id, &msg).await;
632 handler.on_reaction_received(&chat_id, &msg);
633 }
634
635 if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
637 let _ = crate::db::events::save_reaction_event(
638 &reaction, chat_id, None, is_mine, Some(wrapper_event_id.to_string())
639 ).await;
640 }
641
642 true
643}
644
645async fn commit_edit(
647 event: &mut crate::stored_event::StoredEvent,
648 contact: &str,
649 message_id: &str,
650 new_content: &str,
651 edited_at: u64,
652 emoji_tags: Vec<crate::types::EmojiTag>,
653 wrapper_event_id: &str,
654) -> bool {
655 if crate::db::events::event_exists(&event.id).unwrap_or(false) {
656 return false;
657 }
658 if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
659 event.chat_id = chat_id;
660 }
661 event.wrapper_event_id = Some(wrapper_event_id.to_string());
662 let _ = crate::db::events::save_event(event).await;
663
664 let msg_for_emit = {
665 let mut state = crate::state::STATE.lock().await;
666 state.update_message_in_chat(contact, message_id, |msg| {
667 msg.apply_edit(new_content.to_string(), edited_at, emoji_tags.clone());
668 })
669 };
670 if let Some(msg) = msg_for_emit {
671 crate::traits::emit_event("message_update", &serde_json::json!({
672 "old_id": message_id,
673 "message": msg,
674 "chat_id": contact
675 }));
676 }
677 true
678}
679
680async fn commit_deletion(
693 target_event_id: &str,
694 contact: &str,
695 sender: &PublicKey,
696 handler: &dyn InboundEventHandler,
697) -> bool {
698 let (mine, chat_id) = {
716 let state = crate::state::STATE.lock().await;
717 match state.find_message(target_event_id) {
718 Some((chat, msg)) => (msg.mine, chat.id.clone()),
719 None => return false,
720 }
721 };
722
723 let authorized = if mine {
734 match crate::state::my_public_key() {
735 Some(my_pk) => *sender == my_pk,
736 None => false,
737 }
738 } else {
739 match nostr_sdk::PublicKey::from_bech32(&chat_id) {
740 Ok(counterpart) => sender == &counterpart,
741 Err(_) => false, }
743 };
744 if !authorized {
745 eprintln!(
746 "[NIP-17 cooperative-delete] unauthorized: sender {} not the author of target {} (mine={}, chat={})",
747 sender.to_hex(), target_event_id, mine, chat_id
748 );
749 return false;
750 }
751
752 let removed = {
754 let mut state = crate::state::STATE.lock().await;
755 state.remove_message(target_event_id)
756 };
757 let removed_msg = match removed {
758 Some((_chat_id, msg)) => msg,
759 None => return false,
760 };
761
762 let unique = crate::deletion::filter_unreferenced_attachments(
772 target_event_id,
773 removed_msg.attachments,
774 ).await;
775 crate::deletion::delete_cached_attachment_files_pub(&unique);
776
777 if let Err(e) = crate::db::events::delete_event(target_event_id).await {
779 eprintln!(
780 "[NIP-17 cooperative-delete] DB delete failed for {}: {}",
781 target_event_id, e
782 );
783 }
784
785 crate::traits::emit_event(
788 "message_removed",
789 &serde_json::json!({
790 "id": target_event_id,
791 "chat_id": &chat_id,
792 "reason": "deleted-by-sender",
793 }),
794 );
795
796 handler.on_message_deleted(&chat_id, target_event_id);
797 let _ = contact;
798 true
799}
800
801async fn commit_reaction_deletion(target_reaction_id: &str, sender: &PublicKey) -> bool {
806 let found = {
807 let state = crate::state::STATE.lock().await;
808 state.find_reaction(target_reaction_id)
809 };
810 let (chat_id, message_id, author_npub, _is_community) = match found {
811 Some(v) => v,
812 None => return false,
813 };
814
815 let authorized = nostr_sdk::PublicKey::parse(&author_npub)
817 .map(|pk| pk == *sender)
818 .unwrap_or(false);
819 if !authorized {
820 eprintln!(
821 "[reaction-delete] unauthorized: sender {} is not the author of reaction {}",
822 sender.to_hex(), target_reaction_id
823 );
824 return false;
825 }
826
827 let updated = {
828 let mut state = crate::state::STATE.lock().await;
829 state.remove_reaction_from_message(&message_id, target_reaction_id)
830 };
831 let message = match updated {
832 Some((_cid, msg)) => msg,
833 None => return false,
834 };
835
836 if let Err(e) = crate::db::events::delete_event(target_reaction_id).await {
839 eprintln!("[reaction-delete] DB delete failed for {}: {}", target_reaction_id, e);
840 }
841
842 crate::traits::emit_event(
843 "message_update",
844 &serde_json::json!({
845 "old_id": &message_id,
846 "message": &message,
847 "chat_id": &chat_id,
848 }),
849 );
850 true
851}
852
853pub async fn process_event(
862 event: Event,
863 is_new: bool,
864 handler: &dyn InboundEventHandler,
865) -> std::result::Result<bool, String> {
866 let client = crate::state::nostr_client()
867 .ok_or_else(|| "Nostr client not initialized".to_string())?;
868 let my_pk = crate::state::my_public_key()
869 .ok_or_else(|| "Public key not initialized".to_string())?;
870 let prepared = prepare_event(event, &client, my_pk).await;
871 Ok(commit_prepared_event(prepared, is_new, handler).await)
872}