1#[macro_use]
29mod macros;
30
31pub mod error;
33pub mod traits;
34
35use nostr_sdk::prelude::ToBech32;
37
38pub mod types;
40pub mod profile;
41pub mod chat;
42pub mod compact;
43
44pub mod state;
46
47#[cfg(debug_assertions)]
49pub mod stats;
50
51pub mod crypto;
53
54pub mod signer;
56
57pub mod db;
59
60pub mod net;
62pub mod negentropy;
63pub mod blossom;
64pub mod blossom_servers;
65pub mod blossom_capabilities;
66pub mod inbox_relays;
67pub mod emoji_packs;
68pub mod emoji_usage;
69pub mod badges;
70pub mod bot_interface;
71pub mod webxdc;
72#[cfg(feature = "tor")]
73pub mod tor;
74
75pub fn nostr_client_options() -> nostr_sdk::ClientOptions {
87 let opts = nostr_sdk::ClientOptions::new().automatic_authentication(true);
94 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
95 {
96 match tor::transport_state() {
97 tor::TorTransportState::Active(addr) => {
98 return opts.connection(nostr_sdk::client::Connection::new().proxy(addr));
99 }
100 tor::TorTransportState::RequiredButInactive => {
101 return opts.connection(
104 nostr_sdk::client::Connection::new().proxy(tor::blackhole_proxy_addr()),
105 );
106 }
107 tor::TorTransportState::Disabled => {}
108 }
109 }
110 opts
111}
112
113pub fn tor_aware_relay_options(opts: nostr_sdk::RelayOptions) -> nostr_sdk::RelayOptions {
123 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
124 {
125 match tor::transport_state() {
126 tor::TorTransportState::Active(addr) => {
127 return opts.connection_mode(nostr_sdk::pool::ConnectionMode::proxy(addr));
128 }
129 tor::TorTransportState::RequiredButInactive => {
130 return opts.connection_mode(
133 nostr_sdk::pool::ConnectionMode::proxy(tor::blackhole_proxy_addr()),
134 );
135 }
136 tor::TorTransportState::Disabled => {}
137 }
138 }
139 opts
140}
141
142pub fn community_relay_options() -> nostr_sdk::RelayOptions {
152 use nostr_sdk::RelayServiceFlags;
153 tor_aware_relay_options(
154 nostr_sdk::RelayOptions::new().flags(RelayServiceFlags::GOSSIP | RelayServiceFlags::PING),
155 )
156}
157
158pub fn discovery_relay_options() -> nostr_sdk::RelayOptions {
164 community_relay_options()
165}
166
167pub mod stored_event;
169
170pub mod rumor;
172
173pub mod sending;
175
176pub mod wallpaper;
178
179pub mod deletion;
181pub mod self_destruct;
182
183pub mod simd;
185
186pub mod community;
188
189pub mod event_handler;
191
192pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
194pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
195pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
196pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
197pub use state::{
198 ChatState, NOSTR_CLIENT, MY_SECRET_KEY, MY_PUBLIC_KEY, STATE, ENCRYPTION_KEY,
199 nostr_client, my_public_key, has_active_session,
200 set_nostr_client, set_my_public_key,
201 take_nostr_client, clear_my_public_key,
202 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
203};
204pub use crypto::{GuardedKey, GuardedSigner};
205pub use signer::{
206 SignerKind, signer_kind, set_signer_kind, is_bunker,
207 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
208 build_bunker_signer, prewarm_bunker, drain_bunker_state,
209 parse_bunker_remote_pubkey, parse_bunker_relays,
210 BunkerConnectionState, bunker_state, set_bunker_state,
211 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
212 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
213 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
214};
215pub use error::{VectorError, Result};
216pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
217pub use db::{set_app_data_dir, get_app_data_dir};
218pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
219pub use deletion::{delete_own_dm, DeleteOutcome};
220pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
221pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
222pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
223pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
224
225use std::path::PathBuf;
226use std::sync::Arc;
227
228pub struct CoreConfig {
234 pub data_dir: PathBuf,
236 pub event_emitter: Option<Box<dyn EventEmitter>>,
238}
239
240#[derive(Clone, Copy)]
262pub struct VectorCore;
263
264impl VectorCore {
265 pub fn init(config: CoreConfig) -> Result<Self> {
267 db::set_app_data_dir(config.data_dir);
269
270 if let Some(emitter) = config.event_emitter {
272 traits::set_event_emitter(emitter);
273 }
274
275 let _ = rustls::crypto::ring::default_provider().install_default();
277
278 Ok(VectorCore)
279 }
280
281 pub fn accounts(&self) -> Result<Vec<String>> {
283 db::get_accounts().map_err(VectorError::from)
284 }
285
286 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
288 use nostr_sdk::prelude::*;
289
290 let keys = if key.starts_with("nsec1") {
292 let secret = SecretKey::from_bech32(key)
293 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
294 Keys::new(secret)
295 } else {
296 Keys::from_mnemonic(key, None)
298 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
299 };
300
301 let public_key = keys.public_key();
302 let npub = public_key.to_bech32()
303 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
304
305 let secret_bytes = keys.secret_key().to_secret_bytes();
307 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
308 state::set_my_public_key(public_key);
309
310 db::set_current_account(npub.clone())?;
312 db::init_database(&npub)?;
313
314 {
316 let nsec = keys.secret_key().to_bech32()
317 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
318 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
319
320 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
327 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
328 db::set_pkey(&nsec)?;
329 }
330 }
331
332 let has_encryption = state::resolve_encryption_enabled_from_db();
335
336 if has_encryption {
337 if let Some(pwd) = password {
338 let key = crate::crypto::hash_pass(pwd).await;
339 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
340 }
341 }
342 state::init_encryption_enabled();
345
346 let client = ClientBuilder::new()
349 .signer(keys)
350 .opts(nostr_client_options())
351 .monitor(Monitor::new(1024))
353 .build();
354
355 for relay in state::TRUSTED_RELAYS {
357 let opts = tor_aware_relay_options(nostr_sdk::RelayOptions::default());
358 client.pool().add_relay(*relay, opts).await.ok();
359 }
360
361 client.connect().await;
363
364 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
365
366 Ok(LoginResult { npub, has_encryption })
367 }
368
369 pub fn generate_nsec(&self) -> Result<String> {
372 use nostr_sdk::prelude::*;
373 Keys::generate().secret_key().to_bech32()
374 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
375 }
376
377 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
382 let config = SendConfig { self_send: false, ..SendConfig::headless() };
383 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
384 .map_err(|e| VectorError::Other(e))
385 }
386
387 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
389 let config = SendConfig { self_send: false, ..SendConfig::headless() };
390 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
391 .map_err(|e| VectorError::Other(e))
392 }
393
394 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
398 use futures_util::StreamExt;
399 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
400 if attachment.url.is_empty() {
401 return Err(VectorError::Other("attachment has no URL".into()));
402 }
403 crate::net::validate_url_not_private(&attachment.url)
407 .map_err(|e| VectorError::Other(e.to_string()))?;
408 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
409 let resp = client.get(&attachment.url).send().await
410 .map_err(|e| VectorError::Other(format!("download: {e}")))?;
411 if !resp.status().is_success() {
412 return Err(VectorError::Other(format!("download failed: HTTP {}", resp.status())));
413 }
414 let mut encrypted: Vec<u8> = Vec::with_capacity(
416 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
417 );
418 let mut stream = resp.bytes_stream();
419 while let Some(chunk) = stream.next().await {
420 let chunk = chunk.map_err(|e| VectorError::Other(format!("read body: {e}")))?;
421 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
422 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
423 }
424 encrypted.extend_from_slice(&chunk);
425 }
426 crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce).map_err(VectorError::Other)
427 }
428
429 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
431 let path = std::path::Path::new(file_path);
432 let bytes = std::fs::read(path)
433 .map_err(|e| VectorError::Io(e))?;
434 let filename = path.file_name()
435 .and_then(|n| n.to_str())
436 .unwrap_or("file");
437 let extension = path.extension()
438 .and_then(|e| e.to_str())
439 .unwrap_or("bin");
440
441 sending::send_file_dm(
442 to_npub,
443 std::sync::Arc::new(bytes),
444 filename,
445 extension,
446 None,
447 &SendConfig::default(),
448 Arc::new(NoOpSendCallback),
449 ).await.map_err(|e| VectorError::Other(e))
450 }
451
452 pub async fn send_reaction(
457 &self,
458 to_npub: &str,
459 reference_id: &str,
460 emoji: &str,
461 emoji_url: Option<&str>,
462 ) -> Result<String> {
463 use nostr_sdk::prelude::*;
464
465 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
466 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
467
468 let reference_event = EventId::from_hex(reference_id)
469 .map_err(|e| VectorError::Nostr(e.to_string()))?;
470 let receiver_pubkey = PublicKey::from_bech32(to_npub)
471 .map_err(|e| VectorError::Nostr(e.to_string()))?;
472
473 let custom_emoji_tag = emoji_url.and_then(|url| {
475 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
476 return None;
477 }
478 let shortcode = &emoji[1..emoji.len() - 1];
479 if shortcode.is_empty() { return None; }
480 Some(Tag::custom(TagKind::custom("emoji"), [shortcode.to_string(), url.to_string()]))
481 });
482
483 let reaction_target = nostr_sdk::nips::nip25::ReactionTarget {
484 event_id: reference_event,
485 public_key: receiver_pubkey,
486 coordinate: None,
487 kind: Some(Kind::PrivateDirectMessage),
488 relay_hint: None,
489 };
490 let mut builder = EventBuilder::reaction(reaction_target, emoji);
491 if let Some(tag) = custom_emoji_tag {
492 builder = builder.tag(tag);
493 }
494 let rumor = builder.build(my_public_key);
495 let inner_rumor_id = rumor.id;
496 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
497
498 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
502 .await.map_err(VectorError::Other)?;
503 if !outcome.output.success.is_empty() {
504 if let Some(rid) = inner_rumor_id {
505 if let Err(e) = db::nip17_keys::store_wrap_key(
506 &outcome.wrap_event_id, &rid, &receiver_pubkey,
507 db::nip17_keys::WrapRole::Recipient,
508 &outcome.wrap_secret, &outcome.targeted_relays,
509 ) {
510 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
511 }
512 }
513 }
514
515 let self_wrap_client = client.clone();
518 let self_wrap_session = state::SessionGuard::capture();
519 tokio::spawn(async move {
520 if !self_wrap_session.is_valid() { return; }
521 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
522 &self_wrap_client, &my_public_key, rumor, [],
523 ).await {
524 if !self_wrap_session.is_valid() { return; }
525 if !self_outcome.output.success.is_empty() {
526 if let Some(rid) = inner_rumor_id {
527 let _ = db::nip17_keys::store_wrap_key(
528 &self_outcome.wrap_event_id, &rid, &my_public_key,
529 db::nip17_keys::WrapRole::SelfSend,
530 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
531 );
532 }
533 }
534 }
535 });
536
537 let reaction = Reaction {
539 id: rumor_id.clone(),
540 reference_id: reference_id.to_string(),
541 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
542 emoji: emoji.to_string(),
543 emoji_url: emoji_url.map(|s| s.to_string()),
544 };
545 let msg_for_save = {
546 let mut st = state::STATE.lock().await;
547 match st.add_reaction_to_message(reference_id, reaction) {
548 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
549 _ => None,
550 }
551 };
552 if let Some((cid, msg)) = msg_for_save {
553 let _ = db::events::save_message(&cid, &msg).await;
554 traits::emit_event_json("message_update", serde_json::json!({
555 "old_id": reference_id, "message": &msg, "chat_id": &cid
556 }));
557 }
558
559 Ok(rumor_id)
560 }
561
562 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
565 use nostr_sdk::prelude::*;
566
567 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
568 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
569 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
570
571 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
572 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
573 .tag(Tag::public_key(pubkey))
574 .tag(Tag::custom(TagKind::d(), vec!["vector"]))
575 .tag(Tag::expiration(expiry))
576 .build(my_public_key);
577
578 client.gift_wrap_to(
579 state::active_trusted_relays().await,
580 &pubkey,
581 rumor,
582 [Tag::expiration(expiry)],
583 ).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
584 Ok(())
585 }
586
587 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
591 use nostr_sdk::prelude::*;
592
593 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
594 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
595 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
596 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
597 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
598
599 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
601
602 let mut builder = EventBuilder::new(
603 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
604 new_content,
605 ).tag(Tag::event(reference_event));
606 for et in &emoji_tags {
607 builder = builder.tag(Tag::custom(
608 TagKind::custom("emoji"),
609 [et.shortcode.clone(), et.url.clone()],
610 ));
611 }
612 let rumor = builder.build(my_public_key);
613 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
614 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
615
616 let msg_for_emit = {
618 let mut st = state::STATE.lock().await;
619 st.update_message_in_chat(to_npub, message_id, |msg| {
620 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
621 msg.preview_metadata = None;
622 })
623 };
624 if let Some(msg) = msg_for_emit {
625 traits::emit_event_json("message_update", serde_json::json!({
626 "old_id": message_id, "message": &msg, "chat_id": to_npub
627 }));
628 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
629 let _ = db::events::save_edit_event(
630 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
631 ).await;
632 }
633 }
634
635 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
636 .await.map_err(VectorError::Other)?;
637
638 let self_wrap_client = client.clone();
639 let self_wrap_session = state::SessionGuard::capture();
640 tokio::spawn(async move {
641 if !self_wrap_session.is_valid() { return; }
642 let _ = self_wrap_client.gift_wrap(&my_public_key, rumor, []).await;
643 });
644
645 Ok(edit_id)
646 }
647
648 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
650 use nostr_sdk::prelude::*;
651 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
652 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
653 }
654
655 pub async fn get_chats(&self) -> Vec<SerializableChat> {
657 let state = state::STATE.lock().await;
658 state.chats.iter()
659 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
660 .collect()
661 }
662
663 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
665 let state = state::STATE.lock().await;
666 if let Some(chat) = state.get_chat(chat_id) {
667 let msgs = chat.get_all_messages(&state.interner);
668 let start = offset.min(msgs.len());
669 let end = (offset + limit).min(msgs.len());
670 msgs[start..end].to_vec()
671 } else {
672 Vec::new()
673 }
674 }
675
676 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
678 let state = state::STATE.lock().await;
679 state.get_profile(npub)
680 .map(|p| SlimProfile::from_profile(p, &state.interner))
681 }
682
683 pub async fn load_profile(&self, npub: &str) -> bool {
685 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
686 }
687
688 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
690 profile::sync::update_profile(
691 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
692 &NoOpProfileSyncHandler,
693 ).await
694 }
695
696 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
699 profile::sync::update_bot_profile(
700 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
701 &NoOpProfileSyncHandler,
702 ).await
703 }
704
705 pub async fn update_status(&self, status: &str) -> bool {
707 profile::sync::update_status(status.to_string()).await
708 }
709
710 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
716 let path = std::path::Path::new(file_path);
717 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
718 if bytes.is_empty() {
719 return Err(VectorError::Other("Empty image file".into()));
720 }
721 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
722 let mime = crate::crypto::mime_from_extension(&extension);
723 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
724 let signer = client
725 .signer()
726 .await
727 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
728 let servers = crate::blossom_servers::compute_enabled_servers();
729 if servers.is_empty() {
730 return Err(VectorError::Other("No Blossom servers configured".into()));
731 }
732 crate::blossom::upload_blob_with_failover(signer, servers, std::sync::Arc::new(bytes), Some(mime))
733 .await
734 .map_err(VectorError::Other)
735 }
736
737 pub async fn block_user(&self, npub: &str) -> bool {
739 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
740 }
741
742 pub async fn unblock_user(&self, npub: &str) -> bool {
744 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
745 }
746
747 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
749 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
750 }
751
752 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
754 profile::sync::get_blocked_users().await
755 }
756
757 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
759 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
760 }
761
762 pub fn my_npub(&self) -> Option<String> {
764 state::my_public_key()
765 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
766 }
767
768 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
775 use crate::community::ConcordProtocol;
776 let ids = crate::db::community::list_community_ids().unwrap_or_default();
777 let mut out = Vec::new();
778 for id in ids {
779 match crate::db::community::community_protocol(&id).ok().flatten() {
781 Some(ConcordProtocol::V2) => {
782 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
783 let me = state::my_public_key();
784 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
785 out.push(serde_json::json!({
786 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
787 "version": 2,
788 "name": c.name,
789 "description": c.description,
790 "is_owner": is_owner,
791 "channels": c.channels.iter()
792 .map(|ch| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0), "name": ch.name, "private": ch.private }))
793 .collect::<Vec<_>>(),
794 }));
795 }
796 }
797 _ => {
798 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
799 out.push(serde_json::json!({
800 "community_id": c.id.to_hex(),
801 "version": 1,
802 "name": c.name,
803 "description": c.description,
804 "is_owner": crate::community::service::is_proven_owner(&c),
805 "channels": c.channels.iter()
806 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
807 .collect::<Vec<_>>(),
808 }));
809 }
810 }
811 }
812 }
813 out
814 }
815
816 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
821 use crate::community::{v2::service as v2, transport::LiveTransport};
822 let relays: Vec<String> = crate::state::active_trusted_relays()
823 .await
824 .iter()
825 .map(|s| s.to_string())
826 .collect();
827 if relays.is_empty() {
828 return Err(VectorError::Other("no relays available to host the Community".into()));
829 }
830 let session = state::SessionGuard::capture();
831 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
832 let community = v2::create_community(&transport, name, relays, None)
833 .await
834 .map_err(VectorError::Other)?;
835 self.register_v2_chats(&community, &session).await;
836 if let Some(client) = state::nostr_client() {
838 crate::community::v2::realtime::refresh_subscription(&client).await;
839 }
840 Ok(Self::v2_summary(&community))
841 }
842
843 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
849 use crate::community::ConcordProtocol;
850 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
851 return Ok(None);
852 };
853 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
854 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
855 Some(ConcordProtocol::V2) => Some(cid),
856 _ => None,
857 })
858 }
859
860 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
862 let me = state::my_public_key();
863 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
864 serde_json::json!({
865 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
866 "version": 2,
867 "name": community.name,
868 "description": community.description,
869 "is_owner": is_owner,
870 "channels": community.channels.iter()
871 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
872 .collect::<Vec<_>>(),
873 })
874 }
875
876 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
882 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
883 let me = state::my_public_key();
884 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
885 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
886 let Some(primary) = community.primary_channel() else { return };
889 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
890 let sibling_ids: Vec<String> = community
891 .channels
892 .iter()
893 .filter(|c| c.id.0 != primary.id.0)
894 .map(|c| crate::simd::hex::bytes_to_hex_32(&c.id.0))
895 .collect();
896 let slim = {
897 let mut st = state::STATE.lock().await;
898 if !session.is_valid() {
899 return; }
901 st.upsert_community_chat(
902 &primary_hex,
903 &community.name,
904 community.description.as_deref().unwrap_or(""),
905 &id_hex,
906 is_owner,
907 community.icon.is_some(),
908 owner_npub.as_deref(),
909 Some(community.created_at_ms),
910 community.dissolved,
911 crate::community::ConcordProtocol::V2,
912 );
913 st.chats.retain(|c| !sibling_ids.contains(&c.id));
916 st.chats
917 .iter()
918 .find(|c| c.id == primary_hex)
919 .map(|chat| crate::db::chats::SlimChatDB::from_chat(chat, &st.interner))
920 };
921 if !session.is_valid() {
925 return;
926 }
927 if let Some(slim) = slim {
928 let _ = crate::db::chats::save_slim_chat(&slim);
929 }
930 }
931
932 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
936 use crate::community::{public_invite, service, transport::LiveTransport};
937 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
941 let session = state::SessionGuard::capture();
942 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
943 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
944 .await
945 .map_err(VectorError::Other)?;
946 self.register_v2_chats(&community, &session).await;
947 if let Some(client) = state::nostr_client() {
948 crate::community::v2::realtime::refresh_subscription(&client).await;
949 }
950 if crate::community::v2::realtime::follow_worker_running() {
955 crate::community::v2::realtime::enqueue_follow(community.id());
956 } else {
957 let seed_session = state::SessionGuard::capture();
958 let seed_community = community.clone();
959 tokio::spawn(async move {
960 if !seed_session.is_valid() {
961 return;
962 }
963 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
964 if matches!(
965 crate::community::v2::service::sync_guestbook(&transport, &seed_community, &seed_session).await,
966 Ok(fresh) if !fresh.is_empty()
967 ) {
968 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
969 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
970 }
971 });
972 }
973 return Ok(Self::v2_summary(&community));
974 }
975 let (relays, token) = public_invite::parse_invite_url(invite_url)
976 .map_err(|e| VectorError::Other(e.to_string()))?;
977 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
978 let bundle = service::fetch_public_invite(&transport, &relays, &token)
979 .await
980 .map_err(VectorError::Other)?;
981 let now = std::time::SystemTime::now()
982 .duration_since(std::time::UNIX_EPOCH)
983 .map(|d| d.as_secs())
984 .unwrap_or(0);
985 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
986 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
989 self.finalize_member_join(community, &transport, attribution).await
990 }
991
992 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
995 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
996 Ok(rows.iter().map(|p| {
997 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1000 serde_json::json!({
1001 "community_id": p.community_id,
1002 "name": v2.name,
1003 "inviter_npub": p.inviter_npub,
1004 "version": 2,
1005 })
1006 } else {
1007 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1008 .ok().map(|i| i.name).unwrap_or_default();
1009 serde_json::json!({
1010 "community_id": p.community_id,
1011 "name": name,
1012 "inviter_npub": p.inviter_npub,
1013 "version": 1,
1014 })
1015 }
1016 }).collect())
1017 }
1018
1019 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1023 use crate::community::transport::LiveTransport;
1024 let bundle_json = crate::db::community::get_pending_invite(community_id)
1025 .map_err(VectorError::Other)?
1026 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1027 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1028
1029 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1031 let session = state::SessionGuard::capture();
1032 let inviter = crate::db::community::list_pending_invites()
1034 .ok()
1035 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1036 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
1044 .await
1045 .map_err(VectorError::Other)?;
1046 if !session.is_valid() {
1047 return Err(VectorError::Other("account changed during join".into()));
1048 }
1049 self.register_v2_chats(&community, &session).await;
1050 if let Some(client) = state::nostr_client() {
1051 crate::community::v2::realtime::refresh_subscription(&client).await;
1052 }
1053 crate::community::v2::realtime::enqueue_follow(community.id());
1054 let _ = crate::db::community::delete_pending_invite(community_id);
1055 return Ok(Self::v2_summary(&community));
1056 }
1057
1058 use crate::community::invite::{accept_invite, CommunityInvite};
1060 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1061 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1062 let summary = self.finalize_member_join(community, &transport, None).await?;
1064 let _ = crate::db::community::delete_pending_invite(community_id);
1065 Ok(summary)
1066 }
1067
1068 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1073 &self,
1074 community: crate::community::Community,
1075 transport: &T,
1076 attribution: Option<(String, Option<String>)>,
1077 ) -> Result<serde_json::Value> {
1078 use crate::community::service;
1079 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1083 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1086 if c.removed {
1087 let _ = crate::db::community::delete_community(&community.id.to_hex());
1088 return Err(VectorError::Other("you have been removed from this community".into()));
1089 }
1090 }
1091 let community = crate::db::community::load_community(&community.id)
1092 .map_err(VectorError::Other)?
1093 .unwrap_or(community);
1094 let _ = service::fetch_and_apply_control(transport, &community).await;
1098 if service::am_i_banned(&community) {
1099 let _ = crate::db::community::delete_community(&community.id.to_hex());
1100 return Err(VectorError::Other("you are banned from this community".into()));
1101 }
1102 let community = crate::db::community::load_community(&community.id)
1104 .map_err(VectorError::Other)?
1105 .unwrap_or(community);
1106 let owner_npub = community
1107 .owner_attestation
1108 .as_ref()
1109 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1110 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1111 {
1112 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1113 let mut st = state::STATE.lock().await;
1114 for ch in &community.channels {
1115 st.upsert_community_chat(
1116 &ch.id.to_hex(),
1117 &community.name,
1118 community.description.as_deref().unwrap_or(""),
1119 &community.id.to_hex(),
1120 crate::community::service::is_proven_owner(&community),
1121 community.icon.is_some(),
1122 owner_npub.as_deref(),
1123 created_at_ms,
1124 community.dissolved,
1125 crate::community::ConcordProtocol::V1,
1126 );
1127 }
1128 }
1129 if let Some(primary) = community.channels.first() {
1132 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1133 }
1134 Ok(serde_json::json!({
1135 "community_id": community.id.to_hex(),
1136 "version": 1,
1137 "name": community.name,
1138 "channels": community.channels.iter()
1139 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1140 .collect::<Vec<_>>(),
1141 }))
1142 }
1143
1144 pub async fn create_community(&self, name: &str) -> Result<serde_json::Value> {
1148 use crate::community::{service, transport::LiveTransport};
1149 let relays: Vec<String> = crate::state::active_trusted_relays()
1150 .await
1151 .iter()
1152 .map(|s| s.to_string())
1153 .collect();
1154 if relays.is_empty() {
1155 return Err(VectorError::Other("no relays available to host the Community".into()));
1156 }
1157 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1158 let community = service::create_community(&transport, name, "general", relays)
1159 .await
1160 .map_err(VectorError::Other)?;
1161 let owner_npub = community
1162 .owner_attestation
1163 .as_ref()
1164 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1165 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1166 {
1167 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1168 let mut st = state::STATE.lock().await;
1169 for ch in &community.channels {
1170 st.upsert_community_chat(
1171 &ch.id.to_hex(),
1172 &community.name,
1173 community.description.as_deref().unwrap_or(""),
1174 &community.id.to_hex(),
1175 crate::community::service::is_proven_owner(&community),
1176 community.icon.is_some(),
1177 owner_npub.as_deref(),
1178 created_at_ms,
1179 community.dissolved,
1180 crate::community::ConcordProtocol::V1,
1181 );
1182 }
1183 }
1184 Ok(serde_json::json!({
1185 "community_id": community.id.to_hex(),
1186 "version": 1,
1187 "name": community.name,
1188 "channels": community.channels.iter()
1189 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1190 .collect::<Vec<_>>(),
1191 }))
1192 }
1193
1194 pub async fn create_public_invite(&self, community_id: &str) -> Result<String> {
1196 use crate::community::{service, transport::LiveTransport, CommunityId};
1197 if community_id.len() != 64 {
1198 return Err(VectorError::Other("malformed community id".into()));
1199 }
1200 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1201 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1203 crate::db::community::community_protocol(&cid).ok()
1204 {
1205 let community = crate::db::community::load_community_v2(&cid)
1206 .map_err(VectorError::Other)?
1207 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1208 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1209 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1212 let minted = crate::community::v2::service::mint_public_link(&transport, &community, base, None, None)
1213 .await
1214 .map_err(VectorError::Other)?;
1215 return Ok(minted.url);
1216 }
1217 let community = crate::db::community::load_community(&CommunityId(
1218 crate::simd::hex::hex_to_bytes_32(community_id),
1219 ))
1220 .map_err(VectorError::Other)?
1221 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1222 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1223 let (_token, url) = service::create_public_invite(&transport, &community, None, None)
1224 .await
1225 .map_err(VectorError::Other)?;
1226 Ok(url)
1227 }
1228
1229 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1233 use crate::community::{service, CommunityId};
1234 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1235
1236 let session = crate::state::SessionGuard::capture();
1237 let my_pk = crate::state::my_public_key()
1238 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1239
1240 if community_id.len() != 64 {
1241 return Err(VectorError::Other("malformed community id".into()));
1242 }
1243 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1244 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1250 crate::db::community::community_protocol(&cid).ok()
1251 {
1252 let community = crate::db::community::load_community_v2(&cid)
1253 .map_err(VectorError::Other)?
1254 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1255 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1256 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1257 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1258 let bundle = crate::community::v2::service::bundle_of(&community, Some(my_pk), None, None);
1262 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1263 let rumor = nostr_sdk::EventBuilder::new(
1264 nostr_sdk::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1265 bundle_json,
1266 )
1267 .build(my_pk);
1268 let k_tag = nostr_sdk::Tag::custom(
1269 nostr_sdk::TagKind::Custom("k".into()),
1270 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1271 );
1272 if !session.is_valid() {
1273 return Err(VectorError::Other("account changed".into()));
1274 }
1275 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag])
1276 .await
1277 .map_err(VectorError::Other)?;
1278 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1279 }
1280 let community = crate::db::community::load_community(&CommunityId(
1281 crate::simd::hex::hex_to_bytes_32(community_id),
1282 ))
1283 .map_err(VectorError::Other)?
1284 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1285
1286 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1287 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1288 }
1289 let invitee_hex = nostr_sdk::PublicKey::parse(invitee_npub)
1290 .map_err(|_| VectorError::Other("invalid npub".into()))?
1291 .to_hex();
1292 if crate::db::community::get_community_banlist(community_id)
1293 .map_err(VectorError::Other)?
1294 .iter()
1295 .any(|b| b == &invitee_hex)
1296 {
1297 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1298 }
1299
1300 if !session.is_valid() {
1302 return Err(VectorError::Other("account changed during invite".into()));
1303 }
1304
1305 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk).map_err(VectorError::Other)?;
1306 let pending_id = format!("community-invite-{}", community_id);
1307 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1309 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1310
1311 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1312 .await
1313 .map_err(VectorError::Other)?;
1314
1315 Ok(serde_json::json!({
1316 "community_id": community_id,
1317 "invitee": invitee_npub,
1318 "wrap_event_id": result.event_id,
1319 }))
1320 }
1321
1322 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1327 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1328 }
1329
1330 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1335 use crate::community::{service, transport::LiveTransport, CommunityId};
1336 if community_id.len() != 64 {
1337 return Err(VectorError::Other("malformed community id".into()));
1338 }
1339 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1340 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1341 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1344 let community = crate::db::community::load_community_v2(&cid)
1345 .map_err(VectorError::Other)?
1346 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1347 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1348 .await
1349 .map_err(VectorError::Other);
1350 }
1351 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1352 let community = crate::db::community::load_community(&cid)
1353 .map_err(VectorError::Other)?
1354 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1355 service::revoke_public_invite(&transport, &community, &token_bytes)
1356 .await
1357 .map_err(VectorError::Other)
1358 }
1359
1360 pub async fn send_community_message(
1362 &self,
1363 channel_id: &str,
1364 content: &str,
1365 replied_to: Option<&str>,
1366 ) -> Result<String> {
1367 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1368 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1370 let community = crate::db::community::load_community_v2(&id)
1371 .map_err(VectorError::Other)?
1372 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1373 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1374 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1375 let reply = match replied_to.filter(|r| !r.is_empty()) {
1378 Some(parent_id) => {
1379 let author_hex = {
1380 let st = state::STATE.lock().await;
1381 st.find_message(parent_id)
1382 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1383 .map(|pk| pk.to_hex())
1384 .unwrap_or_default()
1385 };
1386 Some((parent_id.to_string(), author_hex))
1387 }
1388 None => None,
1389 };
1390 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1391 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1394 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1395 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1396 .await
1397 .map_err(VectorError::Other);
1398 }
1399 let (community, channel) = self.resolve_channel(channel_id)?;
1400 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1401 let reply = replied_to.filter(|r| !r.is_empty());
1402 let ms = std::time::SystemTime::now()
1403 .duration_since(std::time::UNIX_EPOCH)
1404 .map(|d| d.as_millis() as u64)
1405 .unwrap_or(0);
1406 let unsigned = envelope::build_inner_typed(
1407 author_pk,
1408 &channel.id,
1409 channel.epoch,
1410 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1411 content,
1412 ms,
1413 reply,
1414 &[],
1415 );
1416 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1417 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1418 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1419 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1420 let session = state::SessionGuard::capture();
1421 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1422 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1423 .await
1424 .map_err(VectorError::Other)?;
1425 if !session.is_valid() {
1428 return Ok(message_id);
1429 }
1430 let echoed = {
1431 let mut st = state::STATE.lock().await;
1432 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1433 };
1434 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1435 let _ = crate::db::events::save_message(channel_id, &msg).await;
1436 }
1437 Ok(message_id)
1438 }
1439
1440 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1444 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1445 let path = std::path::Path::new(file_path);
1446 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1447 if bytes.is_empty() {
1448 return Err(VectorError::Other("Empty file".into()));
1449 }
1450 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1451 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1452
1453 let session = state::SessionGuard::capture();
1456 let v2_target = match self.v2_community_for_channel(channel_id)? {
1459 Some(id) => Some(
1460 crate::db::community::load_community_v2(&id)
1461 .map_err(VectorError::Other)?
1462 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1463 ),
1464 None => None,
1465 };
1466 let v1_target = match v2_target {
1467 Some(_) => None,
1468 None => Some(self.resolve_channel(channel_id)?),
1469 };
1470 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1471
1472 let file_hash = crate::crypto::sha256_hex(&bytes);
1473 let mime = crate::crypto::mime_from_extension(&extension);
1474 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1475
1476 let download_dir = crate::db::get_download_dir();
1478 let _ = std::fs::create_dir_all(&download_dir);
1479 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1480 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1481 let _ = std::fs::write(&local_path, &bytes);
1482
1483 let params = crate::crypto::generate_encryption_params();
1485 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1486 let encrypted_size = encrypted.len() as u64;
1487
1488 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1489 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1490 let servers = crate::blossom_servers::compute_enabled_servers();
1491 if servers.is_empty() {
1492 return Err(VectorError::Other("No Blossom servers configured".into()));
1493 }
1494 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1495 let url = crate::blossom::upload_blob_with_progress_and_failover(
1496 signer.clone(),
1497 servers,
1498 std::sync::Arc::new(encrypted),
1499 Some(mime),
1500 true,
1501 noop_progress,
1502 Some(3),
1503 Some(std::time::Duration::from_secs(2)),
1504 None,
1505 ).await.map_err(VectorError::Other)?;
1506
1507 let attachment = crate::types::Attachment {
1508 id: file_hash.clone(),
1509 key: params.key.clone(),
1510 nonce: params.nonce.clone(),
1511 extension: extension.clone(),
1512 name: filename.clone(),
1513 url,
1514 path: local_path.to_string_lossy().to_string(),
1515 size: encrypted_size,
1516 img_meta,
1517 downloading: false,
1518 downloaded: true,
1519 ..Default::default()
1520 };
1521 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
1522
1523 if !session.is_valid() {
1525 return Err(VectorError::Other("account changed during upload".into()));
1526 }
1527 if let Some(community) = v2_target {
1529 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1530 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1531 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
1532 .await
1533 .map_err(VectorError::Other);
1534 }
1535 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
1536 let ms = std::time::SystemTime::now()
1537 .duration_since(std::time::UNIX_EPOCH)
1538 .map(|d| d.as_millis() as u64)
1539 .unwrap_or(0);
1540 let unsigned = envelope::build_inner_full(
1541 author_pk, &channel.id, channel.epoch,
1542 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
1543 );
1544 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1545 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1546 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1547 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1548 .await.map_err(VectorError::Other)?;
1549 let echoed = {
1551 let mut st = state::STATE.lock().await;
1552 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1553 };
1554 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
1555 let _ = crate::db::events::save_message(channel_id, &m).await;
1556 }
1557 Ok(message_id)
1558 }
1559
1560 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
1562 use crate::community::{service, transport::LiveTransport};
1563 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1564 let community = crate::db::community::load_community_v2(&id)
1565 .map_err(VectorError::Other)?
1566 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1567 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1568 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1569 return crate::community::v2::service::send_typing(&transport, &community, &ch)
1570 .await
1571 .map_err(VectorError::Other);
1572 }
1573 let (community, channel) = self.resolve_channel(channel_id)?;
1574 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1575 service::publish_typing_signal(&transport, &community, &channel)
1576 .await
1577 .map_err(VectorError::Other)
1578 }
1579
1580 pub async fn send_community_reaction(
1583 &self,
1584 channel_id: &str,
1585 message_id: &str,
1586 emoji: &str,
1587 emoji_url: Option<&str>,
1588 ) -> Result<()> {
1589 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
1590 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
1591 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
1592 }
1593 _ => Vec::new(),
1594 };
1595 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1596 let session = state::SessionGuard::capture();
1597 let community = crate::db::community::load_community_v2(&id)
1598 .map_err(VectorError::Other)?
1599 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1600 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1601 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1602 let held = {
1607 let st = state::STATE.lock().await;
1608 st.find_message(message_id)
1609 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1610 };
1611 let held = held.or_else(|| {
1612 crate::db::events::event_author(message_id)
1613 .ok()
1614 .flatten()
1615 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
1616 });
1617 let target_author = match held {
1618 Some(pk) => pk,
1619 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
1620 .await
1621 .map_err(VectorError::Other)?
1622 .iter()
1623 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
1624 .map(|f| f.event.opened().author)
1625 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
1626 };
1627 if !session.is_valid() {
1629 return Err(VectorError::Other("account changed before send".into()));
1630 }
1631 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
1632 return crate::community::v2::service::send_reaction(
1637 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
1638 )
1639 .await
1640 .map(|_| ())
1641 .map_err(VectorError::Other);
1642 }
1643 self.publish_community_control(
1644 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
1645 ).await
1646 }
1647
1648 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
1650 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
1651 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1652 let community = crate::db::community::load_community_v2(&id)
1653 .map_err(VectorError::Other)?
1654 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1655 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1656 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1657 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
1658 .await
1659 .map(|_| ())
1660 .map_err(VectorError::Other);
1661 }
1662 self.publish_community_control(
1663 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
1664 ).await
1665 }
1666
1667 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
1671 let channel_id = {
1672 let st = state::STATE.lock().await;
1673 match st.find_message(message_id) {
1674 Some((chat, _)) => chat.id.clone(),
1675 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
1676 }
1677 };
1678 self.delete_community_message_in(&channel_id, message_id).await
1679 }
1680
1681 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
1685 use crate::community::{service, transport::LiveTransport};
1686 let session = state::SessionGuard::capture();
1687 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1688
1689 let attachment_urls: Vec<String> = {
1692 let st = state::STATE.lock().await;
1693 st.find_message(message_id)
1694 .map(|(_, msg)| msg.attachments.iter().filter(|a| !a.url.is_empty()).map(|a| a.url.clone()).collect())
1695 .unwrap_or_default()
1696 };
1697
1698 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1699 let community = crate::db::community::load_community_v2(&id)
1702 .map_err(VectorError::Other)?
1703 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1704 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
1705 crate::community::v2::service::send_delete(
1706 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
1707 )
1708 .await
1709 .map_err(VectorError::Other)?;
1710 } else {
1711 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
1713 let _ = service::delete_message(&transport, message_id).await;
1714 }
1715 self.publish_community_control(
1717 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
1718 ).await?;
1719 }
1720 if !attachment_urls.is_empty() {
1722 if let Some(client) = state::nostr_client() {
1723 if let Ok(signer) = client.signer().await {
1724 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
1725 }
1726 }
1727 }
1728 if !session.is_valid() {
1731 return Ok(());
1732 }
1733 let removed_chat = {
1734 let mut st = state::STATE.lock().await;
1735 st.remove_message(message_id).map(|(cid, _)| cid)
1736 };
1737 let _ = crate::db::events::delete_event(message_id).await;
1738 traits::emit_event_json("message_removed", serde_json::json!({
1739 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
1740 }));
1741 Ok(())
1742 }
1743
1744 async fn publish_community_control(
1747 &self,
1748 channel_id: &str,
1749 kind: u16,
1750 content: &str,
1751 target: &str,
1752 emoji_tags: &[crate::types::EmojiTag],
1753 ) -> Result<()> {
1754 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1755 let (community, channel) = self.resolve_channel(channel_id)?;
1756 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1757 let ms = std::time::SystemTime::now()
1758 .duration_since(std::time::UNIX_EPOCH)
1759 .map(|d| d.as_millis() as u64)
1760 .unwrap_or(0);
1761 let unsigned = envelope::build_inner_typed(
1762 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
1763 );
1764 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1765 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1766 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1767 let session = state::SessionGuard::capture();
1768 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1769 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1770 .await.map_err(VectorError::Other)?;
1771 if !session.is_valid() {
1774 return Ok(());
1775 }
1776 let outcome = {
1777 let mut st = state::STATE.lock().await;
1778 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1779 };
1780 if let Some(inbound::IncomingEvent::Updated { target_id, message, edit_event }) = outcome {
1781 if let Some(ev) = edit_event {
1782 let mut ev = (*ev).clone();
1783 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
1784 let _ = crate::db::events::save_event(&ev).await;
1785 } else {
1786 let _ = crate::db::events::save_message(channel_id, &message).await;
1787 }
1788 traits::emit_event_json("message_update", serde_json::json!({
1789 "old_id": target_id, "message": &message, "chat_id": channel_id,
1790 }));
1791 }
1792 Ok(())
1793 }
1794
1795 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
1803 use crate::community::{inbound, send, service, transport::LiveTransport};
1804 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1805 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1810 let warnings = if community::v2::realtime::follow_worker_running() {
1811 community::v2::realtime::enqueue_follow(&id);
1812 Vec::new()
1813 } else {
1814 Self::v2_inline_follow(&id).await
1815 };
1816 let new = Self::v2_backfill_channel(&id, channel_id, limit).await;
1817 return Ok((new, warnings));
1818 }
1819 let (community, _) = self.resolve_channel(channel_id)?;
1820 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1821 let mut warnings: Vec<String> = Vec::new();
1822
1823 match service::catch_up_server_root(&transport, &community).await {
1831 Ok(c) if c.removed => {
1832 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1834 return Ok((0, warnings));
1835 }
1836 Ok(_) => {}
1837 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
1838 }
1839 let (community, _) = self.resolve_channel(channel_id)?;
1840
1841 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
1847 warnings.push(format!("control fold failed: {e}"));
1848 }
1849 if service::am_i_banned(&community) {
1850 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1852 return Ok((0, warnings));
1853 }
1854 let (community, channel) = self.resolve_channel(channel_id)?;
1857 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
1858 warnings.push(format!("channel catch-up failed: {e}"));
1859 }
1860 let (community, _) = self.resolve_channel(channel_id)?;
1864 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
1865 warnings.push(format!("read-cut resume failed: {e}"));
1866 }
1867 let (community, channel) = self.resolve_channel(channel_id)?;
1868
1869 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
1870 .await
1871 .map_err(VectorError::Other)?;
1872 let outcomes = {
1873 let mut st = state::STATE.lock().await;
1874 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
1875 };
1876 let mut new = 0usize;
1877 for o in &outcomes {
1878 match o {
1879 inbound::IncomingEvent::NewMessage(m) => {
1880 let _ = crate::db::events::save_message(channel_id, m).await;
1881 new += 1;
1882 }
1883 inbound::IncomingEvent::Updated { message, .. } => {
1884 let _ = crate::db::events::save_message(channel_id, message).await;
1885 }
1886 inbound::IncomingEvent::Removed { target_id } => {
1887 let _ = crate::db::events::delete_event(target_id).await;
1888 }
1889 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
1890 let _ = crate::db::events::delete_event(reaction_id).await;
1893 }
1894 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
1895 let et = if *joined {
1896 crate::stored_event::SystemEventType::MemberJoined
1897 } else {
1898 crate::stored_event::SystemEventType::MemberLeft
1899 };
1900 let note = invited_by.as_ref().map(|by| match invited_label {
1902 Some(l) if !l.is_empty() => format!("{by}|{l}"),
1903 _ => by.clone(),
1904 });
1905 let _ = crate::db::events::save_system_event_at(event_id, channel_id, et, npub, note.as_deref(), *created_at, invited_by.as_deref(), invited_label.as_deref()).await;
1906 }
1907 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
1908 community::service::persist_webxdc_signal(
1911 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
1912 ).await;
1913 }
1914 inbound::IncomingEvent::Kicked { community_id }
1915 | inbound::IncomingEvent::SelfLeft { community_id } => {
1916 let _ = crate::db::community::delete_community_retain_keys(community_id);
1921 break;
1922 }
1923 inbound::IncomingEvent::Typing { .. } => {
1924 }
1926 }
1927 }
1928 Ok((new, warnings))
1929 }
1930
1931 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
1943 use crate::bot_interface::{self, ChatCommandsSnapshot};
1944 use nostr_sdk::prelude::ToBech32;
1945
1946 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
1947 let mut relays: Vec<String> = Vec::new();
1948 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
1949 if let Some(cid_hex) = community_hex {
1950 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
1951 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
1952 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
1953 relays = community.relays.clone();
1954 } else {
1955 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1956 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
1957 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
1958 };
1959 relays = community.relays.clone();
1960 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
1961 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
1962 members.push(pk);
1963 }
1964 }
1965 }
1966 let state = crate::state::STATE.lock().await;
1967 for pk in members {
1968 let Ok(npub) = pk.to_bech32() else { continue };
1969 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
1970 bots.push(pk);
1971 }
1972 }
1973 } else if chat_id.starts_with("npub1") {
1974 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
1975 let is_bot = {
1976 let state = crate::state::STATE.lock().await;
1977 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
1978 };
1979 if is_bot {
1980 bots.push(pk);
1981 if let Some(client) = crate::state::nostr_client() {
1984 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
1985 }
1986 }
1987 }
1988 }
1989
1990 if bots.is_empty() {
1991 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
1992 }
1993 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
1996 relays.sort();
1997 relays.dedup();
1998 bots.sort_by_key(|p| p.to_hex());
2001 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2002 let commands = bot_interface::assemble_from_store(&bot_hexes);
2003 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2004 if !fresh {
2005 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2006 }
2007 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2008 }
2009
2010 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2015 use nostr_sdk::prelude::ToBech32;
2016 match Self::load_v2_if_v2(community_id) {
2022 Ok(Some(community)) => {
2023 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2024 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2025 if cursor == 0 {
2026 if crate::community::v2::realtime::follow_worker_running() {
2027 crate::community::v2::realtime::enqueue_follow(community.id());
2028 } else {
2029 let session = state::SessionGuard::capture();
2030 let c2 = community.clone();
2031 tokio::spawn(async move {
2032 if !session.is_valid() {
2033 return;
2034 }
2035 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2036 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2, &session).await, Ok(fresh) if !fresh.is_empty()) {
2037 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2038 }
2039 });
2040 }
2041 }
2042 return crate::community::v2::service::stored_memberlist(&community)
2043 .unwrap_or_default()
2044 .into_iter()
2045 .filter_map(|pk| pk.to_bech32().ok())
2046 .map(|npub| serde_json::json!({ "npub": npub }))
2047 .collect();
2048 }
2049 Ok(None) => {} Err(_) => return Vec::new(),
2052 }
2053 crate::db::community::community_member_activity(community_id)
2054 .unwrap_or_default()
2055 .into_iter()
2056 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2057 .collect()
2058 }
2059
2060 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2064 use crate::community::transport::LiveTransport;
2065 let session = state::SessionGuard::capture();
2066 let lock = crate::community::v2::realtime::follow_lock(id);
2071 let _guard = lock.lock().await;
2072 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2073 let mut warnings: Vec<String> = Vec::new();
2074 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2075 warnings.push("v2 community not found".to_string());
2076 return warnings;
2077 };
2078 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2079 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2080 Ok(f) if f.dissolved => return warnings,
2082 Ok(f) if f.self_removed => {
2083 if session.is_valid() {
2086 let _ = crate::db::community::delete_community(&cid_hex);
2087 }
2088 return warnings;
2089 }
2090 Ok(_) => {}
2091 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2092 }
2093 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2094 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
2095 Ok(Some(changed)) => {
2099 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2100 warnings.push(format!("v2 rekey follow failed: {e}"));
2101 }
2102 }
2103 Ok(None) => {}
2104 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2105 }
2106 }
2107 warnings
2108 }
2109
2110 async fn v2_backfill_channel(id: &crate::community::CommunityId, channel_id: &str, limit: usize) -> usize {
2118 use crate::community::v2::inbound::{apply_chat_to_state, persist_chat, ChatPersist};
2119 const MAX_BACKFILL_PAGES: usize = 8;
2121 let session = state::SessionGuard::capture();
2124 let Some(my_pk) = state::my_public_key() else { return 0 };
2125 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2129 return 0;
2130 }
2131 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
2132 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2133 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2134 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2135 &transport,
2136 &community,
2137 &ch,
2138 limit.max(50),
2139 MAX_BACKFILL_PAGES,
2140 |page| {
2145 let mut saw_message = false;
2146 for f in page {
2147 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2148 saw_message = true;
2149 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2150 return true;
2151 }
2152 }
2153 }
2154 !saw_message
2155 },
2156 )
2157 .await
2158 else {
2159 return 0;
2160 };
2161 let mut new = 0usize;
2162 for f in &page {
2163 if !session.is_valid() {
2166 break;
2167 }
2168 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2173 if opened.author != my_pk {
2174 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2175 if let Ok(npub) = ToBech32::to_bech32(&opened.author) {
2176 crate::community::service::persist_webxdc_signal(
2177 channel_id,
2178 &npub,
2179 &topic,
2180 addr.as_deref(),
2181 &opened.rumor_id.to_hex(),
2182 opened.at_ms / 1000,
2183 )
2184 .await;
2185 }
2186 }
2187 }
2188 continue;
2189 }
2190 let outcome = {
2192 let mut st = state::STATE.lock().await;
2193 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2194 };
2195 if let Some(outcome) = outcome {
2196 if matches!(outcome, ChatPersist::New(_)) {
2197 new += 1;
2198 }
2199 persist_chat(channel_id, &outcome).await;
2200 }
2201 }
2202 new
2203 }
2204
2205 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2209 if community_id.len() != 64 {
2210 return Ok(None);
2211 }
2212 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2213 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
2214 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
2215 _ => Ok(None),
2216 }
2217 }
2218
2219 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
2224 use crate::community::CommunityId;
2225 if community_id.len() != 64 {
2226 return Err(VectorError::Other("malformed community id".into()));
2227 }
2228 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
2229 .map_err(VectorError::Other)?
2230 .ok_or_else(|| VectorError::Other("community not found".into()))
2231 }
2232
2233 fn admin_role_id_of(community_id: &str) -> Result<String> {
2234 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2235 roles.roles.iter()
2236 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
2237 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
2238 .map(|r| r.role_id.clone())
2239 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
2240 }
2241
2242 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
2246 use crate::community::service;
2247 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2248 use crate::community::roles::Permissions;
2249 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
2250 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
2251 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2252 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2255 if banned.contains(&me) && me != owner_hex {
2256 return Ok(serde_json::json!({
2257 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
2258 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
2259 }));
2260 }
2261 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
2262 return Ok(serde_json::json!({
2263 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
2264 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
2265 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
2266 "manage_admin_role": me == owner_hex,
2268 }));
2269 }
2270 let community = Self::load_community_hex(community_id)?;
2271 let caps = service::caller_capabilities(&community);
2272 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
2273 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
2274 .unwrap_or(false);
2275 Ok(serde_json::json!({
2276 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
2277 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
2278 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
2279 "manage_admin_role": manage_admin_role,
2280 }))
2281 }
2282
2283 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
2286 use nostr_sdk::prelude::{PublicKey, ToBech32};
2287 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2288 let owner = v2.owner().map_err(VectorError::Other)?;
2289 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2290 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2292 let admins: Vec<String> = roster.grants.iter()
2293 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
2294 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2295 .collect();
2296 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
2297 }
2298 let community = Self::load_community_hex(community_id)?;
2299 let owner = community.owner_attestation.as_ref()
2300 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
2301 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
2302 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2303 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
2304 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2305 .collect();
2306 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
2307 }
2308
2309 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2311 use crate::community::{service, transport::LiveTransport};
2312 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2313 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2314 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2315 return crate::community::v2::service::grant_admin(&transport, &v2, &member)
2316 .await
2317 .map_err(VectorError::Other);
2318 }
2319 let community = Self::load_community_hex(community_id)?;
2320 let role_id = Self::admin_role_id_of(community_id)?;
2321 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2322 }
2323
2324 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2326 use crate::community::{service, transport::LiveTransport};
2327 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2328 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2329 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2330 return crate::community::v2::service::revoke_admin(&transport, &v2, &member)
2331 .await
2332 .map_err(VectorError::Other);
2333 }
2334 let community = Self::load_community_hex(community_id)?;
2335 let role_id = Self::admin_role_id_of(community_id)?;
2336 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2337 }
2338
2339 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
2341 use crate::community::{service, transport::LiveTransport};
2342 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2343 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2344 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2345 return crate::community::v2::service::kick_member(&transport, &v2, &pk)
2346 .await
2347 .map_err(VectorError::Other);
2348 }
2349 let community = Self::load_community_hex(community_id)?;
2350 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
2351 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
2352 }
2353
2354 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
2357 use crate::community::{service, transport::LiveTransport, CommunityId};
2358 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2359 let hex = pk.to_hex();
2360 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2361 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
2363 list.retain(|h| h != &hex);
2364 if banned {
2365 list.push(hex);
2366 }
2367 if community_id.len() == 64 {
2371 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2372 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2373 let community = crate::db::community::load_community_v2(&cid)
2374 .map_err(VectorError::Other)?
2375 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2376 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
2377 if banned {
2378 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
2379 crate::community::v2::service::refound_community(&transport, &community, &[pk]).await.map_err(VectorError::Other)?;
2380 }
2381 return Ok(());
2382 }
2383 }
2384 let community = Self::load_community_hex(community_id)?;
2385 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
2386 }
2387
2388 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
2392 use crate::community::{service, transport::LiveTransport, CommunityId};
2393 if community_id.len() != 64 {
2394 return Err(VectorError::Other("malformed community id".into()));
2395 }
2396 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2397 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2398 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2401 let community = crate::db::community::load_community_v2(&cid)
2402 .map_err(VectorError::Other)?
2403 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2404 return crate::community::v2::service::dissolve_community(&transport, &community)
2405 .await
2406 .map_err(VectorError::Other);
2407 }
2408 let community = Self::load_community_hex(community_id)?;
2409 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
2410 }
2411
2412 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
2415 use crate::community::{service, transport::LiveTransport, CommunityId};
2416 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2417 if community_id.len() == 64 {
2422 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2423 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2424 let community = crate::db::community::load_community_v2(&cid)
2425 .map_err(VectorError::Other)?
2426 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2427 let mut meta = community.metadata();
2428 if let Some(n) = name {
2429 meta.name = n.to_string();
2430 }
2431 if let Some(d) = description {
2432 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
2433 }
2434 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
2435 .await
2436 .map_err(VectorError::Other);
2437 }
2438 }
2439 let mut community = Self::load_community_hex(community_id)?;
2440 if let Some(n) = name { community.name = n.to_string(); }
2441 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
2442 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
2443 }
2444
2445 pub async fn create_community_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
2451 let v2 = Self::load_v2_if_v2(community_id)?
2452 .ok_or_else(|| VectorError::Other("channel creation is available on v2 communities".into()))?;
2453 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2454 let id = if private {
2455 crate::community::v2::service::create_private_channel(&transport, &v2, name).await
2456 } else {
2457 crate::community::v2::service::create_public_channel(&transport, &v2, name).await
2458 }
2459 .map_err(VectorError::Other)?;
2460 if let Some(client) = state::nostr_client() {
2463 crate::community::v2::realtime::refresh_subscription(&client).await;
2464 }
2465 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
2466 }
2467
2468 pub async fn delete_community_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
2470 let v2 = Self::load_v2_if_v2(community_id)?
2471 .ok_or_else(|| VectorError::Other("channel deletion is available on v2 communities".into()))?;
2472 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2473 let name = v2.channels.iter().find(|c| c.id.0 == ch.0).map(|c| c.name.clone()).unwrap_or_default();
2474 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2475 crate::community::v2::service::delete_channel(&transport, &v2, &ch, &name)
2476 .await
2477 .map_err(VectorError::Other)
2478 }
2479
2480 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
2483 use crate::community::{transport::LiveTransport, CommunityId};
2484 if community_id.len() != 64 {
2485 return Err(VectorError::Other("malformed community id".into()));
2486 }
2487 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2488 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2490 let session = state::SessionGuard::capture();
2491 let channel_ids: Vec<String> =
2492 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
2493 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2494 crate::community::v2::service::leave_community(&transport, &v2)
2495 .await
2496 .map_err(VectorError::Other)?;
2497 if !session.is_valid() {
2498 return Err(VectorError::Other("account changed during leave".into()));
2499 }
2500 let mut st = state::STATE.lock().await;
2501 st.chats.retain(|c| !channel_ids.contains(&c.id));
2502 return Ok(());
2503 }
2504 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
2505 let channel_ids: Vec<String> = community
2506 .as_ref()
2507 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
2508 .unwrap_or_default();
2509 if let Some(ref c) = community {
2511 if let Some(primary) = c.channels.first() {
2512 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2513 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
2514 }
2515 }
2516 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
2518 {
2519 let mut st = state::STATE.lock().await;
2520 st.chats.retain(|c| !channel_ids.contains(&c.id));
2521 }
2522 Ok(())
2523 }
2524
2525 fn resolve_channel(
2527 &self,
2528 channel_id: &str,
2529 ) -> Result<(crate::community::Community, crate::community::Channel)> {
2530 use crate::community::CommunityId;
2531 let community_id = crate::db::community::community_id_for_channel(channel_id)
2532 .map_err(VectorError::Other)?
2533 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
2534 if community_id.len() != 64 {
2535 return Err(VectorError::Other("malformed community id".into()));
2536 }
2537 let community = crate::db::community::load_community(&CommunityId(
2538 crate::simd::hex::hex_to_bytes_32(&community_id),
2539 ))
2540 .map_err(VectorError::Other)?
2541 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
2542 let channel = community
2543 .channels
2544 .iter()
2545 .find(|c| c.id.to_hex() == channel_id)
2546 .cloned()
2547 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
2548 Ok((community, channel))
2549 }
2550
2551
2552 pub async fn sync_dms(
2569 &self,
2570 since_days: Option<u64>,
2571 handler: &dyn InboundEventHandler,
2572 ) -> Result<(u32, u32)> {
2573 use futures_util::StreamExt;
2574 use nostr_sdk::prelude::*;
2575
2576 let client = state::nostr_client()
2577 .ok_or(VectorError::Other("Not connected".into()))?;
2578 let my_pk = state::my_public_key()
2579 .ok_or(VectorError::Other("Not logged in".into()))?;
2580
2581 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
2583
2584 let (items, filter) = if let Some(days) = since_days {
2586 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
2587 let items: Vec<(EventId, Timestamp)> = all_items.iter()
2588 .filter(|(_, ts)| ts.as_secs() >= since_ts)
2589 .cloned()
2590 .collect();
2591 let filter = Filter::new()
2592 .pubkey(my_pk)
2593 .kind(Kind::GiftWrap)
2594 .since(Timestamp::from_secs(since_ts));
2595 (items, filter)
2596 } else {
2597 let filter = Filter::new()
2598 .pubkey(my_pk)
2599 .kind(Kind::GiftWrap);
2600 (all_items, filter)
2601 };
2602
2603 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
2604
2605 let sync_opts = nostr_sdk::SyncOptions::new()
2607 .direction(nostr_sdk::SyncDirection::Down)
2608 .initial_timeout(std::time::Duration::from_secs(10))
2609 .dry_run();
2610
2611 let relay_map = client.relays().await;
2613 let all_relays: Vec<(RelayUrl, Relay)> = relay_map.iter()
2614 .map(|(url, relay)| (url.clone(), relay.clone()))
2615 .collect();
2616 drop(relay_map);
2617
2618 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
2619 for (url, relay) in &all_relays {
2620 let url = url.clone();
2621 let relay = relay.clone();
2622 let f = filter.clone();
2623 let i = items.clone();
2624 let o = sync_opts.clone();
2625 relay_futs.push(async move {
2626 let result = tokio::time::timeout(
2627 std::time::Duration::from_secs(10),
2628 relay.sync_with_items(f, i, &o),
2629 ).await;
2630 (url, result)
2631 });
2632 }
2633
2634 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
2636 while let Some((url, result)) = relay_futs.next().await {
2637 match result {
2638 Ok(Ok(recon)) => {
2639 let count = recon.remote.len();
2640 all_missing.extend(recon.remote);
2641 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
2642 }
2643 Ok(Err(e)) => log_warn!("[SyncDMs] {} failed: {}", url, e),
2644 Err(_) => log_warn!("[SyncDMs] {} timed out (10s)", url),
2645 }
2646 }
2647
2648 if all_missing.is_empty() {
2649 log_info!("[SyncDMs] No missing events");
2650 return Ok((0, 0));
2651 }
2652
2653 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
2655 let ids: Vec<EventId> = all_missing.into_iter().collect();
2656 let relay_strs: Vec<String> = client.relays().await.keys()
2657 .map(|u| u.to_string()).collect();
2658
2659 let mut total_events = 0u32;
2660 let mut new_messages = 0u32;
2661 const BATCH_SIZE: usize = 500;
2662
2663 for batch in ids.chunks(BATCH_SIZE) {
2664 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap);
2665 match client.stream_events_from(
2666 relay_strs.clone(), f,
2667 std::time::Duration::from_secs(30),
2668 ).await {
2669 Ok(stream) => {
2670 let client_clone = client.clone();
2671 let prepared_stream = stream
2672 .map(move |event| {
2673 let c = client_clone.clone();
2674 tokio::spawn(async move {
2675 event_handler::prepare_event(event, &c, my_pk).await
2676 })
2677 })
2678 .buffer_unordered(8);
2679 tokio::pin!(prepared_stream);
2680
2681 while let Some(result) = prepared_stream.next().await {
2682 total_events += 1;
2683 if let Ok(prepared) = result {
2684 if event_handler::commit_prepared_event(prepared, false, handler).await {
2685 new_messages += 1;
2686 }
2687 }
2688 }
2689 }
2690 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
2691 }
2692 }
2693
2694 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
2695 Ok((total_events, new_messages))
2696 }
2697
2698 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::SubscriptionId> {
2707 use nostr_sdk::prelude::*;
2708 let client = state::nostr_client()
2709 .ok_or(VectorError::Other("Not connected".into()))?;
2710 let my_pk = state::my_public_key()
2711 .ok_or(VectorError::Other("Not logged in".into()))?;
2712
2713 let filter = Filter::new()
2714 .pubkey(my_pk)
2715 .kind(Kind::GiftWrap)
2716 .limit(0);
2717
2718 let output = client.subscribe(filter, None).await
2719 .map_err(|e| VectorError::Nostr(e.to_string()))?;
2720 Ok(output.val)
2721 }
2722
2723 pub async fn sync_communities(&self) -> Result<()> {
2734 {
2738 use crate::community::{transport::LiveTransport, v2::service as v2};
2739 let bootstrap: Vec<String> = match crate::state::nostr_client() {
2740 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
2741 None => Vec::new(),
2742 };
2743 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2744 if let Ok(joined) = v2::sync_community_list(&transport, &bootstrap).await {
2745 for c in &joined {
2746 if community::v2::realtime::follow_worker_running() {
2747 community::v2::realtime::enqueue_follow(c.id());
2748 } else {
2749 let _ = Self::v2_inline_follow(c.id()).await;
2750 }
2751 }
2752 if !joined.is_empty() {
2753 if let Some(client) = crate::state::nostr_client() {
2754 community::v2::realtime::refresh_subscription(&client).await;
2755 }
2756 }
2757 }
2758 }
2759
2760 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
2761 for id in ids {
2762 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
2763 if community::v2::realtime::follow_worker_running() {
2766 community::v2::realtime::enqueue_follow(&id);
2767 } else {
2768 let _ = Self::v2_inline_follow(&id).await;
2769 }
2770 continue;
2771 }
2772 if let Ok(Some(community)) = db::community::load_community(&id) {
2773 for ch in &community.channels {
2774 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
2775 }
2776 }
2777 }
2778 Ok(())
2779 }
2780
2781
2782 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
2814 use nostr_sdk::prelude::*;
2815
2816 let client = state::nostr_client()
2817 .ok_or(VectorError::Other("Not connected".into()))?;
2818 let my_pk = state::my_public_key()
2819 .ok_or(VectorError::Other("Not logged in".into()))?;
2820
2821 community::v2::streamauth::ensure_responder(&client);
2828
2829 community::v2::realtime::spawn_follow_worker(handler.clone());
2838 let _ = self.sync_communities().await;
2839 let _ = self.sync_dms(None, &NoOpEventHandler).await;
2840
2841 let dm_sub_id = self.subscribe_dms().await?;
2844 community::realtime::refresh_subscription(&client).await;
2845 community::v2::realtime::refresh_subscription(&client).await;
2846
2847 if let Some(monitor) = client.monitor() {
2854 let mut rx = monitor.subscribe();
2855 let session = state::SessionGuard::capture();
2856 tokio::spawn(async move {
2857 let mut last_resync: Option<std::time::Instant> = None;
2860 while let Ok(notification) = rx.recv().await {
2861 if !session.is_valid() {
2862 return;
2863 }
2864 let MonitorNotification::StatusChanged { status, .. } = notification;
2865 if status == RelayStatus::Connected {
2866 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
2867 continue;
2868 }
2869 let _ = VectorCore.sync_communities().await;
2870 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
2871 if let Some(c) = state::nostr_client() {
2872 community::realtime::refresh_subscription(&c).await;
2873 community::v2::realtime::refresh_subscription(&c).await;
2874 }
2875 last_resync = Some(std::time::Instant::now());
2876 }
2877 }
2878 });
2879 }
2880
2881 {
2885 let client_health = client.clone();
2886 let session = state::SessionGuard::capture();
2887 tokio::spawn(async move {
2888 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
2890 if !session.is_valid() {
2891 return;
2892 }
2893 for (url, relay) in client_health.relays().await {
2894 match relay.status() {
2895 RelayStatus::Connected => {
2896 let probe = tokio::time::timeout(
2897 std::time::Duration::from_secs(10),
2898 client_health.fetch_events_from(
2899 vec![url.to_string()],
2900 Filter::new().kind(Kind::Metadata).limit(1),
2901 std::time::Duration::from_secs(8),
2902 ),
2903 )
2904 .await;
2905 if !matches!(probe, Ok(Ok(_))) {
2906 let _ = relay.disconnect();
2907 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2908 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
2909 }
2910 }
2911 RelayStatus::Terminated | RelayStatus::Disconnected => {
2912 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
2913 }
2914 _ => {}
2915 }
2916 }
2917 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
2918 }
2919 });
2920 }
2921
2922 let client_for_closure = client.clone();
2923
2924 client.handle_notifications(move |notification| {
2925 let handler = handler.clone();
2926 let c = client_for_closure.clone();
2927 let dm_sid = dm_sub_id.clone();
2928 async move {
2929 if let RelayPoolNotification::Message {
2933 message: nostr_sdk::RelayMessage::Ok { event_id, status, .. }, ..
2934 } = ¬ification {
2935 sending::note_relay_ok(event_id, *status);
2936 }
2937 if let RelayPoolNotification::Event { event, subscription_id, .. } = notification {
2938 if subscription_id == dm_sid {
2939 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
2941 event_handler::commit_prepared_event(prepared, true, &*handler).await;
2942 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
2943 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
2944 {
2945 let session = state::SessionGuard::capture();
2949 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
2950 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
2951 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
2952 {
2953 let session = state::SessionGuard::capture();
2955 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
2956 }
2957 }
2958 Ok(false)
2959 }
2960 }).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
2961
2962 Ok(())
2963 }
2964
2965 pub async fn logout(&self) {
2967 if let Some(client) = state::nostr_client() {
2968 let _ = client.disconnect().await;
2969 }
2970 db::close_database();
2971 }
2972
2973 pub async fn swap_session(&self) {
2981 state::bump_session_generation();
2983
2984 if let Some(client) = state::take_nostr_client() {
2987 let _ = client.shutdown().await;
2988 }
2989 db::close_database();
2990
2991 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
2993 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
2994 {
2995 use zeroize::Zeroize;
2996 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
2997 if let Some(s) = g.as_mut() { s.zeroize(); }
2998 *g = None;
2999 }
3000 if let Ok(mut g) = state::PENDING_NSEC.lock() {
3001 if let Some(s) = g.as_mut() { s.zeroize(); }
3002 *g = None;
3003 }
3004 }
3005
3006 {
3008 let mut st = state::STATE.lock().await;
3009 st.profiles.clear();
3010 st.chats.clear();
3011 st.db_loaded = false;
3012 st.is_syncing = false;
3013 }
3014 state::WRAPPER_ID_CACHE.lock().await.clear();
3015 state::PENDING_EVENTS.lock().await.clear();
3016 state::set_active_chat(None);
3017 crate::profile::sync::clear_profile_sync_queue();
3018 crate::inbox_relays::clear_inbox_relay_cache();
3019 crate::sending::clear_wrap_confirms();
3022 crate::emoji_packs::clear_nip65_cache();
3023 crate::db::clear_id_caches();
3027 crate::community::cache::clear();
3031 crate::community::realtime::clear().await;
3034 crate::community::v2::realtime::clear().await;
3035 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
3039 }
3040}
3041
3042#[cfg(test)]
3043mod facade_tests {
3044 use super::*;
3045
3046 #[tokio::test]
3049 async fn download_attachment_rejects_private_url() {
3050 let att = crate::types::Attachment {
3051 url: "http://169.254.169.254/latest/meta-data/".to_string(),
3052 ..Default::default()
3053 };
3054 match VectorCore.download_attachment(&att).await {
3055 Err(VectorError::Other(msg)) => {
3056 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
3057 }
3058 other => panic!("expected SSRF rejection, got {other:?}"),
3059 }
3060 }
3061
3062 #[tokio::test]
3063 async fn download_attachment_rejects_empty_url() {
3064 let att = crate::types::Attachment::default();
3065 assert!(VectorCore.download_attachment(&att).await.is_err());
3066 }
3067
3068 #[tokio::test]
3072 async fn list_communities_and_channel_routing_are_protocol_aware() {
3073 use crate::community::transport::memory::MemoryRelay;
3074 use nostr_sdk::prelude::Keys;
3075
3076 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
3077 crate::db::close_database();
3078 crate::db::clear_id_caches();
3079 let tmp = tempfile::tempdir().unwrap();
3080 let acct = {
3082 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3083 let mut s = String::from("npub1");
3084 for i in 0..58 {
3085 s.push(B[(i * 7 + 3) % 32] as char);
3086 }
3087 s
3088 };
3089 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
3090 crate::db::set_app_data_dir(tmp.path().to_path_buf());
3091 crate::db::set_current_account(acct.clone()).unwrap();
3092 crate::db::init_database(&acct).unwrap();
3093 let _ = crate::state::take_nostr_client();
3094 let me = Keys::generate();
3095 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
3096 crate::state::set_my_public_key(me.public_key());
3097
3098 let relay = MemoryRelay::new();
3100 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
3101 .await
3102 .unwrap();
3103 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
3104
3105 let listed = VectorCore.list_communities().await;
3107 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
3108 assert_eq!(v2["name"], "V2 Guild");
3109 assert_eq!(v2["is_owner"], true);
3110 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
3111
3112 assert_eq!(
3114 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
3115 Some(community.identity.community_id),
3116 "a v2 channel is routed to v2"
3117 );
3118 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
3120 }
3121
3122 #[test]
3127 fn v2_invite_url_base_derivation_round_trips() {
3128 use crate::community::v2::derive::TOKEN_LEN;
3129 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
3130 use nostr_sdk::prelude::Keys;
3131 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
3132 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
3133 let signer = Keys::generate();
3134 let token = [0x07u8; TOKEN_LEN];
3135 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
3136 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
3137 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
3138 let parsed = parse_invite_link(&url).unwrap();
3139 assert_eq!(parsed.link_signer, signer.public_key());
3140 assert_eq!(parsed.token, token);
3141 }
3142}