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 badges;
69pub mod webxdc;
70#[cfg(feature = "tor")]
71pub mod tor;
72
73pub fn nostr_client_options() -> nostr_sdk::ClientOptions {
85 let opts = nostr_sdk::ClientOptions::new();
86 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
87 {
88 match tor::transport_state() {
89 tor::TorTransportState::Active(addr) => {
90 return opts.connection(nostr_sdk::client::Connection::new().proxy(addr));
91 }
92 tor::TorTransportState::RequiredButInactive => {
93 return opts.connection(
96 nostr_sdk::client::Connection::new().proxy(tor::blackhole_proxy_addr()),
97 );
98 }
99 tor::TorTransportState::Disabled => {}
100 }
101 }
102 opts
103}
104
105pub fn tor_aware_relay_options(opts: nostr_sdk::RelayOptions) -> nostr_sdk::RelayOptions {
115 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
116 {
117 match tor::transport_state() {
118 tor::TorTransportState::Active(addr) => {
119 return opts.connection_mode(nostr_sdk::pool::ConnectionMode::proxy(addr));
120 }
121 tor::TorTransportState::RequiredButInactive => {
122 return opts.connection_mode(
125 nostr_sdk::pool::ConnectionMode::proxy(tor::blackhole_proxy_addr()),
126 );
127 }
128 tor::TorTransportState::Disabled => {}
129 }
130 }
131 opts
132}
133
134pub fn community_relay_options() -> nostr_sdk::RelayOptions {
144 use nostr_sdk::RelayServiceFlags;
145 tor_aware_relay_options(
146 nostr_sdk::RelayOptions::new().flags(RelayServiceFlags::GOSSIP | RelayServiceFlags::PING),
147 )
148}
149
150pub mod stored_event;
152
153pub mod rumor;
155
156pub mod sending;
158
159pub mod wallpaper;
161
162pub mod deletion;
164
165pub mod simd;
167
168pub mod community;
170
171pub mod event_handler;
173
174pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
176pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
177pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
178pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
179pub use state::{
180 ChatState, NOSTR_CLIENT, MY_SECRET_KEY, MY_PUBLIC_KEY, STATE, ENCRYPTION_KEY,
181 nostr_client, my_public_key, has_active_session,
182 set_nostr_client, set_my_public_key,
183 take_nostr_client, clear_my_public_key,
184 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
185};
186pub use crypto::{GuardedKey, GuardedSigner};
187pub use signer::{
188 SignerKind, signer_kind, set_signer_kind, is_bunker,
189 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
190 build_bunker_signer, prewarm_bunker, drain_bunker_state,
191 parse_bunker_remote_pubkey, parse_bunker_relays,
192 BunkerConnectionState, bunker_state, set_bunker_state,
193 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
194 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
195 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
196};
197pub use error::{VectorError, Result};
198pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
199pub use db::{set_app_data_dir, get_app_data_dir};
200pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
201pub use deletion::{delete_own_dm, DeleteOutcome};
202pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
203pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
204pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
205pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
206
207use std::path::PathBuf;
208use std::sync::Arc;
209
210pub struct CoreConfig {
216 pub data_dir: PathBuf,
218 pub event_emitter: Option<Box<dyn EventEmitter>>,
220}
221
222#[derive(Clone, Copy)]
244pub struct VectorCore;
245
246impl VectorCore {
247 pub fn init(config: CoreConfig) -> Result<Self> {
249 db::set_app_data_dir(config.data_dir);
251
252 if let Some(emitter) = config.event_emitter {
254 traits::set_event_emitter(emitter);
255 }
256
257 let _ = rustls::crypto::ring::default_provider().install_default();
259
260 Ok(VectorCore)
261 }
262
263 pub fn accounts(&self) -> Result<Vec<String>> {
265 db::get_accounts().map_err(VectorError::from)
266 }
267
268 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
270 use nostr_sdk::prelude::*;
271
272 let keys = if key.starts_with("nsec1") {
274 let secret = SecretKey::from_bech32(key)
275 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
276 Keys::new(secret)
277 } else {
278 Keys::from_mnemonic(key, None)
280 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
281 };
282
283 let public_key = keys.public_key();
284 let npub = public_key.to_bech32()
285 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
286
287 let secret_bytes = keys.secret_key().to_secret_bytes();
289 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
290 state::set_my_public_key(public_key);
291
292 db::set_current_account(npub.clone())?;
294 db::init_database(&npub)?;
295
296 {
298 let nsec = keys.secret_key().to_bech32()
299 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
300 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
301
302 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
309 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
310 db::set_pkey(&nsec)?;
311 }
312 }
313
314 let has_encryption = state::resolve_encryption_enabled_from_db();
317
318 if has_encryption {
319 if let Some(pwd) = password {
320 let key = crate::crypto::hash_pass(pwd).await;
321 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
322 }
323 }
324 state::init_encryption_enabled();
327
328 let client = ClientBuilder::new()
331 .signer(keys)
332 .opts(nostr_client_options())
333 .monitor(Monitor::new(1024))
335 .build();
336
337 for relay in state::TRUSTED_RELAYS {
339 let opts = tor_aware_relay_options(nostr_sdk::RelayOptions::default());
340 client.pool().add_relay(*relay, opts).await.ok();
341 }
342
343 client.connect().await;
345
346 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
347
348 Ok(LoginResult { npub, has_encryption })
349 }
350
351 pub fn generate_nsec(&self) -> Result<String> {
354 use nostr_sdk::prelude::*;
355 Keys::generate().secret_key().to_bech32()
356 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
357 }
358
359 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
361 sending::send_dm(to_npub, content, None, &SendConfig::default(), Arc::new(NoOpSendCallback)).await
362 .map_err(|e| VectorError::Other(e))
363 }
364
365 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
367 sending::send_dm(to_npub, content, Some(replied_to), &SendConfig::default(), Arc::new(NoOpSendCallback)).await
368 .map_err(|e| VectorError::Other(e))
369 }
370
371 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
375 use futures_util::StreamExt;
376 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
377 if attachment.url.is_empty() {
378 return Err(VectorError::Other("attachment has no URL".into()));
379 }
380 crate::net::validate_url_not_private(&attachment.url)
384 .map_err(|e| VectorError::Other(e.to_string()))?;
385 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
386 let resp = client.get(&attachment.url).send().await
387 .map_err(|e| VectorError::Other(format!("download: {e}")))?;
388 if !resp.status().is_success() {
389 return Err(VectorError::Other(format!("download failed: HTTP {}", resp.status())));
390 }
391 let mut encrypted: Vec<u8> = Vec::with_capacity(
393 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
394 );
395 let mut stream = resp.bytes_stream();
396 while let Some(chunk) = stream.next().await {
397 let chunk = chunk.map_err(|e| VectorError::Other(format!("read body: {e}")))?;
398 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
399 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
400 }
401 encrypted.extend_from_slice(&chunk);
402 }
403 crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce).map_err(VectorError::Other)
404 }
405
406 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
408 let path = std::path::Path::new(file_path);
409 let bytes = std::fs::read(path)
410 .map_err(|e| VectorError::Io(e))?;
411 let filename = path.file_name()
412 .and_then(|n| n.to_str())
413 .unwrap_or("file");
414 let extension = path.extension()
415 .and_then(|e| e.to_str())
416 .unwrap_or("bin");
417
418 sending::send_file_dm(
419 to_npub,
420 std::sync::Arc::new(bytes),
421 filename,
422 extension,
423 None,
424 &SendConfig::default(),
425 Arc::new(NoOpSendCallback),
426 ).await.map_err(|e| VectorError::Other(e))
427 }
428
429 pub async fn send_reaction(
434 &self,
435 to_npub: &str,
436 reference_id: &str,
437 emoji: &str,
438 emoji_url: Option<&str>,
439 ) -> Result<String> {
440 use nostr_sdk::prelude::*;
441
442 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
443 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
444
445 let reference_event = EventId::from_hex(reference_id)
446 .map_err(|e| VectorError::Nostr(e.to_string()))?;
447 let receiver_pubkey = PublicKey::from_bech32(to_npub)
448 .map_err(|e| VectorError::Nostr(e.to_string()))?;
449
450 let custom_emoji_tag = emoji_url.and_then(|url| {
452 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
453 return None;
454 }
455 let shortcode = &emoji[1..emoji.len() - 1];
456 if shortcode.is_empty() { return None; }
457 Some(Tag::custom(TagKind::custom("emoji"), [shortcode.to_string(), url.to_string()]))
458 });
459
460 let reaction_target = nostr_sdk::nips::nip25::ReactionTarget {
461 event_id: reference_event,
462 public_key: receiver_pubkey,
463 coordinate: None,
464 kind: Some(Kind::PrivateDirectMessage),
465 relay_hint: None,
466 };
467 let mut builder = EventBuilder::reaction(reaction_target, emoji);
468 if let Some(tag) = custom_emoji_tag {
469 builder = builder.tag(tag);
470 }
471 let rumor = builder.build(my_public_key);
472 let rumor_id = rumor.id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
473
474 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
475 .await.map_err(VectorError::Other)?;
476
477 let self_wrap_client = client.clone();
479 let self_wrap_session = state::SessionGuard::capture();
480 tokio::spawn(async move {
481 if !self_wrap_session.is_valid() { return; }
482 let _ = self_wrap_client.gift_wrap(&my_public_key, rumor, []).await;
483 });
484
485 let reaction = Reaction {
487 id: rumor_id.clone(),
488 reference_id: reference_id.to_string(),
489 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
490 emoji: emoji.to_string(),
491 emoji_url: emoji_url.map(|s| s.to_string()),
492 };
493 let msg_for_save = {
494 let mut st = state::STATE.lock().await;
495 match st.add_reaction_to_message(reference_id, reaction) {
496 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
497 _ => None,
498 }
499 };
500 if let Some((cid, msg)) = msg_for_save {
501 let _ = db::events::save_message(&cid, &msg).await;
502 traits::emit_event_json("message_update", serde_json::json!({
503 "old_id": reference_id, "message": &msg, "chat_id": &cid
504 }));
505 }
506
507 Ok(rumor_id)
508 }
509
510 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
513 use nostr_sdk::prelude::*;
514
515 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
516 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
517 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
518
519 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
520 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
521 .tag(Tag::public_key(pubkey))
522 .tag(Tag::custom(TagKind::d(), vec!["vector"]))
523 .tag(Tag::expiration(expiry))
524 .build(my_public_key);
525
526 client.gift_wrap_to(
527 state::active_trusted_relays().await,
528 &pubkey,
529 rumor,
530 [Tag::expiration(expiry)],
531 ).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
532 Ok(())
533 }
534
535 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
539 use nostr_sdk::prelude::*;
540
541 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
542 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
543 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
544 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
545 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
546
547 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
549
550 let mut builder = EventBuilder::new(
551 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
552 new_content,
553 ).tag(Tag::event(reference_event));
554 for et in &emoji_tags {
555 builder = builder.tag(Tag::custom(
556 TagKind::custom("emoji"),
557 [et.shortcode.clone(), et.url.clone()],
558 ));
559 }
560 let rumor = builder.build(my_public_key);
561 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
562 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
563
564 let msg_for_emit = {
566 let mut st = state::STATE.lock().await;
567 st.update_message_in_chat(to_npub, message_id, |msg| {
568 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
569 msg.preview_metadata = None;
570 })
571 };
572 if let Some(msg) = msg_for_emit {
573 traits::emit_event_json("message_update", serde_json::json!({
574 "old_id": message_id, "message": &msg, "chat_id": to_npub
575 }));
576 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
577 let _ = db::events::save_edit_event(
578 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
579 ).await;
580 }
581 }
582
583 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
584 .await.map_err(VectorError::Other)?;
585
586 let self_wrap_client = client.clone();
587 let self_wrap_session = state::SessionGuard::capture();
588 tokio::spawn(async move {
589 if !self_wrap_session.is_valid() { return; }
590 let _ = self_wrap_client.gift_wrap(&my_public_key, rumor, []).await;
591 });
592
593 Ok(edit_id)
594 }
595
596 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
598 use nostr_sdk::prelude::*;
599 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
600 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
601 }
602
603 pub async fn get_chats(&self) -> Vec<SerializableChat> {
605 let state = state::STATE.lock().await;
606 state.chats.iter()
607 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
608 .collect()
609 }
610
611 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
613 let state = state::STATE.lock().await;
614 if let Some(chat) = state.get_chat(chat_id) {
615 let msgs = chat.get_all_messages(&state.interner);
616 let start = offset.min(msgs.len());
617 let end = (offset + limit).min(msgs.len());
618 msgs[start..end].to_vec()
619 } else {
620 Vec::new()
621 }
622 }
623
624 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
626 let state = state::STATE.lock().await;
627 state.get_profile(npub)
628 .map(|p| SlimProfile::from_profile(p, &state.interner))
629 }
630
631 pub async fn load_profile(&self, npub: &str) -> bool {
633 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
634 }
635
636 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
638 profile::sync::update_profile(
639 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
640 &NoOpProfileSyncHandler,
641 ).await
642 }
643
644 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
647 profile::sync::update_bot_profile(
648 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
649 &NoOpProfileSyncHandler,
650 ).await
651 }
652
653 pub async fn update_status(&self, status: &str) -> bool {
655 profile::sync::update_status(status.to_string()).await
656 }
657
658 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
664 let path = std::path::Path::new(file_path);
665 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
666 if bytes.is_empty() {
667 return Err(VectorError::Other("Empty image file".into()));
668 }
669 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
670 let mime = crate::crypto::mime_from_extension(&extension);
671 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
672 let signer = client
673 .signer()
674 .await
675 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
676 let servers = crate::blossom_servers::compute_enabled_servers();
677 if servers.is_empty() {
678 return Err(VectorError::Other("No Blossom servers configured".into()));
679 }
680 crate::blossom::upload_blob_with_failover(signer, servers, std::sync::Arc::new(bytes), Some(mime))
681 .await
682 .map_err(VectorError::Other)
683 }
684
685 pub async fn block_user(&self, npub: &str) -> bool {
687 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
688 }
689
690 pub async fn unblock_user(&self, npub: &str) -> bool {
692 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
693 }
694
695 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
697 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
698 }
699
700 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
702 profile::sync::get_blocked_users().await
703 }
704
705 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
707 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
708 }
709
710 pub fn my_npub(&self) -> Option<String> {
712 state::my_public_key()
713 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
714 }
715
716 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
723 let ids = crate::db::community::list_community_ids().unwrap_or_default();
724 let mut out = Vec::new();
725 for id in ids {
726 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
727 out.push(serde_json::json!({
728 "community_id": c.id.to_hex(),
729 "name": c.name,
730 "description": c.description,
731 "is_owner": crate::community::service::is_proven_owner(&c),
732 "channels": c.channels.iter()
733 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
734 .collect::<Vec<_>>(),
735 }));
736 }
737 }
738 out
739 }
740
741 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
745 use crate::community::{public_invite, service, transport::LiveTransport};
746 let (relays, token) = public_invite::parse_invite_url(invite_url)
747 .map_err(|e| VectorError::Other(e.to_string()))?;
748 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
749 let bundle = service::fetch_public_invite(&transport, &relays, &token)
750 .await
751 .map_err(VectorError::Other)?;
752 let now = std::time::SystemTime::now()
753 .duration_since(std::time::UNIX_EPOCH)
754 .map(|d| d.as_secs())
755 .unwrap_or(0);
756 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
757 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
760 self.finalize_member_join(community, &transport, attribution).await
761 }
762
763 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
766 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
767 Ok(rows.iter().map(|p| {
768 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
769 .ok().map(|i| i.name).unwrap_or_default();
770 serde_json::json!({
771 "community_id": p.community_id,
772 "name": name,
773 "inviter_npub": p.inviter_npub,
774 })
775 }).collect())
776 }
777
778 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
782 use crate::community::{invite::{CommunityInvite, accept_invite}, transport::LiveTransport};
783 let bundle_json = crate::db::community::get_pending_invite(community_id)
784 .map_err(VectorError::Other)?
785 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
786 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
787 let community = accept_invite(&invite).map_err(VectorError::Other)?;
788 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
789 let summary = self.finalize_member_join(community, &transport, None).await?;
791 let _ = crate::db::community::delete_pending_invite(community_id);
792 Ok(summary)
793 }
794
795 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
800 &self,
801 community: crate::community::Community,
802 transport: &T,
803 attribution: Option<(String, Option<String>)>,
804 ) -> Result<serde_json::Value> {
805 use crate::community::service;
806 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
810 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
813 if c.removed {
814 let _ = crate::db::community::delete_community(&community.id.to_hex());
815 return Err(VectorError::Other("you have been removed from this community".into()));
816 }
817 }
818 let community = crate::db::community::load_community(&community.id)
819 .map_err(VectorError::Other)?
820 .unwrap_or(community);
821 let _ = service::fetch_and_apply_control(transport, &community).await;
825 if service::am_i_banned(&community) {
826 let _ = crate::db::community::delete_community(&community.id.to_hex());
827 return Err(VectorError::Other("you are banned from this community".into()));
828 }
829 let community = crate::db::community::load_community(&community.id)
831 .map_err(VectorError::Other)?
832 .unwrap_or(community);
833 let owner_npub = community
834 .owner_attestation
835 .as_ref()
836 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
837 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
838 {
839 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
840 let mut st = state::STATE.lock().await;
841 for ch in &community.channels {
842 st.upsert_community_chat(
843 &ch.id.to_hex(),
844 &community.name,
845 community.description.as_deref().unwrap_or(""),
846 &community.id.to_hex(),
847 crate::community::service::is_proven_owner(&community),
848 community.icon.is_some(),
849 owner_npub.as_deref(),
850 created_at_ms,
851 community.dissolved,
852 );
853 }
854 }
855 if let Some(primary) = community.channels.first() {
858 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
859 }
860 Ok(serde_json::json!({
861 "community_id": community.id.to_hex(),
862 "name": community.name,
863 "channels": community.channels.iter()
864 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
865 .collect::<Vec<_>>(),
866 }))
867 }
868
869 pub async fn create_community(&self, name: &str) -> Result<serde_json::Value> {
873 use crate::community::{service, transport::LiveTransport};
874 let relays: Vec<String> = crate::state::active_trusted_relays()
875 .await
876 .iter()
877 .map(|s| s.to_string())
878 .collect();
879 if relays.is_empty() {
880 return Err(VectorError::Other("no relays available to host the Community".into()));
881 }
882 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
883 let community = service::create_community(&transport, name, "general", relays)
884 .await
885 .map_err(VectorError::Other)?;
886 let owner_npub = community
887 .owner_attestation
888 .as_ref()
889 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
890 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
891 {
892 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
893 let mut st = state::STATE.lock().await;
894 for ch in &community.channels {
895 st.upsert_community_chat(
896 &ch.id.to_hex(),
897 &community.name,
898 community.description.as_deref().unwrap_or(""),
899 &community.id.to_hex(),
900 crate::community::service::is_proven_owner(&community),
901 community.icon.is_some(),
902 owner_npub.as_deref(),
903 created_at_ms,
904 community.dissolved,
905 );
906 }
907 }
908 Ok(serde_json::json!({
909 "community_id": community.id.to_hex(),
910 "name": community.name,
911 "channels": community.channels.iter()
912 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
913 .collect::<Vec<_>>(),
914 }))
915 }
916
917 pub async fn create_public_invite(&self, community_id: &str) -> Result<String> {
919 use crate::community::{service, transport::LiveTransport, CommunityId};
920 if community_id.len() != 64 {
921 return Err(VectorError::Other("malformed community id".into()));
922 }
923 let community = crate::db::community::load_community(&CommunityId(
924 crate::simd::hex::hex_to_bytes_32(community_id),
925 ))
926 .map_err(VectorError::Other)?
927 .ok_or_else(|| VectorError::Other("community not found".into()))?;
928 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
929 let (_token, url) = service::create_public_invite(&transport, &community, None, None)
930 .await
931 .map_err(VectorError::Other)?;
932 Ok(url)
933 }
934
935 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
939 use crate::community::{service, CommunityId};
940 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
941
942 let session = crate::state::SessionGuard::capture();
943 let my_pk = crate::state::my_public_key()
944 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
945
946 if community_id.len() != 64 {
947 return Err(VectorError::Other("malformed community id".into()));
948 }
949 let community = crate::db::community::load_community(&CommunityId(
950 crate::simd::hex::hex_to_bytes_32(community_id),
951 ))
952 .map_err(VectorError::Other)?
953 .ok_or_else(|| VectorError::Other("community not found".into()))?;
954
955 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
956 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
957 }
958 let invitee_hex = nostr_sdk::PublicKey::parse(invitee_npub)
959 .map_err(|_| VectorError::Other("invalid npub".into()))?
960 .to_hex();
961 if crate::db::community::get_community_banlist(community_id)
962 .map_err(VectorError::Other)?
963 .iter()
964 .any(|b| b == &invitee_hex)
965 {
966 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
967 }
968
969 if !session.is_valid() {
971 return Err(VectorError::Other("account changed during invite".into()));
972 }
973
974 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk).map_err(VectorError::Other)?;
975 let pending_id = format!("community-invite-{}", community_id);
976 let config = SendConfig { self_send: false, ..SendConfig::gui() };
978 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
979
980 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
981 .await
982 .map_err(VectorError::Other)?;
983
984 Ok(serde_json::json!({
985 "community_id": community_id,
986 "invitee": invitee_npub,
987 "wrap_event_id": result.event_id,
988 }))
989 }
990
991 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
994 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
995 }
996
997 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1002 use crate::community::{service, transport::LiveTransport, CommunityId};
1003 if community_id.len() != 64 {
1004 return Err(VectorError::Other("malformed community id".into()));
1005 }
1006 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1007 let community = crate::db::community::load_community(&CommunityId(
1008 crate::simd::hex::hex_to_bytes_32(community_id),
1009 ))
1010 .map_err(VectorError::Other)?
1011 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1012 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1013 service::revoke_public_invite(&transport, &community, &token_bytes)
1014 .await
1015 .map_err(VectorError::Other)
1016 }
1017
1018 pub async fn send_community_message(
1020 &self,
1021 channel_id: &str,
1022 content: &str,
1023 replied_to: Option<&str>,
1024 ) -> Result<String> {
1025 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1026 let (community, channel) = self.resolve_channel(channel_id)?;
1027 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1028 let reply = replied_to.filter(|r| !r.is_empty());
1029 let ms = std::time::SystemTime::now()
1030 .duration_since(std::time::UNIX_EPOCH)
1031 .map(|d| d.as_millis() as u64)
1032 .unwrap_or(0);
1033 let unsigned = envelope::build_inner_typed(
1034 author_pk,
1035 &channel.id,
1036 channel.epoch,
1037 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1038 content,
1039 ms,
1040 reply,
1041 &[],
1042 );
1043 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1044 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1045 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1046 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1047 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1048 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1049 .await
1050 .map_err(VectorError::Other)?;
1051 let echoed = {
1053 let mut st = state::STATE.lock().await;
1054 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1055 };
1056 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1057 let _ = crate::db::events::save_message(channel_id, &msg).await;
1058 }
1059 Ok(message_id)
1060 }
1061
1062 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1066 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1067 let path = std::path::Path::new(file_path);
1068 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1069 if bytes.is_empty() {
1070 return Err(VectorError::Other("Empty file".into()));
1071 }
1072 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1073 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1074
1075 let (community, channel) = self.resolve_channel(channel_id)?;
1076 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1077
1078 let file_hash = crate::crypto::sha256_hex(&bytes);
1079 let mime = crate::crypto::mime_from_extension(&extension);
1080 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1081
1082 let download_dir = crate::db::get_download_dir();
1084 let _ = std::fs::create_dir_all(&download_dir);
1085 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1086 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1087 let _ = std::fs::write(&local_path, &bytes);
1088
1089 let params = crate::crypto::generate_encryption_params();
1091 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1092 let encrypted_size = encrypted.len() as u64;
1093
1094 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1095 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1096 let servers = crate::blossom_servers::compute_enabled_servers();
1097 if servers.is_empty() {
1098 return Err(VectorError::Other("No Blossom servers configured".into()));
1099 }
1100 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1101 let url = crate::blossom::upload_blob_with_progress_and_failover(
1102 signer.clone(),
1103 servers,
1104 std::sync::Arc::new(encrypted),
1105 Some(mime),
1106 true,
1107 noop_progress,
1108 Some(3),
1109 Some(std::time::Duration::from_secs(2)),
1110 None,
1111 ).await.map_err(VectorError::Other)?;
1112
1113 let attachment = crate::types::Attachment {
1114 id: file_hash.clone(),
1115 key: params.key.clone(),
1116 nonce: params.nonce.clone(),
1117 extension: extension.clone(),
1118 name: filename.clone(),
1119 url,
1120 path: local_path.to_string_lossy().to_string(),
1121 size: encrypted_size,
1122 img_meta,
1123 downloading: false,
1124 downloaded: true,
1125 ..Default::default()
1126 };
1127 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
1128 let ms = std::time::SystemTime::now()
1129 .duration_since(std::time::UNIX_EPOCH)
1130 .map(|d| d.as_millis() as u64)
1131 .unwrap_or(0);
1132 let unsigned = envelope::build_inner_full(
1133 author_pk, &channel.id, channel.epoch,
1134 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
1135 );
1136 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1137 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1138 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1139 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1140 .await.map_err(VectorError::Other)?;
1141 let echoed = {
1143 let mut st = state::STATE.lock().await;
1144 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1145 };
1146 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
1147 let _ = crate::db::events::save_message(channel_id, &m).await;
1148 }
1149 Ok(message_id)
1150 }
1151
1152 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
1154 use crate::community::{service, transport::LiveTransport};
1155 let (community, channel) = self.resolve_channel(channel_id)?;
1156 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1157 service::publish_typing_signal(&transport, &community, &channel)
1158 .await
1159 .map_err(VectorError::Other)
1160 }
1161
1162 pub async fn send_community_reaction(
1165 &self,
1166 channel_id: &str,
1167 message_id: &str,
1168 emoji: &str,
1169 emoji_url: Option<&str>,
1170 ) -> Result<()> {
1171 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
1172 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
1173 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
1174 }
1175 _ => Vec::new(),
1176 };
1177 self.publish_community_control(
1178 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
1179 ).await
1180 }
1181
1182 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
1184 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
1185 self.publish_community_control(
1186 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
1187 ).await
1188 }
1189
1190 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
1193 use crate::community::{service, transport::LiveTransport};
1194 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1195
1196 let (channel_id, attachment_urls) = {
1197 let st = state::STATE.lock().await;
1198 match st.find_message(message_id) {
1199 Some((chat, msg)) => (
1200 chat.id.clone(),
1201 msg.attachments.iter().filter(|a| !a.url.is_empty()).map(|a| a.url.clone()).collect::<Vec<_>>(),
1202 ),
1203 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
1204 }
1205 };
1206
1207 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
1209 let _ = service::delete_message(&transport, message_id).await;
1210 }
1211 self.publish_community_control(
1213 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
1214 ).await?;
1215 if !attachment_urls.is_empty() {
1217 if let Some(client) = state::nostr_client() {
1218 if let Ok(signer) = client.signer().await {
1219 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
1220 }
1221 }
1222 }
1223 let removed_chat = {
1225 let mut st = state::STATE.lock().await;
1226 st.remove_message(message_id).map(|(cid, _)| cid)
1227 };
1228 let _ = crate::db::events::delete_event(message_id).await;
1229 traits::emit_event_json("message_removed", serde_json::json!({
1230 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
1231 }));
1232 Ok(())
1233 }
1234
1235 async fn publish_community_control(
1238 &self,
1239 channel_id: &str,
1240 kind: u16,
1241 content: &str,
1242 target: &str,
1243 emoji_tags: &[crate::types::EmojiTag],
1244 ) -> Result<()> {
1245 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1246 let (community, channel) = self.resolve_channel(channel_id)?;
1247 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1248 let ms = std::time::SystemTime::now()
1249 .duration_since(std::time::UNIX_EPOCH)
1250 .map(|d| d.as_millis() as u64)
1251 .unwrap_or(0);
1252 let unsigned = envelope::build_inner_typed(
1253 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
1254 );
1255 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1256 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1257 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1258 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1259 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1260 .await.map_err(VectorError::Other)?;
1261 let outcome = {
1263 let mut st = state::STATE.lock().await;
1264 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1265 };
1266 if let Some(inbound::IncomingEvent::Updated { target_id, message, edit_event }) = outcome {
1267 if let Some(ev) = edit_event {
1268 let mut ev = (*ev).clone();
1269 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
1270 let _ = crate::db::events::save_event(&ev).await;
1271 } else {
1272 let _ = crate::db::events::save_message(channel_id, &message).await;
1273 }
1274 traits::emit_event_json("message_update", serde_json::json!({
1275 "old_id": target_id, "message": &message, "chat_id": channel_id,
1276 }));
1277 }
1278 Ok(())
1279 }
1280
1281 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
1287 use crate::community::{inbound, send, service, transport::LiveTransport};
1288 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1289 let (community, _) = self.resolve_channel(channel_id)?;
1290 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1291 let mut warnings: Vec<String> = Vec::new();
1292
1293 match service::catch_up_server_root(&transport, &community).await {
1301 Ok(c) if c.removed => {
1302 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1304 return Ok((0, warnings));
1305 }
1306 Ok(_) => {}
1307 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
1308 }
1309 let (community, _) = self.resolve_channel(channel_id)?;
1310
1311 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
1317 warnings.push(format!("control fold failed: {e}"));
1318 }
1319 if service::am_i_banned(&community) {
1320 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1322 return Ok((0, warnings));
1323 }
1324 let (community, channel) = self.resolve_channel(channel_id)?;
1327 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
1328 warnings.push(format!("channel catch-up failed: {e}"));
1329 }
1330 let (community, _) = self.resolve_channel(channel_id)?;
1334 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
1335 warnings.push(format!("read-cut resume failed: {e}"));
1336 }
1337 let (community, channel) = self.resolve_channel(channel_id)?;
1338
1339 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
1340 .await
1341 .map_err(VectorError::Other)?;
1342 let outcomes = {
1343 let mut st = state::STATE.lock().await;
1344 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
1345 };
1346 let mut new = 0usize;
1347 for o in &outcomes {
1348 match o {
1349 inbound::IncomingEvent::NewMessage(m) => {
1350 let _ = crate::db::events::save_message(channel_id, m).await;
1351 new += 1;
1352 }
1353 inbound::IncomingEvent::Updated { message, .. } => {
1354 let _ = crate::db::events::save_message(channel_id, message).await;
1355 }
1356 inbound::IncomingEvent::Removed { target_id } => {
1357 let _ = crate::db::events::delete_event(target_id).await;
1358 }
1359 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
1360 let et = if *joined {
1361 crate::stored_event::SystemEventType::MemberJoined
1362 } else {
1363 crate::stored_event::SystemEventType::MemberLeft
1364 };
1365 let note = invited_by.as_ref().map(|by| match invited_label {
1367 Some(l) if !l.is_empty() => format!("{by}|{l}"),
1368 _ => by.clone(),
1369 });
1370 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;
1371 }
1372 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
1373 community::service::persist_webxdc_signal(
1376 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
1377 ).await;
1378 }
1379 inbound::IncomingEvent::Kicked { community_id }
1380 | inbound::IncomingEvent::SelfLeft { community_id } => {
1381 let _ = crate::db::community::delete_community_retain_keys(community_id);
1386 break;
1387 }
1388 inbound::IncomingEvent::Typing { .. } => {
1389 }
1391 }
1392 }
1393 Ok((new, warnings))
1394 }
1395
1396 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
1399 crate::db::community::community_member_activity(community_id)
1400 .unwrap_or_default()
1401 .into_iter()
1402 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
1403 .collect()
1404 }
1405
1406 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
1411 use crate::community::CommunityId;
1412 if community_id.len() != 64 {
1413 return Err(VectorError::Other("malformed community id".into()));
1414 }
1415 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
1416 .map_err(VectorError::Other)?
1417 .ok_or_else(|| VectorError::Other("community not found".into()))
1418 }
1419
1420 fn admin_role_id_of(community_id: &str) -> Result<String> {
1421 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
1422 roles.roles.iter()
1423 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
1424 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
1425 .map(|r| r.role_id.clone())
1426 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
1427 }
1428
1429 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
1432 use crate::community::service;
1433 let community = Self::load_community_hex(community_id)?;
1434 let caps = service::caller_capabilities(&community);
1435 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
1436 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
1437 .unwrap_or(false);
1438 Ok(serde_json::json!({
1439 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
1440 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
1441 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
1442 "manage_admin_role": manage_admin_role,
1443 }))
1444 }
1445
1446 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
1448 use nostr_sdk::prelude::{PublicKey, ToBech32};
1449 let community = Self::load_community_hex(community_id)?;
1450 let owner = community.owner_attestation.as_ref()
1451 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1452 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1453 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
1454 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
1455 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1456 .collect();
1457 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
1458 }
1459
1460 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
1462 use crate::community::{service, transport::LiveTransport};
1463 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
1464 let community = Self::load_community_hex(community_id)?;
1465 let role_id = Self::admin_role_id_of(community_id)?;
1466 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1467 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
1468 }
1469
1470 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
1472 use crate::community::{service, transport::LiveTransport};
1473 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
1474 let community = Self::load_community_hex(community_id)?;
1475 let role_id = Self::admin_role_id_of(community_id)?;
1476 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1477 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
1478 }
1479
1480 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
1482 use crate::community::{service, transport::LiveTransport};
1483 let hex = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?.to_hex();
1484 let community = Self::load_community_hex(community_id)?;
1485 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
1486 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1487 service::publish_kick(&transport, &community, channel, &hex).await.map(|_| ()).map_err(VectorError::Other)
1488 }
1489
1490 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
1493 use crate::community::{service, transport::LiveTransport};
1494 let hex = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?.to_hex();
1495 let community = Self::load_community_hex(community_id)?;
1496 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
1498 list.retain(|h| h != &hex);
1499 if banned { list.push(hex); }
1500 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1501 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
1502 }
1503
1504 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
1508 use crate::community::{service, transport::LiveTransport};
1509 let community = Self::load_community_hex(community_id)?;
1510 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1511 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
1512 }
1513
1514 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
1517 use crate::community::{service, transport::LiveTransport};
1518 let mut community = Self::load_community_hex(community_id)?;
1519 if let Some(n) = name { community.name = n.to_string(); }
1520 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
1521 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1522 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
1523 }
1524
1525 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
1528 use crate::community::{transport::LiveTransport, CommunityId};
1529 if community_id.len() != 64 {
1530 return Err(VectorError::Other("malformed community id".into()));
1531 }
1532 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1533 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
1534 let channel_ids: Vec<String> = community
1535 .as_ref()
1536 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
1537 .unwrap_or_default();
1538 if let Some(ref c) = community {
1540 if let Some(primary) = c.channels.first() {
1541 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1542 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
1543 }
1544 }
1545 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
1547 {
1548 let mut st = state::STATE.lock().await;
1549 st.chats.retain(|c| !channel_ids.contains(&c.id));
1550 }
1551 Ok(())
1552 }
1553
1554 fn resolve_channel(
1556 &self,
1557 channel_id: &str,
1558 ) -> Result<(crate::community::Community, crate::community::Channel)> {
1559 use crate::community::CommunityId;
1560 let community_id = crate::db::community::community_id_for_channel(channel_id)
1561 .map_err(VectorError::Other)?
1562 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
1563 if community_id.len() != 64 {
1564 return Err(VectorError::Other("malformed community id".into()));
1565 }
1566 let community = crate::db::community::load_community(&CommunityId(
1567 crate::simd::hex::hex_to_bytes_32(&community_id),
1568 ))
1569 .map_err(VectorError::Other)?
1570 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
1571 let channel = community
1572 .channels
1573 .iter()
1574 .find(|c| c.id.to_hex() == channel_id)
1575 .cloned()
1576 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
1577 Ok((community, channel))
1578 }
1579
1580
1581 pub async fn sync_dms(
1598 &self,
1599 since_days: Option<u64>,
1600 handler: &dyn InboundEventHandler,
1601 ) -> Result<(u32, u32)> {
1602 use futures_util::StreamExt;
1603 use nostr_sdk::prelude::*;
1604
1605 let client = state::nostr_client()
1606 .ok_or(VectorError::Other("Not connected".into()))?;
1607 let my_pk = state::my_public_key()
1608 .ok_or(VectorError::Other("Not logged in".into()))?;
1609
1610 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
1612
1613 let (items, filter) = if let Some(days) = since_days {
1615 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
1616 let items: Vec<(EventId, Timestamp)> = all_items.iter()
1617 .filter(|(_, ts)| ts.as_secs() >= since_ts)
1618 .cloned()
1619 .collect();
1620 let filter = Filter::new()
1621 .pubkey(my_pk)
1622 .kind(Kind::GiftWrap)
1623 .since(Timestamp::from_secs(since_ts));
1624 (items, filter)
1625 } else {
1626 let filter = Filter::new()
1627 .pubkey(my_pk)
1628 .kind(Kind::GiftWrap);
1629 (all_items, filter)
1630 };
1631
1632 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
1633
1634 let sync_opts = nostr_sdk::SyncOptions::new()
1636 .direction(nostr_sdk::SyncDirection::Down)
1637 .initial_timeout(std::time::Duration::from_secs(10))
1638 .dry_run();
1639
1640 let relay_map = client.relays().await;
1642 let all_relays: Vec<(RelayUrl, Relay)> = relay_map.iter()
1643 .map(|(url, relay)| (url.clone(), relay.clone()))
1644 .collect();
1645 drop(relay_map);
1646
1647 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
1648 for (url, relay) in &all_relays {
1649 let url = url.clone();
1650 let relay = relay.clone();
1651 let f = filter.clone();
1652 let i = items.clone();
1653 let o = sync_opts.clone();
1654 relay_futs.push(async move {
1655 let result = tokio::time::timeout(
1656 std::time::Duration::from_secs(10),
1657 relay.sync_with_items(f, i, &o),
1658 ).await;
1659 (url, result)
1660 });
1661 }
1662
1663 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1665 while let Some((url, result)) = relay_futs.next().await {
1666 match result {
1667 Ok(Ok(recon)) => {
1668 let count = recon.remote.len();
1669 all_missing.extend(recon.remote);
1670 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
1671 }
1672 Ok(Err(e)) => log_warn!("[SyncDMs] {} failed: {}", url, e),
1673 Err(_) => log_warn!("[SyncDMs] {} timed out (10s)", url),
1674 }
1675 }
1676
1677 if all_missing.is_empty() {
1678 log_info!("[SyncDMs] No missing events");
1679 return Ok((0, 0));
1680 }
1681
1682 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
1684 let ids: Vec<EventId> = all_missing.into_iter().collect();
1685 let relay_strs: Vec<String> = client.relays().await.keys()
1686 .map(|u| u.to_string()).collect();
1687
1688 let mut total_events = 0u32;
1689 let mut new_messages = 0u32;
1690 const BATCH_SIZE: usize = 500;
1691
1692 for batch in ids.chunks(BATCH_SIZE) {
1693 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap);
1694 match client.stream_events_from(
1695 relay_strs.clone(), f,
1696 std::time::Duration::from_secs(30),
1697 ).await {
1698 Ok(stream) => {
1699 let client_clone = client.clone();
1700 let prepared_stream = stream
1701 .map(move |event| {
1702 let c = client_clone.clone();
1703 tokio::spawn(async move {
1704 event_handler::prepare_event(event, &c, my_pk).await
1705 })
1706 })
1707 .buffer_unordered(8);
1708 tokio::pin!(prepared_stream);
1709
1710 while let Some(result) = prepared_stream.next().await {
1711 total_events += 1;
1712 if let Ok(prepared) = result {
1713 if event_handler::commit_prepared_event(prepared, false, handler).await {
1714 new_messages += 1;
1715 }
1716 }
1717 }
1718 }
1719 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
1720 }
1721 }
1722
1723 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
1724 Ok((total_events, new_messages))
1725 }
1726
1727 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::SubscriptionId> {
1736 use nostr_sdk::prelude::*;
1737 let client = state::nostr_client()
1738 .ok_or(VectorError::Other("Not connected".into()))?;
1739 let my_pk = state::my_public_key()
1740 .ok_or(VectorError::Other("Not logged in".into()))?;
1741
1742 let filter = Filter::new()
1743 .pubkey(my_pk)
1744 .kind(Kind::GiftWrap)
1745 .limit(0);
1746
1747 let output = client.subscribe(filter, None).await
1748 .map_err(|e| VectorError::Nostr(e.to_string()))?;
1749 Ok(output.val)
1750 }
1751
1752 pub async fn sync_communities(&self) -> Result<()> {
1757 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
1758 for id in ids {
1759 if let Ok(Some(community)) = db::community::load_community(&id) {
1760 for ch in &community.channels {
1761 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
1762 }
1763 }
1764 }
1765 Ok(())
1766 }
1767
1768 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
1800 use nostr_sdk::prelude::*;
1801
1802 let client = state::nostr_client()
1803 .ok_or(VectorError::Other("Not connected".into()))?;
1804 let my_pk = state::my_public_key()
1805 .ok_or(VectorError::Other("Not logged in".into()))?;
1806
1807 let _ = self.sync_communities().await;
1814 let _ = self.sync_dms(None, &NoOpEventHandler).await;
1815
1816 let dm_sub_id = self.subscribe_dms().await?;
1819 community::realtime::refresh_subscription(&client).await;
1820
1821 if let Some(monitor) = client.monitor() {
1828 let mut rx = monitor.subscribe();
1829 let session = state::SessionGuard::capture();
1830 tokio::spawn(async move {
1831 let mut last_resync: Option<std::time::Instant> = None;
1834 while let Ok(notification) = rx.recv().await {
1835 if !session.is_valid() {
1836 return;
1837 }
1838 let MonitorNotification::StatusChanged { status, .. } = notification;
1839 if status == RelayStatus::Connected {
1840 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
1841 continue;
1842 }
1843 let _ = VectorCore.sync_communities().await;
1844 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
1845 if let Some(c) = state::nostr_client() {
1846 community::realtime::refresh_subscription(&c).await;
1847 }
1848 last_resync = Some(std::time::Instant::now());
1849 }
1850 }
1851 });
1852 }
1853
1854 {
1858 let client_health = client.clone();
1859 let session = state::SessionGuard::capture();
1860 tokio::spawn(async move {
1861 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
1863 if !session.is_valid() {
1864 return;
1865 }
1866 for (url, relay) in client_health.relays().await {
1867 match relay.status() {
1868 RelayStatus::Connected => {
1869 let probe = tokio::time::timeout(
1870 std::time::Duration::from_secs(10),
1871 client_health.fetch_events_from(
1872 vec![url.to_string()],
1873 Filter::new().kind(Kind::Metadata).limit(1),
1874 std::time::Duration::from_secs(8),
1875 ),
1876 )
1877 .await;
1878 if !matches!(probe, Ok(Ok(_))) {
1879 let _ = relay.disconnect();
1880 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1881 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
1882 }
1883 }
1884 RelayStatus::Terminated | RelayStatus::Disconnected => {
1885 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
1886 }
1887 _ => {}
1888 }
1889 }
1890 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
1891 }
1892 });
1893 }
1894
1895 let client_for_closure = client.clone();
1896
1897 client.handle_notifications(move |notification| {
1898 let handler = handler.clone();
1899 let c = client_for_closure.clone();
1900 let dm_sid = dm_sub_id.clone();
1901 async move {
1902 if let RelayPoolNotification::Event { event, subscription_id, .. } = notification {
1903 if subscription_id == dm_sid {
1904 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
1906 event_handler::commit_prepared_event(prepared, true, &*handler).await;
1907 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id) {
1908 let session = state::SessionGuard::capture();
1910 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
1911 }
1912 }
1913 Ok(false)
1914 }
1915 }).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
1916
1917 Ok(())
1918 }
1919
1920 pub async fn logout(&self) {
1922 if let Some(client) = state::nostr_client() {
1923 let _ = client.disconnect().await;
1924 }
1925 db::close_database();
1926 }
1927
1928 pub async fn swap_session(&self) {
1936 state::bump_session_generation();
1938
1939 if let Some(client) = state::take_nostr_client() {
1942 let _ = client.shutdown().await;
1943 }
1944 db::close_database();
1945
1946 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
1948 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
1949 {
1950 use zeroize::Zeroize;
1951 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
1952 if let Some(s) = g.as_mut() { s.zeroize(); }
1953 *g = None;
1954 }
1955 if let Ok(mut g) = state::PENDING_NSEC.lock() {
1956 if let Some(s) = g.as_mut() { s.zeroize(); }
1957 *g = None;
1958 }
1959 }
1960
1961 {
1963 let mut st = state::STATE.lock().await;
1964 st.profiles.clear();
1965 st.chats.clear();
1966 st.db_loaded = false;
1967 st.is_syncing = false;
1968 }
1969 state::WRAPPER_ID_CACHE.lock().await.clear();
1970 state::PENDING_EVENTS.lock().await.clear();
1971 state::set_active_chat(None);
1972 crate::profile::sync::clear_profile_sync_queue();
1973 crate::inbox_relays::clear_inbox_relay_cache();
1974 crate::emoji_packs::clear_nip65_cache();
1975 crate::db::clear_id_caches();
1979 crate::community::cache::clear();
1983 crate::community::realtime::clear().await;
1986 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
1990 }
1991}
1992
1993#[cfg(test)]
1994mod facade_tests {
1995 use super::*;
1996
1997 #[tokio::test]
2000 async fn download_attachment_rejects_private_url() {
2001 let att = crate::types::Attachment {
2002 url: "http://169.254.169.254/latest/meta-data/".to_string(),
2003 ..Default::default()
2004 };
2005 match VectorCore.download_attachment(&att).await {
2006 Err(VectorError::Other(msg)) => {
2007 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
2008 }
2009 other => panic!("expected SSRF rejection, got {other:?}"),
2010 }
2011 }
2012
2013 #[tokio::test]
2014 async fn download_attachment_rejects_empty_url() {
2015 let att = crate::types::Attachment::default();
2016 assert!(VectorCore.download_attachment(&att).await.is_err());
2017 }
2018}