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 #[allow(clippy::too_many_arguments)]
82 fn on_webxdc_signal(
83 &self,
84 _contact: &str,
85 _npub: &str,
86 _topic_id: &str,
87 _node_addr: Option<&str>,
88 _event_id: &str,
89 _created_at: u64,
90 ) {}
91
92 fn on_community_self_removed(&self, _community_id: &str) {}
95
96 fn on_channel_keyed(&self, _community_id: &str, _channel_id: &str, _backfilled: usize) {}
108
109 fn on_subscription_ready(&self, _communities: usize) {}
118
119 fn on_community_refreshed(&self, _community_id: &str) {}
122
123 fn on_community_dissolved(&self, _community_id: &str) {}
126
127 fn buffer_persist(&self, _chat_id: &str, _msg: &Message, _wrapper: Option<([u8; 32], u64)>) -> bool { false }
134}
135
136pub struct NoOpEventHandler;
138impl InboundEventHandler for NoOpEventHandler {}
139
140pub struct LiveOnly<'a> {
146 inner: &'a dyn InboundEventHandler,
147 live_from_ms: Option<u64>,
148}
149
150impl<'a> LiveOnly<'a> {
151 pub fn signals(inner: &'a dyn InboundEventHandler) -> Self {
153 LiveOnly { inner, live_from_ms: None }
154 }
155
156 pub fn from(inner: &'a dyn InboundEventHandler, ms: u64) -> Self {
158 LiveOnly { inner, live_from_ms: Some(ms) }
159 }
160
161 fn is_live(&self, msg: &Message) -> bool {
162 self.live_from_ms.is_some_and(|from| msg.at >= from)
163 }
164}
165
166impl InboundEventHandler for LiveOnly<'_> {
167 fn on_community_webxdc(&self, chat_id: &str, npub: &str, topic_id: &str, node_addr: Option<&str>, event_id: &str, created_at: u64) {
168 self.inner.on_community_webxdc(chat_id, npub, topic_id, node_addr, event_id, created_at)
169 }
170
171 fn on_webxdc_signal(&self, contact: &str, npub: &str, topic_id: &str, node_addr: Option<&str>, event_id: &str, created_at: u64) {
172 self.inner.on_webxdc_signal(contact, npub, topic_id, node_addr, event_id, created_at)
173 }
174
175 fn on_dm_received(&self, chat_id: &str, msg: &Message, is_new: bool) {
176 if self.is_live(msg) {
177 self.inner.on_dm_received(chat_id, msg, is_new)
178 }
179 }
180
181 fn on_file_received(&self, chat_id: &str, msg: &Message, is_new: bool) {
182 if self.is_live(msg) {
183 self.inner.on_file_received(chat_id, msg, is_new)
184 }
185 }
186}
187
188struct BufferedDm {
192 chat_id: String,
193 msg: Message,
194 wrapper: Option<([u8; 32], u64)>,
195}
196
197pub struct BatchingPersist<'a> {
206 inner: &'a dyn InboundEventHandler,
207 buf: std::sync::Mutex<Vec<BufferedDm>>,
208 session: std::sync::Arc<crate::db::Session>,
215}
216
217impl<'a> BatchingPersist<'a> {
218 pub fn new(inner: &'a dyn InboundEventHandler) -> Self {
219 Self {
220 inner,
221 buf: std::sync::Mutex::new(Vec::new()),
222 session: crate::db::current_session(),
223 }
224 }
225
226 pub fn buffered(&self) -> usize {
228 self.buf.lock().map(|b| b.len()).unwrap_or(0)
229 }
230
231 pub async fn flush(&self) -> usize {
234 self.try_flush().await.unwrap_or(0)
235 }
236
237 pub async fn try_flush(&self) -> Result<usize, String> {
242 crate::db::with_session(self.session.clone(), async move {
243 let mut drained: Vec<BufferedDm> = match self.buf.lock() {
244 Ok(mut b) => b.drain(..).collect(),
245 Err(_) => return Ok(0),
246 };
247 if drained.is_empty() {
248 return Ok(0);
249 }
250 drained.retain(|e| !crate::state::was_message_deleted(&e.msg.id));
258 if drained.is_empty() {
259 return Ok(0);
260 }
261 let mut groups: Vec<crate::db::events::ChatBatch<'_>> = Vec::new();
263 for e in &drained {
264 match groups.iter_mut().find(|(c, _)| c == &e.chat_id) {
265 Some((_, v)) => v.push((&e.msg, e.wrapper)),
266 None => groups.push((e.chat_id.clone(), vec![(&e.msg, e.wrapper)])),
267 }
268 }
269 match crate::db::events::save_messages_batch_multi(&groups).await {
270 Ok(n) => Ok(n),
271 Err(e) => {
272 crate::log_warn!("[Sync] batched persist failed ({} msgs): {}", drained.len(), e);
273 Err(e)
274 }
275 }
276 })
277 .await
278 }
279}
280
281impl InboundEventHandler for BatchingPersist<'_> {
282 fn buffer_persist(&self, chat_id: &str, msg: &Message, wrapper: Option<([u8; 32], u64)>) -> bool {
283 match self.buf.lock() {
284 Ok(mut b) => {
285 b.push(BufferedDm { chat_id: chat_id.to_string(), msg: msg.clone(), wrapper });
286 true
287 }
288 Err(_) => false,
290 }
291 }
292
293 fn on_dm_received(&self, chat_id: &str, msg: &Message, is_new: bool) {
294 self.inner.on_dm_received(chat_id, msg, is_new)
295 }
296 fn on_file_received(&self, chat_id: &str, msg: &Message, is_new: bool) {
297 self.inner.on_file_received(chat_id, msg, is_new)
298 }
299 fn on_reaction_received(&self, chat_id: &str, msg: &Message) {
300 self.inner.on_reaction_received(chat_id, msg)
301 }
302 fn on_message_deleted(&self, chat_id: &str, message_id: &str) {
303 if let Ok(mut b) = self.buf.lock() {
307 b.retain(|e| e.msg.id != message_id);
308 }
309 self.inner.on_message_deleted(chat_id, message_id)
310 }
311 fn on_community_invite(&self, community_id: &str) {
312 self.inner.on_community_invite(community_id)
313 }
314 fn on_community_message(&self, chat_id: &str, msg: &Message, is_new: bool) {
315 self.inner.on_community_message(chat_id, msg, is_new)
316 }
317 fn on_community_update(&self, chat_id: &str, target_id: &str, msg: &Message) {
318 self.inner.on_community_update(chat_id, target_id, msg)
319 }
320 fn on_community_removed(&self, chat_id: &str, target_id: &str) {
321 self.inner.on_community_removed(chat_id, target_id)
322 }
323 fn on_community_presence(
324 &self,
325 chat_id: &str,
326 npub: &str,
327 joined: bool,
328 event_id: &str,
329 created_at: u64,
330 invited_by: Option<&str>,
331 invited_label: Option<&str>,
332 ) {
333 self.inner.on_community_presence(chat_id, npub, joined, event_id, created_at, invited_by, invited_label)
334 }
335 fn on_community_typing(&self, chat_id: &str, npub: &str, until: u64) {
336 self.inner.on_community_typing(chat_id, npub, until)
337 }
338 fn on_community_webxdc(
339 &self,
340 chat_id: &str,
341 npub: &str,
342 topic_id: &str,
343 node_addr: Option<&str>,
344 event_id: &str,
345 created_at: u64,
346 ) {
347 self.inner.on_community_webxdc(chat_id, npub, topic_id, node_addr, event_id, created_at)
348 }
349 fn on_webxdc_signal(
350 &self,
351 contact: &str,
352 npub: &str,
353 topic_id: &str,
354 node_addr: Option<&str>,
355 event_id: &str,
356 created_at: u64,
357 ) {
358 self.inner.on_webxdc_signal(contact, npub, topic_id, node_addr, event_id, created_at)
359 }
360 fn on_community_self_removed(&self, community_id: &str) {
361 self.inner.on_community_self_removed(community_id)
362 }
363 fn on_community_refreshed(&self, community_id: &str) {
364 self.inner.on_community_refreshed(community_id)
365 }
366 fn on_community_dissolved(&self, community_id: &str) {
367 self.inner.on_community_dissolved(community_id)
368 }
369}
370
371#[allow(clippy::large_enum_variant)] pub enum PreparedEvent {
374 Processed {
376 result: RumorProcessingResult,
377 contact: String,
378 sender: PublicKey,
379 is_mine: bool,
380 wrapper_event_id: String,
381 wrapper_event_id_bytes: [u8; 32],
382 wrapper_created_at: u64,
383 unwrap_ns: u64,
385 parse_ns: u64,
387 },
388 CommunityInvite {
390 invite: crate::community::invite::CommunityInvite,
391 inviter: String,
393 is_mine: bool,
394 wrapper_event_id_bytes: [u8; 32],
395 wrapper_created_at: u64,
396 rumor_created_at: u64,
400 expires_at: u64,
402 },
403 CommunityInviteV2 {
406 bundle_json: String,
407 community_id: String,
408 inviter: String,
410 is_mine: bool,
411 wrapper_event_id_bytes: [u8; 32],
412 wrapper_created_at: u64,
413 rumor_created_at: u64,
415 expires_at: u64,
417 },
418 DedupSkip {
420 wrapper_id_bytes: [u8; 32],
421 wrapper_created_at: u64,
422 },
423 ErrorSkip {
425 wrapper_id_bytes: [u8; 32],
426 wrapper_created_at: u64,
427 },
428}
429
430pub async fn prepare_event(
435 event: Event,
436 _client: &Client,
437 my_public_key: PublicKey,
438) -> PreparedEvent {
439 if let Some(skip) = dedup_skip(&event).await {
440 return skip;
441 }
442 let unwrap_start = web_time::Instant::now();
443 let unwrapped = crate::signer::unwrap_gift_wrap(&event).await;
444 finish_prepare(&event, unwrapped, unwrap_start.elapsed().as_nanos() as u64, my_public_key)
445}
446
447pub const UNWRAP_BATCH: usize = 16;
450
451pub async fn prepare_events(events: &[Event], my_public_key: PublicKey) -> Vec<PreparedEvent> {
454 let mut out: Vec<Option<PreparedEvent>> = Vec::with_capacity(events.len());
455 for event in events {
456 out.push(dedup_skip(event).await);
457 }
458 let todo: Vec<&Event> = events.iter().zip(&out).filter(|(_, o)| o.is_none()).map(|(e, _)| e).collect();
459 if !todo.is_empty() {
460 let unwrap_start = web_time::Instant::now();
461 let unwrapped = crate::signer::unwrap_gift_wraps(&todo).await;
462 let unwrap_ns = unwrap_start.elapsed().as_nanos() as u64 / todo.len() as u64;
463 let mut unwrapped = unwrapped.into_iter();
464 for (event, slot) in events.iter().zip(out.iter_mut()) {
465 if slot.is_none() {
466 let u = unwrapped.next().unwrap_or_else(|| Err("missing unwrap result".into()));
467 *slot = Some(finish_prepare(event, u, unwrap_ns, my_public_key));
468 }
469 }
470 }
471 out.into_iter().flatten().collect()
472}
473
474async fn dedup_skip(event: &Event) -> Option<PreparedEvent> {
476 let wrapper_created_at = event.created_at.as_secs();
477 let wrapper_event_id_bytes: [u8; 32] = event.id.to_bytes();
478 let skip = || Some(PreparedEvent::DedupSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at });
479
480 {
482 let cache = WRAPPER_ID_CACHE.lock().await;
483 if cache.contains(&wrapper_event_id_bytes) {
484 return skip();
485 }
486 }
487
488 if let Ok(true) = crate::db::events::wrapper_event_exists(&event.id.to_hex()) {
489 return skip();
490 }
491
492 if crate::db::wrappers::processed_wrapper_exists(&wrapper_event_id_bytes) {
496 return skip();
497 }
498 None
499}
500
501fn finish_prepare(
503 event: &Event,
504 unwrapped: Result<UnwrappedGift, String>,
505 unwrap_ns: u64,
506 my_public_key: PublicKey,
507) -> PreparedEvent {
508 let wrapper_created_at = event.created_at.as_secs();
509 let wrapper_event_id_bytes: [u8; 32] = event.id.to_bytes();
510 let wrapper_event_id = event.id.to_hex();
511
512 let (rumor, sender) = match unwrapped {
513 Ok(UnwrappedGift { rumor, sender }) => (rumor, sender),
514 Err(_) => return PreparedEvent::ErrorSkip {
515 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
516 },
517 };
518
519 let rumor_created_at = rumor.created_at.as_secs();
522
523 let is_mine = sender == my_public_key;
524 let contact = if is_mine {
525 rumor.tags.public_keys().next()
526 .and_then(|pk| pk.to_bech32().ok())
527 .unwrap_or_else(|| sender.to_bech32().unwrap_or_default())
528 } else {
529 sender.to_bech32().unwrap_or_default()
530 };
531
532 if rumor.tags.public_keys().count() > 1 {
534 return PreparedEvent::ErrorSkip {
535 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
536 };
537 }
538
539 if rumor.kind == Kind::Custom(crate::stored_event::event_kind::COMMUNITY_INVITE_BUNDLE) {
542 return match crate::community::invite::parse_invite_rumor(rumor.kind, &rumor.content) {
543 Some(invite) => PreparedEvent::CommunityInvite {
544 invite, inviter: contact.clone(), is_mine, wrapper_event_id_bytes, wrapper_created_at, rumor_created_at,
545 expires_at: crate::community::invite::expiration_secs(&rumor.tags).unwrap_or(0),
546 },
547 None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
548 };
549 }
550
551 if rumor.kind == Kind::Custom(crate::community::v2::kind::DIRECT_INVITE) {
554 return match crate::community::v2::invite::CommunityInvite::from_bundle_json(&rumor.content)
555 .ok()
556 .and_then(|b| serde_json::to_string(&b).ok().map(|j| (b.community_id, j)))
557 {
558 Some((community_id, bundle_json)) => PreparedEvent::CommunityInviteV2 {
559 community_id,
560 bundle_json,
561 inviter: contact.clone(), is_mine,
563 wrapper_event_id_bytes,
564 wrapper_created_at,
565 rumor_created_at,
566 expires_at: crate::community::invite::expiration_secs(&rumor.tags).unwrap_or(0),
567 },
568 None => PreparedEvent::ErrorSkip { wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at },
569 };
570 }
571
572 let Some(rumor_id) = rumor.id else {
574 return PreparedEvent::ErrorSkip {
575 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
576 };
577 };
578
579 let rumor_event = RumorEvent {
580 id: rumor_id,
581 kind: rumor.kind,
582 content: rumor.content,
583 tags: rumor.tags,
584 created_at: rumor.created_at,
585 pubkey: rumor.pubkey,
586 };
587 let rumor_context = RumorContext {
588 sender,
589 is_mine,
590 conversation_id: contact.clone(),
591 conversation_type: ConversationType::DirectMessage,
592 };
593
594 let parse_start = web_time::Instant::now();
595 let download_dir = crate::db::get_download_dir();
596 match process_rumor(rumor_event, rumor_context, &download_dir) {
597 Ok(result) => {
598 let parse_ns = parse_start.elapsed().as_nanos() as u64;
599 PreparedEvent::Processed {
600 result, contact, sender, is_mine,
601 wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at,
602 unwrap_ns, parse_ns,
603 }
604 }
605 Err(e) => {
606 log_warn!("[EventHandler] Failed to process rumor: {}", e);
607 PreparedEvent::ErrorSkip {
608 wrapper_id_bytes: wrapper_event_id_bytes, wrapper_created_at,
609 }
610 }
611 }
612}
613
614pub const DIRECT_INVITE_LIFETIME_SECS: u64 = 24 * 3600 + 3600;
635
636fn park_catch_up_channel_keys(
648 community_id: &str,
649 bundle_json: &str,
650 inviter: &str,
651) {
652 use crate::community::v2::invite::CommunityInvite;
653 let Ok(bundle) = CommunityInvite::from_bundle_json(bundle_json) else { return };
654 let (Some(cid), Some(owner), Some(salt)) = (
657 crate::simd::hex::hex_to_bytes_32_checked(&bundle.community_id),
658 crate::simd::hex::hex_to_bytes_32_checked(&bundle.owner),
659 crate::simd::hex::hex_to_bytes_32_checked(&bundle.owner_salt),
660 ) else {
661 return;
662 };
663 if !crate::community::v2::derive::verify_community_id(&crate::community::CommunityId(cid), &owner, &salt) {
664 log_warn!("[community] catch-up bundle failed self-certification — dropped");
665 return;
666 }
667 let Ok(sender) = PublicKey::parse(inviter) else { return };
668 let sender_hex = sender.to_hex();
669 let held = crate::db::community::load_community_v2(&crate::community::CommunityId(cid)).ok().flatten();
673 let mut parked_any = false;
674 for grant in &bundle.channels {
675 let Some(chan) = crate::simd::hex::hex_to_bytes_32_checked(&grant.id) else { continue };
676 let Some(key) = crate::simd::hex::hex_to_bytes_32_checked(&grant.key) else { continue };
677 if let Some(c) = held.as_ref().and_then(|h| h.channel(&crate::community::ChannelId(chan))) {
680 if !c.private || (c.key.is_some() && grant.epoch <= c.epoch.0) {
681 continue;
682 }
683 }
684 if let Err(e) = crate::db::community::park_channel_key(community_id, &grant.id, grant.epoch, &key, &sender_hex) {
685 log_warn!("[community] parking a vended channel key failed: {e}");
686 continue;
687 }
688 parked_any = true;
689 }
690 if parked_any {
694 crate::community::v2::realtime::enqueue_follow(&crate::community::CommunityId(cid));
695 }
696}
697
698fn expired_invite(expires_at: u64, rumor_created_at: u64) -> bool {
699 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
700 if expires_at != 0 && expires_at <= now {
701 return true;
702 }
703 rumor_created_at.saturating_add(DIRECT_INVITE_LIFETIME_SECS) <= now
704}
705
706#[cfg(test)]
707mod revoked_reaction_tests {
708 use super::*;
709
710 #[tokio::test]
711 async fn a_revoked_reaction_is_refused_under_any_wrapper() {
712 let id = "7".repeat(64);
713 let reaction = crate::types::Reaction {
714 id: id.clone(),
715 reference_id: "message".into(),
716 author_id: "author".into(),
717 emoji: "+".into(),
718 emoji_url: None,
719 };
720 crate::state::note_message_deleted(&id);
721 assert!(!commit_reaction(reaction, "contact", false, "any-wrapper", &NoOpEventHandler).await);
722 }
723}
724
725#[cfg(test)]
726mod invite_expiry_tests {
727 use super::*;
728
729 fn now() -> u64 {
730 nostr_sdk::prelude::Timestamp::now().as_secs()
731 }
732
733 #[test]
734 fn declared_deadline_is_honored() {
735 assert!(expired_invite(now() - 10, now()));
736 assert!(!expired_invite(now() + 3600, now()));
737 }
738
739 #[test]
740 fn tagless_invites_die_at_the_recipient_lifetime() {
741 assert!(!expired_invite(0, now() - 3600));
743 assert!(expired_invite(0, now() - DIRECT_INVITE_LIFETIME_SECS));
746 assert!(expired_invite(0, now() - 90 * 24 * 3600));
747 }
748
749 #[test]
750 fn lifetime_caps_a_generous_declared_deadline() {
751 assert!(expired_invite(now() + 7 * 24 * 3600, now() - DIRECT_INVITE_LIFETIME_SECS));
753 }
754}
755
756pub async fn commit_prepared_event(
757 prepared: PreparedEvent,
758 is_new: bool,
759 handler: &dyn InboundEventHandler,
760) -> bool {
761 let session = crate::db::current_session();
762 if !session.is_live() {
763 return false;
764 }
765 match prepared {
766 PreparedEvent::Processed { result, contact, sender, is_mine, wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at, .. } => {
767 {
769 let mut cache = WRAPPER_ID_CACHE.lock().await;
770 cache.insert(wrapper_event_id_bytes);
771 }
772
773 if !is_mine {
776 let blocked = {
777 let state = crate::state::STATE.lock().await;
778 state.get_profile(&contact).is_some_and(|p| p.flags.is_blocked())
779 };
780 if blocked {
781 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
782 return false;
783 }
784 }
785
786 if !matches!(result, RumorProcessingResult::TextMessage(_) | RumorProcessingResult::FileAttachment(_)) {
792 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
793 }
794
795 match result {
796 RumorProcessingResult::TextMessage(mut msg) => {
797 msg.wrapper_event_id = Some(wrapper_event_id.clone());
798 commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at, handler, false).await
799 }
800 RumorProcessingResult::FileAttachment(mut msg) => {
801 msg.wrapper_event_id = Some(wrapper_event_id.clone());
802 if !is_mine {
817 for att in &mut msg.attachments {
818 if att.size == 0
819 && (att.url.starts_with("https://") || att.url.starts_with("http://"))
820 {
821 if let Ok(Some(size)) = crate::rt::time::timeout(
822 std::time::Duration::from_secs(3),
823 crate::net::get_remote_file_size(&att.url),
824 ).await {
825 att.size = size;
826 }
827 }
828 }
829 }
830 crate::db::attachments::verify_message_attachments(&mut msg, None).await;
835 commit_dm_message(msg, &contact, is_mine, is_new, &wrapper_event_id, wrapper_event_id_bytes, wrapper_created_at, handler, true).await
836 }
837 RumorProcessingResult::Reaction(reaction) => {
838 commit_reaction(reaction, &contact, is_mine, &wrapper_event_id, handler).await
839 }
840 RumorProcessingResult::Edit { message_id, new_content, edited_at, emoji_tags, mut event } => {
841 commit_edit(&mut event, &contact, &message_id, &new_content, edited_at, emoji_tags, &wrapper_event_id).await
842 }
843 RumorProcessingResult::TypingIndicator { profile_id, until } => {
844 let active_typers = {
845 let mut state = crate::state::STATE.lock().await;
846 state.update_typing_and_get_active(&contact, &profile_id, until)
847 };
848 crate::traits::emit_event("typing-update", &serde_json::json!({
849 "conversation_id": contact,
850 "typers": active_typers,
851 }));
852 false
853 }
854 RumorProcessingResult::PivxPayment { gift_code, amount_piv, address, message_id, mut event } => {
855 if crate::db::events::event_exists(&event.id).unwrap_or(false) {
856 return false;
857 }
858 event.wrapper_event_id = Some(wrapper_event_id.clone());
859 let ts = event.created_at;
860 let _ = crate::db::events::save_pivx_payment_event(&contact, event).await;
861 crate::traits::emit_event("pivx_payment_received", &serde_json::json!({
862 "conversation_id": contact,
863 "gift_code": gift_code, "amount_piv": amount_piv,
864 "address": address, "message_id": message_id,
865 "sender": sender.to_hex(), "is_mine": is_mine,
866 "at": ts * 1000,
867 }));
868 true
869 }
870 RumorProcessingResult::UnknownEvent(mut event) => {
871 event.wrapper_event_id = Some(wrapper_event_id.clone());
872 if let Ok(chat_id) = crate::db::id_cache::get_or_create_chat_id(&contact) {
874 event.chat_id = chat_id;
875 }
876 let _ = crate::db::events::save_event(&event).await;
877 false
878 }
879 RumorProcessingResult::LeaveRequest { .. } => false,
880 RumorProcessingResult::WebxdcPeerAdvertisement { event_id, topic_id, node_addr, sender_npub, created_at } => {
881 handler.on_webxdc_signal(&contact, &sender_npub, &topic_id, Some(&node_addr), &event_id, created_at);
882 false
883 }
884 RumorProcessingResult::WebxdcPeerLeft { event_id, topic_id, sender_npub, created_at } => {
885 handler.on_webxdc_signal(&contact, &sender_npub, &topic_id, None, &event_id, created_at);
886 false
887 }
888 #[cfg(feature = "calls")]
889 RumorProcessingResult::CallSignal { call_id, signal, node_addr, sender_npub, created_at, video, media, .. } => {
890 if contact == sender_npub {
892 crate::calls::session::on_signal(&sender_npub, &call_id, &signal, node_addr.as_deref(), created_at, video.as_deref(), media.as_deref()).await;
893 }
894 false
895 }
896 #[cfg(not(feature = "calls"))]
897 RumorProcessingResult::CallSignal { .. } => false,
898 RumorProcessingResult::WallpaperChanged {
899 sender_npub, created_at, url, decryption_key, decryption_nonce,
900 plaintext_hash, mime, blur, dim, event_id,
901 } => {
902 let _ = crate::wallpaper::apply_received_wallpaper(
903 &contact, &sender_npub, created_at, &url,
904 &decryption_key, &decryption_nonce,
905 plaintext_hash.as_deref(), mime.as_deref(),
906 blur, dim,
907 &event_id,
908 ).await;
909 true
912 }
913 RumorProcessingResult::DeletionRequest { target_event_id } => {
914 if commit_deletion(&target_event_id, &contact, &sender, handler).await {
918 true
919 } else {
920 commit_reaction_deletion(&target_event_id, &sender).await
921 }
922 }
923 RumorProcessingResult::Ignored => false,
924 }
925 }
926 PreparedEvent::CommunityInvite { invite, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at, rumor_created_at, expires_at } => {
927 {
930 let mut cache = WRAPPER_ID_CACHE.lock().await;
931 cache.insert(wrapper_event_id_bytes);
932 }
933 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
934
935 if is_mine {
937 return false;
938 }
939
940 if expired_invite(expires_at, rumor_created_at) {
944 return false;
945 }
946
947 if let Err(e) = invite.validate() {
950 log_warn!("[community] invite rejected: {}", e);
951 return false;
952 }
953
954 let community_id = invite.community_id.clone();
959 let already_held = crate::community::CommunityId(
962 match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
963 Some(b) => b,
964 None => { log_warn!("[community] invite has malformed id"); return false; }
965 },
966 );
967 if crate::db::community::community_exists(&already_held).unwrap_or(false) {
968 return false;
969 }
970 if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
971 return false;
972 }
973
974 if crate::community::list::tombstone_suppresses(&community_id, rumor_created_at) {
980 return false;
981 }
982
983 let bundle_json = match invite.to_json() {
986 Ok(j) => j,
987 Err(e) => { log_warn!("[community] invite re-serialize failed: {}", e); return false; }
988 };
989 match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter, expires_at as i64) {
990 Ok(true) => {
991 handler.on_community_invite(&community_id);
992 let invite_warm = invite.clone();
996 let bg = crate::db::current_session();
997 crate::rt::spawn(async move {
998 if !bg.is_live() {
999 return;
1000 }
1001 crate::community::service::preload_community(&invite_warm).await;
1002 });
1003 }
1004 Ok(false) => {} Err(e) => log_warn!("[community] invite park failed: {}", e),
1006 }
1007 false
1008 }
1009 PreparedEvent::CommunityInviteV2 { bundle_json, community_id, inviter, is_mine, wrapper_event_id_bytes, wrapper_created_at, rumor_created_at, expires_at } => {
1010 {
1011 let mut cache = WRAPPER_ID_CACHE.lock().await;
1012 cache.insert(wrapper_event_id_bytes);
1013 }
1014 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
1015
1016 if is_mine {
1018 return false;
1019 }
1020 if expired_invite(expires_at, rumor_created_at) {
1022 return false;
1023 }
1024 let held = match crate::simd::hex::hex_to_bytes_32_checked(&community_id) {
1027 Some(b) => crate::community::CommunityId(b),
1028 None => return false,
1029 };
1030 if crate::db::community::community_exists(&held).unwrap_or(false) {
1036 park_catch_up_channel_keys(&community_id, &bundle_json, &inviter);
1037 return false;
1038 }
1039 if crate::db::community::pending_invite_exists(&community_id).unwrap_or(false) {
1040 return false;
1041 }
1042 if crate::community::list::tombstone_suppresses(&community_id, rumor_created_at) {
1047 return false;
1048 }
1049 match crate::db::community::save_pending_invite(&community_id, &bundle_json, &inviter, expires_at as i64) {
1051 Ok(true) => handler.on_community_invite(&community_id),
1052 Ok(false) => {} Err(e) => log_warn!("[community] v2 invite park failed: {}", e),
1054 }
1055 false
1056 }
1057 PreparedEvent::DedupSkip { wrapper_id_bytes, wrapper_created_at } => {
1058 if wrapper_created_at > 0 {
1065 if crate::db::wrappers::processed_wrapper_exists(&wrapper_id_bytes) {
1066 let _ = crate::db::wrappers::update_wrapper_timestamp(&wrapper_id_bytes, wrapper_created_at);
1067 } else {
1068 let wrapper_hex = crate::simd::hex::bytes_to_hex_32(&wrapper_id_bytes);
1069 if crate::db::events::wrapper_event_exists(&wrapper_hex).unwrap_or(false) {
1070 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
1071 }
1072 }
1073 }
1074 false
1075 }
1076 PreparedEvent::ErrorSkip { wrapper_id_bytes, wrapper_created_at } => {
1077 let _ = crate::db::wrappers::save_processed_wrapper(&wrapper_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17);
1078 false
1079 }
1080 }
1081}
1082
1083#[allow(clippy::too_many_arguments)]
1091async fn commit_dm_message(
1092 mut msg: Message,
1093 contact: &str,
1094 _is_mine: bool,
1095 is_new: bool,
1096 wrapper_event_id: &str,
1097 wrapper_event_id_bytes: [u8; 32],
1098 wrapper_created_at: u64,
1099 handler: &dyn InboundEventHandler,
1100 is_file: bool,
1101) -> bool {
1102 let ledger_wrapper = || {
1103 let _ = crate::db::wrappers::save_processed_wrapper(
1104 &wrapper_event_id_bytes, wrapper_created_at, crate::db::wrappers::TRANSPORT_NIP17,
1105 );
1106 };
1107 if crate::state::was_message_deleted(&msg.id) {
1111 ledger_wrapper();
1112 return false;
1113 }
1114
1115 if let Ok(true) = crate::db::events::message_exists_in_db(&msg.id) {
1117 if let Ok(updated) = crate::db::events::update_wrapper_event_id(&msg.id, wrapper_event_id) {
1119 if !updated {
1120 let mut cache = WRAPPER_ID_CACHE.lock().await;
1121 cache.insert(wrapper_event_id_bytes);
1122 }
1123 }
1124 ledger_wrapper();
1125 return false;
1126 }
1127
1128 if !msg.replied_to.is_empty() {
1130 let _ = crate::db::events::populate_reply_context(&mut msg).await;
1131 }
1132
1133 let added = {
1135 let mut state = crate::state::STATE.lock().await;
1136 let added = state.add_message_to_participant(contact, &msg);
1137 if is_file && added {
1138 state.update_typing_and_get_active(contact, contact, 0);
1139 }
1140 added
1141 };
1142
1143 let hidden_author = msg
1148 .npub
1149 .as_deref()
1150 .and_then(|author| {
1151 let community = crate::db::community::community_id_for_channel(contact).ok().flatten()?;
1152 nostr_sdk::prelude::PublicKey::parse(author)
1153 .ok()
1154 .map(|pk| crate::db::community::is_author_banned(&community, &pk))
1155 })
1156 .unwrap_or(false);
1157
1158 if added && !hidden_author {
1159 crate::traits::emit_message_new(contact, &msg);
1161
1162 if is_file {
1164 handler.on_file_received(contact, &msg, is_new);
1165 } else {
1166 handler.on_dm_received(contact, &msg, is_new);
1167 }
1168
1169 if !handler.buffer_persist(contact, &msg, Some((wrapper_event_id_bytes, wrapper_created_at)))
1174 && crate::db::events::save_message(contact, &msg).await.is_ok()
1175 {
1176 ledger_wrapper();
1177 }
1178 } else {
1179 ledger_wrapper();
1182 }
1183
1184 added
1185}
1186
1187async fn commit_reaction(
1189 reaction: crate::types::Reaction,
1190 contact: &str,
1191 is_mine: bool,
1192 wrapper_event_id: &str,
1193 handler: &dyn InboundEventHandler,
1194) -> bool {
1195 if crate::state::was_message_deleted(&reaction.id) {
1197 return false;
1198 }
1199
1200 let msg_for_emit = {
1202 let mut state = crate::state::STATE.lock().await;
1203 if let Some((chat_id, was_added)) = state.add_reaction_to_message(&reaction.reference_id, reaction.clone()) {
1204 if was_added {
1205 state.find_message(&reaction.reference_id)
1206 .map(|(_, msg)| (chat_id, msg))
1207 } else { None }
1208 } else { None }
1209 };
1210
1211 if let Some((chat_id, mut msg)) = msg_for_emit {
1212 crate::traits::emit_message_update(&chat_id, &reaction.reference_id, &mut msg).await;
1213 let _ = crate::db::events::save_message(&chat_id, &msg).await;
1214 handler.on_reaction_received(&chat_id, &msg);
1215 }
1216
1217 if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
1219 let _ = crate::db::events::save_reaction_event(
1220 &reaction, chat_id, None, is_mine, Some(wrapper_event_id.to_string())
1221 ).await;
1222 }
1223
1224 true
1225}
1226
1227async fn commit_edit(
1229 event: &mut crate::stored_event::StoredEvent,
1230 contact: &str,
1231 message_id: &str,
1232 new_content: &str,
1233 edited_at: u64,
1234 emoji_tags: Vec<crate::types::EmojiTag>,
1235 wrapper_event_id: &str,
1236) -> bool {
1237 if crate::db::events::event_exists(&event.id).unwrap_or(false) {
1238 return false;
1239 }
1240 if let Ok(chat_id) = crate::db::id_cache::get_chat_id_by_identifier(contact) {
1241 event.chat_id = chat_id;
1242 }
1243 event.wrapper_event_id = Some(wrapper_event_id.to_string());
1244 let _ = crate::db::events::save_event(event).await;
1245
1246 let msg_for_emit = {
1247 let mut state = crate::state::STATE.lock().await;
1248 state.update_message_in_chat(contact, message_id, |msg| {
1249 msg.apply_edit(new_content.to_string(), edited_at, emoji_tags.clone());
1250 })
1251 };
1252 if let Some(mut msg) = msg_for_emit {
1253 crate::traits::emit_message_update(contact, message_id, &mut msg).await;
1254 }
1255 true
1256}
1257
1258async fn commit_deletion(
1271 target_event_id: &str,
1272 contact: &str,
1273 sender: &PublicKey,
1274 handler: &dyn InboundEventHandler,
1275) -> bool {
1276 let (mine, chat_id) = {
1294 let state = crate::state::STATE.lock().await;
1295 match state.find_message(target_event_id) {
1296 Some((chat, msg)) => (msg.mine, chat.id.clone()),
1297 None => return false,
1298 }
1299 };
1300
1301 let authorized = if mine {
1312 match crate::state::my_public_key() {
1313 Some(my_pk) => *sender == my_pk,
1314 None => false,
1315 }
1316 } else {
1317 match nostr_sdk::prelude::PublicKey::from_bech32(&chat_id) {
1318 Ok(counterpart) => sender == &counterpart,
1319 Err(_) => false, }
1321 };
1322 if !authorized {
1323 eprintln!(
1324 "[NIP-17 cooperative-delete] unauthorized: sender {} not the author of target {} (mine={}, chat={})",
1325 sender.to_hex(), target_event_id, mine, chat_id
1326 );
1327 return false;
1328 }
1329
1330 let removed = {
1332 let mut state = crate::state::STATE.lock().await;
1333 state.remove_message(target_event_id)
1334 };
1335 let removed_msg = match removed {
1336 Some((_chat_id, msg)) => msg,
1337 None => return false,
1338 };
1339 crate::state::note_message_deleted(target_event_id);
1344 if let Err(e) = crate::db::events::add_message_tombstone(target_event_id) {
1345 crate::log_warn!("[NIP-17 cooperative-delete] tombstone write failed: {}", e);
1346 }
1347
1348 let unique = crate::deletion::filter_unreferenced_attachments(
1358 target_event_id,
1359 removed_msg.attachments,
1360 ).await;
1361 crate::deletion::delete_cached_attachment_files_pub(&unique);
1362
1363 if let Err(e) = crate::db::events::delete_event(target_event_id).await {
1365 eprintln!(
1366 "[NIP-17 cooperative-delete] DB delete failed for {}: {}",
1367 target_event_id, e
1368 );
1369 }
1370
1371 crate::traits::emit_event(
1374 "message_removed",
1375 &serde_json::json!({
1376 "id": target_event_id,
1377 "chat_id": &chat_id,
1378 "reason": "deleted-by-sender",
1379 }),
1380 );
1381
1382 handler.on_message_deleted(&chat_id, target_event_id);
1383 let _ = contact;
1384 true
1385}
1386
1387async fn commit_reaction_deletion(target_reaction_id: &str, sender: &PublicKey) -> bool {
1392 let found = {
1393 let state = crate::state::STATE.lock().await;
1394 state.find_reaction(target_reaction_id)
1395 };
1396 let (chat_id, message_id, author_npub, _is_community) = match found {
1397 Some(v) => v,
1398 None => return false,
1399 };
1400
1401 let authorized = nostr_sdk::prelude::PublicKey::parse(&author_npub)
1403 .map(|pk| pk == *sender)
1404 .unwrap_or(false);
1405 if !authorized {
1406 eprintln!(
1407 "[reaction-delete] unauthorized: sender {} is not the author of reaction {}",
1408 sender.to_hex(), target_reaction_id
1409 );
1410 return false;
1411 }
1412
1413 crate::state::note_message_deleted(target_reaction_id);
1414 if let Err(e) = crate::db::events::add_message_tombstone(target_reaction_id) {
1415 crate::log_warn!("[reaction-delete] tombstone write failed: {}", e);
1416 }
1417
1418 let updated = {
1419 let mut state = crate::state::STATE.lock().await;
1420 state.remove_reaction_from_message(&message_id, target_reaction_id)
1421 };
1422 let mut message = match updated {
1423 Some((_cid, msg)) => msg,
1424 None => return false,
1425 };
1426
1427 if let Err(e) = crate::db::events::delete_event(target_reaction_id).await {
1430 eprintln!("[reaction-delete] DB delete failed for {}: {}", target_reaction_id, e);
1431 }
1432
1433 crate::traits::emit_message_update(&chat_id, &message_id, &mut message).await;
1434 true
1435}
1436
1437pub async fn process_event(
1446 event: Event,
1447 is_new: bool,
1448 handler: &dyn InboundEventHandler,
1449) -> std::result::Result<bool, String> {
1450 let client = crate::state::nostr_client()
1451 .ok_or_else(|| "Nostr client not initialized".to_string())?;
1452 let my_pk = crate::state::my_public_key()
1453 .ok_or_else(|| "Public key not initialized".to_string())?;
1454 let prepared = prepare_event(event, &client, my_pk).await;
1455 Ok(commit_prepared_event(prepared, is_new, handler).await)
1456}
1457
1458#[cfg(test)]
1459mod signals_only_tests {
1460 use super::*;
1461 use std::sync::atomic::{AtomicUsize, Ordering};
1462
1463 #[derive(Default)]
1464 struct Counts {
1465 signals: AtomicUsize,
1466 messages: AtomicUsize,
1467 }
1468
1469 impl InboundEventHandler for Counts {
1470 fn on_webxdc_signal(&self, _: &str, _: &str, _: &str, _: Option<&str>, _: &str, _: u64) {
1471 self.signals.fetch_add(1, Ordering::Relaxed);
1472 }
1473 fn on_community_webxdc(&self, _: &str, _: &str, _: &str, _: Option<&str>, _: &str, _: u64) {
1474 self.signals.fetch_add(1, Ordering::Relaxed);
1475 }
1476 fn on_dm_received(&self, _: &str, _: &Message, _: bool) {
1477 self.messages.fetch_add(1, Ordering::Relaxed);
1478 }
1479 fn on_file_received(&self, _: &str, _: &Message, _: bool) {
1480 self.messages.fetch_add(1, Ordering::Relaxed);
1481 }
1482 }
1483
1484 #[test]
1485 fn a_catch_up_sync_passes_on_signals_and_nothing_else() {
1486 let inner = Counts::default();
1487 let sync = LiveOnly::signals(&inner);
1488 sync.on_webxdc_signal("npub1a", "npub1a", "T", Some("addr"), "e1", 1);
1489 sync.on_community_webxdc("chan", "npub1a", "T", None, "e2", 1);
1490 sync.on_dm_received("npub1a", &Message { at: u64::MAX, ..Default::default() }, true);
1491 assert_eq!(inner.signals.load(Ordering::Relaxed), 2);
1492 assert_eq!(inner.messages.load(Ordering::Relaxed), 0, "messages from a sync are history");
1493 }
1494
1495 #[test]
1496 fn a_startup_sync_passes_on_messages_sent_since_listening_began() {
1497 let inner = Counts::default();
1498 let sync = LiveOnly::from(&inner, 5_000);
1499 sync.on_dm_received("npub1a", &Message { at: 4_999, ..Default::default() }, true);
1500 assert_eq!(inner.messages.load(Ordering::Relaxed), 0, "before: history");
1501 sync.on_dm_received("npub1a", &Message { at: 5_000, ..Default::default() }, true);
1502 sync.on_file_received("npub1a", &Message { at: 6_000, ..Default::default() }, true);
1503 assert_eq!(inner.messages.load(Ordering::Relaxed), 2, "since: live");
1504 }
1505}