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 webxdc;
71#[cfg(feature = "tor")]
72pub mod tor;
73
74pub fn nostr_client_options() -> nostr_sdk::ClientOptions {
86 let opts = nostr_sdk::ClientOptions::new().automatic_authentication(true);
93 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
94 {
95 match tor::transport_state() {
96 tor::TorTransportState::Active(addr) => {
97 return opts.connection(nostr_sdk::client::Connection::new().proxy(addr));
98 }
99 tor::TorTransportState::RequiredButInactive => {
100 return opts.connection(
103 nostr_sdk::client::Connection::new().proxy(tor::blackhole_proxy_addr()),
104 );
105 }
106 tor::TorTransportState::Disabled => {}
107 }
108 }
109 opts
110}
111
112pub fn tor_aware_relay_options(opts: nostr_sdk::RelayOptions) -> nostr_sdk::RelayOptions {
122 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
123 {
124 match tor::transport_state() {
125 tor::TorTransportState::Active(addr) => {
126 return opts.connection_mode(nostr_sdk::pool::ConnectionMode::proxy(addr));
127 }
128 tor::TorTransportState::RequiredButInactive => {
129 return opts.connection_mode(
132 nostr_sdk::pool::ConnectionMode::proxy(tor::blackhole_proxy_addr()),
133 );
134 }
135 tor::TorTransportState::Disabled => {}
136 }
137 }
138 opts
139}
140
141pub fn community_relay_options() -> nostr_sdk::RelayOptions {
151 use nostr_sdk::RelayServiceFlags;
152 tor_aware_relay_options(
153 nostr_sdk::RelayOptions::new().flags(RelayServiceFlags::GOSSIP | RelayServiceFlags::PING),
154 )
155}
156
157pub fn discovery_relay_options() -> nostr_sdk::RelayOptions {
163 community_relay_options()
164}
165
166pub mod stored_event;
168
169pub mod rumor;
171
172pub mod sending;
174
175pub mod wallpaper;
177
178pub mod deletion;
180
181pub mod simd;
183
184pub mod community;
186
187pub mod event_handler;
189
190pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
192pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
193pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
194pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
195pub use state::{
196 ChatState, NOSTR_CLIENT, MY_SECRET_KEY, MY_PUBLIC_KEY, STATE, ENCRYPTION_KEY,
197 nostr_client, my_public_key, has_active_session,
198 set_nostr_client, set_my_public_key,
199 take_nostr_client, clear_my_public_key,
200 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
201};
202pub use crypto::{GuardedKey, GuardedSigner};
203pub use signer::{
204 SignerKind, signer_kind, set_signer_kind, is_bunker,
205 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
206 build_bunker_signer, prewarm_bunker, drain_bunker_state,
207 parse_bunker_remote_pubkey, parse_bunker_relays,
208 BunkerConnectionState, bunker_state, set_bunker_state,
209 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
210 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
211 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
212};
213pub use error::{VectorError, Result};
214pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
215pub use db::{set_app_data_dir, get_app_data_dir};
216pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
217pub use deletion::{delete_own_dm, DeleteOutcome};
218pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
219pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
220pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
221pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
222
223use std::path::PathBuf;
224use std::sync::Arc;
225
226pub struct CoreConfig {
232 pub data_dir: PathBuf,
234 pub event_emitter: Option<Box<dyn EventEmitter>>,
236}
237
238#[derive(Clone, Copy)]
260pub struct VectorCore;
261
262impl VectorCore {
263 pub fn init(config: CoreConfig) -> Result<Self> {
265 db::set_app_data_dir(config.data_dir);
267
268 if let Some(emitter) = config.event_emitter {
270 traits::set_event_emitter(emitter);
271 }
272
273 let _ = rustls::crypto::ring::default_provider().install_default();
275
276 Ok(VectorCore)
277 }
278
279 pub fn accounts(&self) -> Result<Vec<String>> {
281 db::get_accounts().map_err(VectorError::from)
282 }
283
284 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
286 use nostr_sdk::prelude::*;
287
288 let keys = if key.starts_with("nsec1") {
290 let secret = SecretKey::from_bech32(key)
291 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
292 Keys::new(secret)
293 } else {
294 Keys::from_mnemonic(key, None)
296 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
297 };
298
299 let public_key = keys.public_key();
300 let npub = public_key.to_bech32()
301 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
302
303 let secret_bytes = keys.secret_key().to_secret_bytes();
305 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
306 state::set_my_public_key(public_key);
307
308 db::set_current_account(npub.clone())?;
310 db::init_database(&npub)?;
311
312 {
314 let nsec = keys.secret_key().to_bech32()
315 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
316 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
317
318 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
325 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
326 db::set_pkey(&nsec)?;
327 }
328 }
329
330 let has_encryption = state::resolve_encryption_enabled_from_db();
333
334 if has_encryption {
335 if let Some(pwd) = password {
336 let key = crate::crypto::hash_pass(pwd).await;
337 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
338 }
339 }
340 state::init_encryption_enabled();
343
344 let client = ClientBuilder::new()
347 .signer(keys)
348 .opts(nostr_client_options())
349 .monitor(Monitor::new(1024))
351 .build();
352
353 for relay in state::TRUSTED_RELAYS {
355 let opts = tor_aware_relay_options(nostr_sdk::RelayOptions::default());
356 client.pool().add_relay(*relay, opts).await.ok();
357 }
358
359 client.connect().await;
361
362 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
363
364 Ok(LoginResult { npub, has_encryption })
365 }
366
367 pub fn generate_nsec(&self) -> Result<String> {
370 use nostr_sdk::prelude::*;
371 Keys::generate().secret_key().to_bech32()
372 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
373 }
374
375 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
380 let config = SendConfig { self_send: false, ..SendConfig::headless() };
381 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
382 .map_err(|e| VectorError::Other(e))
383 }
384
385 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
387 let config = SendConfig { self_send: false, ..SendConfig::headless() };
388 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
389 .map_err(|e| VectorError::Other(e))
390 }
391
392 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
396 use futures_util::StreamExt;
397 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
398 if attachment.url.is_empty() {
399 return Err(VectorError::Other("attachment has no URL".into()));
400 }
401 crate::net::validate_url_not_private(&attachment.url)
405 .map_err(|e| VectorError::Other(e.to_string()))?;
406 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
407 let resp = client.get(&attachment.url).send().await
408 .map_err(|e| VectorError::Other(format!("download: {e}")))?;
409 if !resp.status().is_success() {
410 return Err(VectorError::Other(format!("download failed: HTTP {}", resp.status())));
411 }
412 let mut encrypted: Vec<u8> = Vec::with_capacity(
414 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
415 );
416 let mut stream = resp.bytes_stream();
417 while let Some(chunk) = stream.next().await {
418 let chunk = chunk.map_err(|e| VectorError::Other(format!("read body: {e}")))?;
419 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
420 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
421 }
422 encrypted.extend_from_slice(&chunk);
423 }
424 crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce).map_err(VectorError::Other)
425 }
426
427 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
429 let path = std::path::Path::new(file_path);
430 let bytes = std::fs::read(path)
431 .map_err(|e| VectorError::Io(e))?;
432 let filename = path.file_name()
433 .and_then(|n| n.to_str())
434 .unwrap_or("file");
435 let extension = path.extension()
436 .and_then(|e| e.to_str())
437 .unwrap_or("bin");
438
439 sending::send_file_dm(
440 to_npub,
441 std::sync::Arc::new(bytes),
442 filename,
443 extension,
444 None,
445 &SendConfig::default(),
446 Arc::new(NoOpSendCallback),
447 ).await.map_err(|e| VectorError::Other(e))
448 }
449
450 pub async fn send_reaction(
455 &self,
456 to_npub: &str,
457 reference_id: &str,
458 emoji: &str,
459 emoji_url: Option<&str>,
460 ) -> Result<String> {
461 use nostr_sdk::prelude::*;
462
463 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
464 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
465
466 let reference_event = EventId::from_hex(reference_id)
467 .map_err(|e| VectorError::Nostr(e.to_string()))?;
468 let receiver_pubkey = PublicKey::from_bech32(to_npub)
469 .map_err(|e| VectorError::Nostr(e.to_string()))?;
470
471 let custom_emoji_tag = emoji_url.and_then(|url| {
473 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
474 return None;
475 }
476 let shortcode = &emoji[1..emoji.len() - 1];
477 if shortcode.is_empty() { return None; }
478 Some(Tag::custom(TagKind::custom("emoji"), [shortcode.to_string(), url.to_string()]))
479 });
480
481 let reaction_target = nostr_sdk::nips::nip25::ReactionTarget {
482 event_id: reference_event,
483 public_key: receiver_pubkey,
484 coordinate: None,
485 kind: Some(Kind::PrivateDirectMessage),
486 relay_hint: None,
487 };
488 let mut builder = EventBuilder::reaction(reaction_target, emoji);
489 if let Some(tag) = custom_emoji_tag {
490 builder = builder.tag(tag);
491 }
492 let rumor = builder.build(my_public_key);
493 let inner_rumor_id = rumor.id;
494 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
495
496 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
500 .await.map_err(VectorError::Other)?;
501 if !outcome.output.success.is_empty() {
502 if let Some(rid) = inner_rumor_id {
503 if let Err(e) = db::nip17_keys::store_wrap_key(
504 &outcome.wrap_event_id, &rid, &receiver_pubkey,
505 db::nip17_keys::WrapRole::Recipient,
506 &outcome.wrap_secret, &outcome.targeted_relays,
507 ) {
508 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
509 }
510 }
511 }
512
513 let self_wrap_client = client.clone();
516 let self_wrap_session = state::SessionGuard::capture();
517 tokio::spawn(async move {
518 if !self_wrap_session.is_valid() { return; }
519 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
520 &self_wrap_client, &my_public_key, rumor, [],
521 ).await {
522 if !self_wrap_session.is_valid() { return; }
523 if !self_outcome.output.success.is_empty() {
524 if let Some(rid) = inner_rumor_id {
525 let _ = db::nip17_keys::store_wrap_key(
526 &self_outcome.wrap_event_id, &rid, &my_public_key,
527 db::nip17_keys::WrapRole::SelfSend,
528 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
529 );
530 }
531 }
532 }
533 });
534
535 let reaction = Reaction {
537 id: rumor_id.clone(),
538 reference_id: reference_id.to_string(),
539 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
540 emoji: emoji.to_string(),
541 emoji_url: emoji_url.map(|s| s.to_string()),
542 };
543 let msg_for_save = {
544 let mut st = state::STATE.lock().await;
545 match st.add_reaction_to_message(reference_id, reaction) {
546 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
547 _ => None,
548 }
549 };
550 if let Some((cid, msg)) = msg_for_save {
551 let _ = db::events::save_message(&cid, &msg).await;
552 traits::emit_event_json("message_update", serde_json::json!({
553 "old_id": reference_id, "message": &msg, "chat_id": &cid
554 }));
555 }
556
557 Ok(rumor_id)
558 }
559
560 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
563 use nostr_sdk::prelude::*;
564
565 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
566 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
567 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
568
569 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
570 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
571 .tag(Tag::public_key(pubkey))
572 .tag(Tag::custom(TagKind::d(), vec!["vector"]))
573 .tag(Tag::expiration(expiry))
574 .build(my_public_key);
575
576 client.gift_wrap_to(
577 state::active_trusted_relays().await,
578 &pubkey,
579 rumor,
580 [Tag::expiration(expiry)],
581 ).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
582 Ok(())
583 }
584
585 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
589 use nostr_sdk::prelude::*;
590
591 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
592 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
593 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
594 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
595 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
596
597 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
599
600 let mut builder = EventBuilder::new(
601 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
602 new_content,
603 ).tag(Tag::event(reference_event));
604 for et in &emoji_tags {
605 builder = builder.tag(Tag::custom(
606 TagKind::custom("emoji"),
607 [et.shortcode.clone(), et.url.clone()],
608 ));
609 }
610 let rumor = builder.build(my_public_key);
611 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
612 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
613
614 let msg_for_emit = {
616 let mut st = state::STATE.lock().await;
617 st.update_message_in_chat(to_npub, message_id, |msg| {
618 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
619 msg.preview_metadata = None;
620 })
621 };
622 if let Some(msg) = msg_for_emit {
623 traits::emit_event_json("message_update", serde_json::json!({
624 "old_id": message_id, "message": &msg, "chat_id": to_npub
625 }));
626 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
627 let _ = db::events::save_edit_event(
628 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
629 ).await;
630 }
631 }
632
633 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
634 .await.map_err(VectorError::Other)?;
635
636 let self_wrap_client = client.clone();
637 let self_wrap_session = state::SessionGuard::capture();
638 tokio::spawn(async move {
639 if !self_wrap_session.is_valid() { return; }
640 let _ = self_wrap_client.gift_wrap(&my_public_key, rumor, []).await;
641 });
642
643 Ok(edit_id)
644 }
645
646 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
648 use nostr_sdk::prelude::*;
649 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
650 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
651 }
652
653 pub async fn get_chats(&self) -> Vec<SerializableChat> {
655 let state = state::STATE.lock().await;
656 state.chats.iter()
657 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
658 .collect()
659 }
660
661 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
663 let state = state::STATE.lock().await;
664 if let Some(chat) = state.get_chat(chat_id) {
665 let msgs = chat.get_all_messages(&state.interner);
666 let start = offset.min(msgs.len());
667 let end = (offset + limit).min(msgs.len());
668 msgs[start..end].to_vec()
669 } else {
670 Vec::new()
671 }
672 }
673
674 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
676 let state = state::STATE.lock().await;
677 state.get_profile(npub)
678 .map(|p| SlimProfile::from_profile(p, &state.interner))
679 }
680
681 pub async fn load_profile(&self, npub: &str) -> bool {
683 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
684 }
685
686 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
688 profile::sync::update_profile(
689 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
690 &NoOpProfileSyncHandler,
691 ).await
692 }
693
694 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
697 profile::sync::update_bot_profile(
698 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
699 &NoOpProfileSyncHandler,
700 ).await
701 }
702
703 pub async fn update_status(&self, status: &str) -> bool {
705 profile::sync::update_status(status.to_string()).await
706 }
707
708 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
714 let path = std::path::Path::new(file_path);
715 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
716 if bytes.is_empty() {
717 return Err(VectorError::Other("Empty image file".into()));
718 }
719 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
720 let mime = crate::crypto::mime_from_extension(&extension);
721 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
722 let signer = client
723 .signer()
724 .await
725 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
726 let servers = crate::blossom_servers::compute_enabled_servers();
727 if servers.is_empty() {
728 return Err(VectorError::Other("No Blossom servers configured".into()));
729 }
730 crate::blossom::upload_blob_with_failover(signer, servers, std::sync::Arc::new(bytes), Some(mime))
731 .await
732 .map_err(VectorError::Other)
733 }
734
735 pub async fn block_user(&self, npub: &str) -> bool {
737 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
738 }
739
740 pub async fn unblock_user(&self, npub: &str) -> bool {
742 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
743 }
744
745 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
747 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
748 }
749
750 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
752 profile::sync::get_blocked_users().await
753 }
754
755 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
757 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
758 }
759
760 pub fn my_npub(&self) -> Option<String> {
762 state::my_public_key()
763 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
764 }
765
766 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
773 use crate::community::ConcordProtocol;
774 let ids = crate::db::community::list_community_ids().unwrap_or_default();
775 let mut out = Vec::new();
776 for id in ids {
777 match crate::db::community::community_protocol(&id).ok().flatten() {
779 Some(ConcordProtocol::V2) => {
780 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
781 let me = state::my_public_key();
782 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
783 out.push(serde_json::json!({
784 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
785 "version": 2,
786 "name": c.name,
787 "description": c.description,
788 "is_owner": is_owner,
789 "channels": c.channels.iter()
790 .map(|ch| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0), "name": ch.name, "private": ch.private }))
791 .collect::<Vec<_>>(),
792 }));
793 }
794 }
795 _ => {
796 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
797 out.push(serde_json::json!({
798 "community_id": c.id.to_hex(),
799 "version": 1,
800 "name": c.name,
801 "description": c.description,
802 "is_owner": crate::community::service::is_proven_owner(&c),
803 "channels": c.channels.iter()
804 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
805 .collect::<Vec<_>>(),
806 }));
807 }
808 }
809 }
810 }
811 out
812 }
813
814 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
819 use crate::community::{v2::service as v2, transport::LiveTransport};
820 let relays: Vec<String> = crate::state::active_trusted_relays()
821 .await
822 .iter()
823 .map(|s| s.to_string())
824 .collect();
825 if relays.is_empty() {
826 return Err(VectorError::Other("no relays available to host the Community".into()));
827 }
828 let session = state::SessionGuard::capture();
829 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
830 let community = v2::create_community(&transport, name, relays, None)
831 .await
832 .map_err(VectorError::Other)?;
833 self.register_v2_chats(&community, &session).await;
834 if let Some(client) = state::nostr_client() {
836 crate::community::v2::realtime::refresh_subscription(&client).await;
837 }
838 Ok(Self::v2_summary(&community))
839 }
840
841 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
847 use crate::community::ConcordProtocol;
848 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
849 return Ok(None);
850 };
851 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
852 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
853 Some(ConcordProtocol::V2) => Some(cid),
854 _ => None,
855 })
856 }
857
858 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
860 let me = state::my_public_key();
861 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
862 serde_json::json!({
863 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
864 "version": 2,
865 "name": community.name,
866 "description": community.description,
867 "is_owner": is_owner,
868 "channels": community.channels.iter()
869 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
870 .collect::<Vec<_>>(),
871 })
872 }
873
874 async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
880 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
881 let me = state::my_public_key();
882 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
883 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
884 let mut st = state::STATE.lock().await;
885 if !session.is_valid() {
886 return; }
888 for ch in &community.channels {
889 st.upsert_community_chat(
890 &crate::simd::hex::bytes_to_hex_32(&ch.id.0),
891 &community.name,
892 community.description.as_deref().unwrap_or(""),
893 &id_hex,
894 is_owner,
895 false,
896 owner_npub.as_deref(),
897 Some(community.created_at_ms),
898 community.dissolved,
899 );
900 }
901 }
902
903 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
907 use crate::community::{public_invite, service, transport::LiveTransport};
908 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
912 let session = state::SessionGuard::capture();
913 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
914 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
915 .await
916 .map_err(VectorError::Other)?;
917 self.register_v2_chats(&community, &session).await;
918 if let Some(client) = state::nostr_client() {
919 crate::community::v2::realtime::refresh_subscription(&client).await;
920 }
921 return Ok(Self::v2_summary(&community));
922 }
923 let (relays, token) = public_invite::parse_invite_url(invite_url)
924 .map_err(|e| VectorError::Other(e.to_string()))?;
925 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
926 let bundle = service::fetch_public_invite(&transport, &relays, &token)
927 .await
928 .map_err(VectorError::Other)?;
929 let now = std::time::SystemTime::now()
930 .duration_since(std::time::UNIX_EPOCH)
931 .map(|d| d.as_secs())
932 .unwrap_or(0);
933 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
934 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
937 self.finalize_member_join(community, &transport, attribution).await
938 }
939
940 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
943 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
944 Ok(rows.iter().map(|p| {
945 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
948 serde_json::json!({
949 "community_id": p.community_id,
950 "name": v2.name,
951 "inviter_npub": p.inviter_npub,
952 "version": 2,
953 })
954 } else {
955 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
956 .ok().map(|i| i.name).unwrap_or_default();
957 serde_json::json!({
958 "community_id": p.community_id,
959 "name": name,
960 "inviter_npub": p.inviter_npub,
961 "version": 1,
962 })
963 }
964 }).collect())
965 }
966
967 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
971 use crate::community::transport::LiveTransport;
972 let bundle_json = crate::db::community::get_pending_invite(community_id)
973 .map_err(VectorError::Other)?
974 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
975 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
976
977 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
979 let session = state::SessionGuard::capture();
980 let inviter = crate::db::community::list_pending_invites()
982 .ok()
983 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
984 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
992 .await
993 .map_err(VectorError::Other)?;
994 if !session.is_valid() {
995 return Err(VectorError::Other("account changed during join".into()));
996 }
997 self.register_v2_chats(&community, &session).await;
998 if let Some(client) = state::nostr_client() {
999 crate::community::v2::realtime::refresh_subscription(&client).await;
1000 }
1001 crate::community::v2::realtime::enqueue_follow(community.id());
1002 let _ = crate::db::community::delete_pending_invite(community_id);
1003 return Ok(Self::v2_summary(&community));
1004 }
1005
1006 use crate::community::invite::{accept_invite, CommunityInvite};
1008 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1009 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1010 let summary = self.finalize_member_join(community, &transport, None).await?;
1012 let _ = crate::db::community::delete_pending_invite(community_id);
1013 Ok(summary)
1014 }
1015
1016 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1021 &self,
1022 community: crate::community::Community,
1023 transport: &T,
1024 attribution: Option<(String, Option<String>)>,
1025 ) -> Result<serde_json::Value> {
1026 use crate::community::service;
1027 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1031 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1034 if c.removed {
1035 let _ = crate::db::community::delete_community(&community.id.to_hex());
1036 return Err(VectorError::Other("you have been removed from this community".into()));
1037 }
1038 }
1039 let community = crate::db::community::load_community(&community.id)
1040 .map_err(VectorError::Other)?
1041 .unwrap_or(community);
1042 let _ = service::fetch_and_apply_control(transport, &community).await;
1046 if service::am_i_banned(&community) {
1047 let _ = crate::db::community::delete_community(&community.id.to_hex());
1048 return Err(VectorError::Other("you are banned from this community".into()));
1049 }
1050 let community = crate::db::community::load_community(&community.id)
1052 .map_err(VectorError::Other)?
1053 .unwrap_or(community);
1054 let owner_npub = community
1055 .owner_attestation
1056 .as_ref()
1057 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1058 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1059 {
1060 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1061 let mut st = state::STATE.lock().await;
1062 for ch in &community.channels {
1063 st.upsert_community_chat(
1064 &ch.id.to_hex(),
1065 &community.name,
1066 community.description.as_deref().unwrap_or(""),
1067 &community.id.to_hex(),
1068 crate::community::service::is_proven_owner(&community),
1069 community.icon.is_some(),
1070 owner_npub.as_deref(),
1071 created_at_ms,
1072 community.dissolved,
1073 );
1074 }
1075 }
1076 if let Some(primary) = community.channels.first() {
1079 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1080 }
1081 Ok(serde_json::json!({
1082 "community_id": community.id.to_hex(),
1083 "version": 1,
1084 "name": community.name,
1085 "channels": community.channels.iter()
1086 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1087 .collect::<Vec<_>>(),
1088 }))
1089 }
1090
1091 pub async fn create_community(&self, name: &str) -> Result<serde_json::Value> {
1095 use crate::community::{service, transport::LiveTransport};
1096 let relays: Vec<String> = crate::state::active_trusted_relays()
1097 .await
1098 .iter()
1099 .map(|s| s.to_string())
1100 .collect();
1101 if relays.is_empty() {
1102 return Err(VectorError::Other("no relays available to host the Community".into()));
1103 }
1104 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1105 let community = service::create_community(&transport, name, "general", relays)
1106 .await
1107 .map_err(VectorError::Other)?;
1108 let owner_npub = community
1109 .owner_attestation
1110 .as_ref()
1111 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1112 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1113 {
1114 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1115 let mut st = state::STATE.lock().await;
1116 for ch in &community.channels {
1117 st.upsert_community_chat(
1118 &ch.id.to_hex(),
1119 &community.name,
1120 community.description.as_deref().unwrap_or(""),
1121 &community.id.to_hex(),
1122 crate::community::service::is_proven_owner(&community),
1123 community.icon.is_some(),
1124 owner_npub.as_deref(),
1125 created_at_ms,
1126 community.dissolved,
1127 );
1128 }
1129 }
1130 Ok(serde_json::json!({
1131 "community_id": community.id.to_hex(),
1132 "version": 1,
1133 "name": community.name,
1134 "channels": community.channels.iter()
1135 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1136 .collect::<Vec<_>>(),
1137 }))
1138 }
1139
1140 pub async fn create_public_invite(&self, community_id: &str) -> Result<String> {
1142 use crate::community::{service, transport::LiveTransport, CommunityId};
1143 if community_id.len() != 64 {
1144 return Err(VectorError::Other("malformed community id".into()));
1145 }
1146 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1147 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1149 crate::db::community::community_protocol(&cid).ok()
1150 {
1151 let community = crate::db::community::load_community_v2(&cid)
1152 .map_err(VectorError::Other)?
1153 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1154 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1155 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1158 let minted = crate::community::v2::service::mint_public_link(&transport, &community, base, None, None)
1159 .await
1160 .map_err(VectorError::Other)?;
1161 return Ok(minted.url);
1162 }
1163 let community = crate::db::community::load_community(&CommunityId(
1164 crate::simd::hex::hex_to_bytes_32(community_id),
1165 ))
1166 .map_err(VectorError::Other)?
1167 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1168 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1169 let (_token, url) = service::create_public_invite(&transport, &community, None, None)
1170 .await
1171 .map_err(VectorError::Other)?;
1172 Ok(url)
1173 }
1174
1175 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1179 use crate::community::{service, CommunityId};
1180 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1181
1182 let session = crate::state::SessionGuard::capture();
1183 let my_pk = crate::state::my_public_key()
1184 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1185
1186 if community_id.len() != 64 {
1187 return Err(VectorError::Other("malformed community id".into()));
1188 }
1189 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1190 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1196 crate::db::community::community_protocol(&cid).ok()
1197 {
1198 let community = crate::db::community::load_community_v2(&cid)
1199 .map_err(VectorError::Other)?
1200 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1201 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1202 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1203 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1204 let bundle = crate::community::v2::service::bundle_of(&community, Some(my_pk), None, None);
1208 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1209 let rumor = nostr_sdk::EventBuilder::new(
1210 nostr_sdk::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1211 bundle_json,
1212 )
1213 .build(my_pk);
1214 let k_tag = nostr_sdk::Tag::custom(
1215 nostr_sdk::TagKind::Custom("k".into()),
1216 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1217 );
1218 if !session.is_valid() {
1219 return Err(VectorError::Other("account changed".into()));
1220 }
1221 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag])
1222 .await
1223 .map_err(VectorError::Other)?;
1224 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1225 }
1226 let community = crate::db::community::load_community(&CommunityId(
1227 crate::simd::hex::hex_to_bytes_32(community_id),
1228 ))
1229 .map_err(VectorError::Other)?
1230 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1231
1232 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1233 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1234 }
1235 let invitee_hex = nostr_sdk::PublicKey::parse(invitee_npub)
1236 .map_err(|_| VectorError::Other("invalid npub".into()))?
1237 .to_hex();
1238 if crate::db::community::get_community_banlist(community_id)
1239 .map_err(VectorError::Other)?
1240 .iter()
1241 .any(|b| b == &invitee_hex)
1242 {
1243 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1244 }
1245
1246 if !session.is_valid() {
1248 return Err(VectorError::Other("account changed during invite".into()));
1249 }
1250
1251 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk).map_err(VectorError::Other)?;
1252 let pending_id = format!("community-invite-{}", community_id);
1253 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1255 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1256
1257 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1258 .await
1259 .map_err(VectorError::Other)?;
1260
1261 Ok(serde_json::json!({
1262 "community_id": community_id,
1263 "invitee": invitee_npub,
1264 "wrap_event_id": result.event_id,
1265 }))
1266 }
1267
1268 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1273 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1274 }
1275
1276 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1281 use crate::community::{service, transport::LiveTransport, CommunityId};
1282 if community_id.len() != 64 {
1283 return Err(VectorError::Other("malformed community id".into()));
1284 }
1285 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1286 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1287 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1290 let community = crate::db::community::load_community_v2(&cid)
1291 .map_err(VectorError::Other)?
1292 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1293 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1294 .await
1295 .map_err(VectorError::Other);
1296 }
1297 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1298 let community = crate::db::community::load_community(&cid)
1299 .map_err(VectorError::Other)?
1300 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1301 service::revoke_public_invite(&transport, &community, &token_bytes)
1302 .await
1303 .map_err(VectorError::Other)
1304 }
1305
1306 pub async fn send_community_message(
1308 &self,
1309 channel_id: &str,
1310 content: &str,
1311 replied_to: Option<&str>,
1312 ) -> Result<String> {
1313 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1314 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1316 let community = crate::db::community::load_community_v2(&id)
1317 .map_err(VectorError::Other)?
1318 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1319 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1320 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1321 let reply = match replied_to.filter(|r| !r.is_empty()) {
1324 Some(parent_id) => {
1325 let author_hex = {
1326 let st = state::STATE.lock().await;
1327 st.find_message(parent_id)
1328 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1329 .map(|pk| pk.to_hex())
1330 .unwrap_or_default()
1331 };
1332 Some((parent_id.to_string(), author_hex))
1333 }
1334 None => None,
1335 };
1336 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1337 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &[], vec![])
1338 .await
1339 .map_err(VectorError::Other);
1340 }
1341 let (community, channel) = self.resolve_channel(channel_id)?;
1342 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1343 let reply = replied_to.filter(|r| !r.is_empty());
1344 let ms = std::time::SystemTime::now()
1345 .duration_since(std::time::UNIX_EPOCH)
1346 .map(|d| d.as_millis() as u64)
1347 .unwrap_or(0);
1348 let unsigned = envelope::build_inner_typed(
1349 author_pk,
1350 &channel.id,
1351 channel.epoch,
1352 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1353 content,
1354 ms,
1355 reply,
1356 &[],
1357 );
1358 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1359 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1360 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1361 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1362 let session = state::SessionGuard::capture();
1363 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1364 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1365 .await
1366 .map_err(VectorError::Other)?;
1367 if !session.is_valid() {
1370 return Ok(message_id);
1371 }
1372 let echoed = {
1373 let mut st = state::STATE.lock().await;
1374 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1375 };
1376 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1377 let _ = crate::db::events::save_message(channel_id, &msg).await;
1378 }
1379 Ok(message_id)
1380 }
1381
1382 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1386 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1387 let path = std::path::Path::new(file_path);
1388 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1389 if bytes.is_empty() {
1390 return Err(VectorError::Other("Empty file".into()));
1391 }
1392 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1393 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1394
1395 let session = state::SessionGuard::capture();
1398 let v2_target = match self.v2_community_for_channel(channel_id)? {
1401 Some(id) => Some(
1402 crate::db::community::load_community_v2(&id)
1403 .map_err(VectorError::Other)?
1404 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1405 ),
1406 None => None,
1407 };
1408 let v1_target = match v2_target {
1409 Some(_) => None,
1410 None => Some(self.resolve_channel(channel_id)?),
1411 };
1412 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1413
1414 let file_hash = crate::crypto::sha256_hex(&bytes);
1415 let mime = crate::crypto::mime_from_extension(&extension);
1416 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1417
1418 let download_dir = crate::db::get_download_dir();
1420 let _ = std::fs::create_dir_all(&download_dir);
1421 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1422 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1423 let _ = std::fs::write(&local_path, &bytes);
1424
1425 let params = crate::crypto::generate_encryption_params();
1427 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1428 let encrypted_size = encrypted.len() as u64;
1429
1430 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1431 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1432 let servers = crate::blossom_servers::compute_enabled_servers();
1433 if servers.is_empty() {
1434 return Err(VectorError::Other("No Blossom servers configured".into()));
1435 }
1436 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1437 let url = crate::blossom::upload_blob_with_progress_and_failover(
1438 signer.clone(),
1439 servers,
1440 std::sync::Arc::new(encrypted),
1441 Some(mime),
1442 true,
1443 noop_progress,
1444 Some(3),
1445 Some(std::time::Duration::from_secs(2)),
1446 None,
1447 ).await.map_err(VectorError::Other)?;
1448
1449 let attachment = crate::types::Attachment {
1450 id: file_hash.clone(),
1451 key: params.key.clone(),
1452 nonce: params.nonce.clone(),
1453 extension: extension.clone(),
1454 name: filename.clone(),
1455 url,
1456 path: local_path.to_string_lossy().to_string(),
1457 size: encrypted_size,
1458 img_meta,
1459 downloading: false,
1460 downloaded: true,
1461 ..Default::default()
1462 };
1463 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
1464
1465 if !session.is_valid() {
1467 return Err(VectorError::Other("account changed during upload".into()));
1468 }
1469 if let Some(community) = v2_target {
1471 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1472 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1473 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
1474 .await
1475 .map_err(VectorError::Other);
1476 }
1477 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
1478 let ms = std::time::SystemTime::now()
1479 .duration_since(std::time::UNIX_EPOCH)
1480 .map(|d| d.as_millis() as u64)
1481 .unwrap_or(0);
1482 let unsigned = envelope::build_inner_full(
1483 author_pk, &channel.id, channel.epoch,
1484 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
1485 );
1486 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1487 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1488 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1489 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1490 .await.map_err(VectorError::Other)?;
1491 let echoed = {
1493 let mut st = state::STATE.lock().await;
1494 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1495 };
1496 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
1497 let _ = crate::db::events::save_message(channel_id, &m).await;
1498 }
1499 Ok(message_id)
1500 }
1501
1502 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
1504 use crate::community::{service, transport::LiveTransport};
1505 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1506 let community = crate::db::community::load_community_v2(&id)
1507 .map_err(VectorError::Other)?
1508 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1509 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1510 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1511 return crate::community::v2::service::send_typing(&transport, &community, &ch)
1512 .await
1513 .map_err(VectorError::Other);
1514 }
1515 let (community, channel) = self.resolve_channel(channel_id)?;
1516 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1517 service::publish_typing_signal(&transport, &community, &channel)
1518 .await
1519 .map_err(VectorError::Other)
1520 }
1521
1522 pub async fn send_community_reaction(
1525 &self,
1526 channel_id: &str,
1527 message_id: &str,
1528 emoji: &str,
1529 emoji_url: Option<&str>,
1530 ) -> Result<()> {
1531 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
1532 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
1533 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
1534 }
1535 _ => Vec::new(),
1536 };
1537 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1538 let session = state::SessionGuard::capture();
1539 let community = crate::db::community::load_community_v2(&id)
1540 .map_err(VectorError::Other)?
1541 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1542 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1543 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1544 let held = {
1549 let st = state::STATE.lock().await;
1550 st.find_message(message_id)
1551 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1552 };
1553 let held = held.or_else(|| {
1554 crate::db::events::event_author(message_id)
1555 .ok()
1556 .flatten()
1557 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
1558 });
1559 let target_author = match held {
1560 Some(pk) => pk,
1561 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
1562 .await
1563 .map_err(VectorError::Other)?
1564 .iter()
1565 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
1566 .map(|f| f.event.opened().author)
1567 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
1568 };
1569 if !session.is_valid() {
1571 return Err(VectorError::Other("account changed before send".into()));
1572 }
1573 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
1574 return crate::community::v2::service::send_reaction(
1579 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
1580 )
1581 .await
1582 .map(|_| ())
1583 .map_err(VectorError::Other);
1584 }
1585 self.publish_community_control(
1586 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
1587 ).await
1588 }
1589
1590 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
1592 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
1593 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1594 let community = crate::db::community::load_community_v2(&id)
1595 .map_err(VectorError::Other)?
1596 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1597 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1598 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1599 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
1600 .await
1601 .map(|_| ())
1602 .map_err(VectorError::Other);
1603 }
1604 self.publish_community_control(
1605 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
1606 ).await
1607 }
1608
1609 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
1613 let channel_id = {
1614 let st = state::STATE.lock().await;
1615 match st.find_message(message_id) {
1616 Some((chat, _)) => chat.id.clone(),
1617 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
1618 }
1619 };
1620 self.delete_community_message_in(&channel_id, message_id).await
1621 }
1622
1623 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
1627 use crate::community::{service, transport::LiveTransport};
1628 let session = state::SessionGuard::capture();
1629 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1630
1631 let attachment_urls: Vec<String> = {
1634 let st = state::STATE.lock().await;
1635 st.find_message(message_id)
1636 .map(|(_, msg)| msg.attachments.iter().filter(|a| !a.url.is_empty()).map(|a| a.url.clone()).collect())
1637 .unwrap_or_default()
1638 };
1639
1640 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1641 let community = crate::db::community::load_community_v2(&id)
1644 .map_err(VectorError::Other)?
1645 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1646 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
1647 crate::community::v2::service::send_delete(
1648 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
1649 )
1650 .await
1651 .map_err(VectorError::Other)?;
1652 } else {
1653 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
1655 let _ = service::delete_message(&transport, message_id).await;
1656 }
1657 self.publish_community_control(
1659 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
1660 ).await?;
1661 }
1662 if !attachment_urls.is_empty() {
1664 if let Some(client) = state::nostr_client() {
1665 if let Ok(signer) = client.signer().await {
1666 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
1667 }
1668 }
1669 }
1670 if !session.is_valid() {
1673 return Ok(());
1674 }
1675 let removed_chat = {
1676 let mut st = state::STATE.lock().await;
1677 st.remove_message(message_id).map(|(cid, _)| cid)
1678 };
1679 let _ = crate::db::events::delete_event(message_id).await;
1680 traits::emit_event_json("message_removed", serde_json::json!({
1681 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
1682 }));
1683 Ok(())
1684 }
1685
1686 async fn publish_community_control(
1689 &self,
1690 channel_id: &str,
1691 kind: u16,
1692 content: &str,
1693 target: &str,
1694 emoji_tags: &[crate::types::EmojiTag],
1695 ) -> Result<()> {
1696 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1697 let (community, channel) = self.resolve_channel(channel_id)?;
1698 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1699 let ms = std::time::SystemTime::now()
1700 .duration_since(std::time::UNIX_EPOCH)
1701 .map(|d| d.as_millis() as u64)
1702 .unwrap_or(0);
1703 let unsigned = envelope::build_inner_typed(
1704 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
1705 );
1706 let client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1707 let signer = client.signer().await.map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1708 let inner = unsigned.sign(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1709 let session = state::SessionGuard::capture();
1710 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1711 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1712 .await.map_err(VectorError::Other)?;
1713 if !session.is_valid() {
1716 return Ok(());
1717 }
1718 let outcome = {
1719 let mut st = state::STATE.lock().await;
1720 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1721 };
1722 if let Some(inbound::IncomingEvent::Updated { target_id, message, edit_event }) = outcome {
1723 if let Some(ev) = edit_event {
1724 let mut ev = (*ev).clone();
1725 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
1726 let _ = crate::db::events::save_event(&ev).await;
1727 } else {
1728 let _ = crate::db::events::save_message(channel_id, &message).await;
1729 }
1730 traits::emit_event_json("message_update", serde_json::json!({
1731 "old_id": target_id, "message": &message, "chat_id": channel_id,
1732 }));
1733 }
1734 Ok(())
1735 }
1736
1737 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
1745 use crate::community::{inbound, send, service, transport::LiveTransport};
1746 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1747 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1752 let warnings = if community::v2::realtime::follow_worker_running() {
1753 community::v2::realtime::enqueue_follow(&id);
1754 Vec::new()
1755 } else {
1756 Self::v2_inline_follow(&id).await
1757 };
1758 let new = Self::v2_backfill_channel(&id, channel_id, limit).await;
1759 return Ok((new, warnings));
1760 }
1761 let (community, _) = self.resolve_channel(channel_id)?;
1762 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1763 let mut warnings: Vec<String> = Vec::new();
1764
1765 match service::catch_up_server_root(&transport, &community).await {
1773 Ok(c) if c.removed => {
1774 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1776 return Ok((0, warnings));
1777 }
1778 Ok(_) => {}
1779 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
1780 }
1781 let (community, _) = self.resolve_channel(channel_id)?;
1782
1783 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
1789 warnings.push(format!("control fold failed: {e}"));
1790 }
1791 if service::am_i_banned(&community) {
1792 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
1794 return Ok((0, warnings));
1795 }
1796 let (community, channel) = self.resolve_channel(channel_id)?;
1799 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
1800 warnings.push(format!("channel catch-up failed: {e}"));
1801 }
1802 let (community, _) = self.resolve_channel(channel_id)?;
1806 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
1807 warnings.push(format!("read-cut resume failed: {e}"));
1808 }
1809 let (community, channel) = self.resolve_channel(channel_id)?;
1810
1811 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
1812 .await
1813 .map_err(VectorError::Other)?;
1814 let outcomes = {
1815 let mut st = state::STATE.lock().await;
1816 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
1817 };
1818 let mut new = 0usize;
1819 for o in &outcomes {
1820 match o {
1821 inbound::IncomingEvent::NewMessage(m) => {
1822 let _ = crate::db::events::save_message(channel_id, m).await;
1823 new += 1;
1824 }
1825 inbound::IncomingEvent::Updated { message, .. } => {
1826 let _ = crate::db::events::save_message(channel_id, message).await;
1827 }
1828 inbound::IncomingEvent::Removed { target_id } => {
1829 let _ = crate::db::events::delete_event(target_id).await;
1830 }
1831 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
1832 let _ = crate::db::events::delete_event(reaction_id).await;
1835 }
1836 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
1837 let et = if *joined {
1838 crate::stored_event::SystemEventType::MemberJoined
1839 } else {
1840 crate::stored_event::SystemEventType::MemberLeft
1841 };
1842 let note = invited_by.as_ref().map(|by| match invited_label {
1844 Some(l) if !l.is_empty() => format!("{by}|{l}"),
1845 _ => by.clone(),
1846 });
1847 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;
1848 }
1849 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
1850 community::service::persist_webxdc_signal(
1853 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
1854 ).await;
1855 }
1856 inbound::IncomingEvent::Kicked { community_id }
1857 | inbound::IncomingEvent::SelfLeft { community_id } => {
1858 let _ = crate::db::community::delete_community_retain_keys(community_id);
1863 break;
1864 }
1865 inbound::IncomingEvent::Typing { .. } => {
1866 }
1868 }
1869 }
1870 Ok((new, warnings))
1871 }
1872
1873 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
1878 use nostr_sdk::prelude::ToBech32;
1879 match Self::load_v2_if_v2(community_id) {
1881 Ok(Some(community)) => {
1882 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1883 return crate::community::v2::service::memberlist(&transport, &community)
1884 .await
1885 .unwrap_or_default()
1886 .into_iter()
1887 .filter_map(|pk| pk.to_bech32().ok())
1888 .map(|npub| serde_json::json!({ "npub": npub }))
1889 .collect();
1890 }
1891 Ok(None) => {} Err(_) => return Vec::new(),
1894 }
1895 crate::db::community::community_member_activity(community_id)
1896 .unwrap_or_default()
1897 .into_iter()
1898 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
1899 .collect()
1900 }
1901
1902 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
1906 use crate::community::transport::LiveTransport;
1907 let session = state::SessionGuard::capture();
1908 let lock = crate::community::v2::realtime::follow_lock(id);
1913 let _guard = lock.lock().await;
1914 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1915 let mut warnings: Vec<String> = Vec::new();
1916 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
1917 warnings.push("v2 community not found".to_string());
1918 return warnings;
1919 };
1920 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1921 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
1922 Ok(f) if f.dissolved => return warnings,
1924 Ok(f) if f.self_removed => {
1925 if session.is_valid() {
1928 let _ = crate::db::community::delete_community(&cid_hex);
1929 }
1930 return warnings;
1931 }
1932 Ok(_) => {}
1933 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
1934 }
1935 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
1936 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
1937 Ok(Some(changed)) => {
1941 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
1942 warnings.push(format!("v2 rekey follow failed: {e}"));
1943 }
1944 }
1945 Ok(None) => {}
1946 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
1947 }
1948 }
1949 warnings
1950 }
1951
1952 async fn v2_backfill_channel(id: &crate::community::CommunityId, channel_id: &str, limit: usize) -> usize {
1960 use crate::community::v2::inbound::{apply_chat_to_state, persist_chat, ChatPersist};
1961 const MAX_BACKFILL_PAGES: usize = 8;
1963 let session = state::SessionGuard::capture();
1966 let Some(my_pk) = state::my_public_key() else { return 0 };
1967 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
1971 return 0;
1972 }
1973 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
1974 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1975 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1976 let Ok(page) = crate::community::v2::service::fetch_channel_history(
1977 &transport,
1978 &community,
1979 &ch,
1980 limit.max(50),
1981 MAX_BACKFILL_PAGES,
1982 |page| {
1987 let mut saw_message = false;
1988 for f in page {
1989 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
1990 saw_message = true;
1991 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
1992 return true;
1993 }
1994 }
1995 }
1996 !saw_message
1997 },
1998 )
1999 .await
2000 else {
2001 return 0;
2002 };
2003 let mut new = 0usize;
2004 for f in &page {
2005 if !session.is_valid() {
2008 break;
2009 }
2010 let outcome = {
2012 let mut st = state::STATE.lock().await;
2013 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2014 };
2015 if let Some(outcome) = outcome {
2016 if matches!(outcome, ChatPersist::New(_)) {
2017 new += 1;
2018 }
2019 persist_chat(channel_id, &outcome).await;
2020 }
2021 }
2022 new
2023 }
2024
2025 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2029 if community_id.len() != 64 {
2030 return Ok(None);
2031 }
2032 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2033 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
2034 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
2035 _ => Ok(None),
2036 }
2037 }
2038
2039 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
2044 use crate::community::CommunityId;
2045 if community_id.len() != 64 {
2046 return Err(VectorError::Other("malformed community id".into()));
2047 }
2048 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
2049 .map_err(VectorError::Other)?
2050 .ok_or_else(|| VectorError::Other("community not found".into()))
2051 }
2052
2053 fn admin_role_id_of(community_id: &str) -> Result<String> {
2054 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2055 roles.roles.iter()
2056 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
2057 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
2058 .map(|r| r.role_id.clone())
2059 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
2060 }
2061
2062 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
2066 use crate::community::service;
2067 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2068 use crate::community::roles::Permissions;
2069 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
2070 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
2071 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2072 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2075 if banned.contains(&me) && me != owner_hex {
2076 return Ok(serde_json::json!({
2077 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
2078 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
2079 }));
2080 }
2081 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
2082 return Ok(serde_json::json!({
2083 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
2084 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
2085 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
2086 "manage_admin_role": me == owner_hex,
2088 }));
2089 }
2090 let community = Self::load_community_hex(community_id)?;
2091 let caps = service::caller_capabilities(&community);
2092 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
2093 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
2094 .unwrap_or(false);
2095 Ok(serde_json::json!({
2096 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
2097 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
2098 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
2099 "manage_admin_role": manage_admin_role,
2100 }))
2101 }
2102
2103 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
2106 use nostr_sdk::prelude::{PublicKey, ToBech32};
2107 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2108 let owner = v2.owner().map_err(VectorError::Other)?;
2109 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2110 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2112 let admins: Vec<String> = roster.grants.iter()
2113 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
2114 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2115 .collect();
2116 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
2117 }
2118 let community = Self::load_community_hex(community_id)?;
2119 let owner = community.owner_attestation.as_ref()
2120 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
2121 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
2122 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2123 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
2124 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2125 .collect();
2126 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
2127 }
2128
2129 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2131 use crate::community::{service, transport::LiveTransport};
2132 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2133 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2134 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2135 return crate::community::v2::service::grant_admin(&transport, &v2, &member)
2136 .await
2137 .map_err(VectorError::Other);
2138 }
2139 let community = Self::load_community_hex(community_id)?;
2140 let role_id = Self::admin_role_id_of(community_id)?;
2141 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2142 }
2143
2144 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2146 use crate::community::{service, transport::LiveTransport};
2147 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2148 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2149 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2150 return crate::community::v2::service::revoke_admin(&transport, &v2, &member)
2151 .await
2152 .map_err(VectorError::Other);
2153 }
2154 let community = Self::load_community_hex(community_id)?;
2155 let role_id = Self::admin_role_id_of(community_id)?;
2156 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2157 }
2158
2159 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
2161 use crate::community::{service, transport::LiveTransport};
2162 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2163 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2164 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2165 return crate::community::v2::service::kick_member(&transport, &v2, &pk)
2166 .await
2167 .map_err(VectorError::Other);
2168 }
2169 let community = Self::load_community_hex(community_id)?;
2170 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
2171 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
2172 }
2173
2174 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
2177 use crate::community::{service, transport::LiveTransport, CommunityId};
2178 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2179 let hex = pk.to_hex();
2180 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2181 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
2183 list.retain(|h| h != &hex);
2184 if banned {
2185 list.push(hex);
2186 }
2187 if community_id.len() == 64 {
2191 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2192 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2193 let community = crate::db::community::load_community_v2(&cid)
2194 .map_err(VectorError::Other)?
2195 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2196 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
2197 if banned {
2198 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
2199 crate::community::v2::service::refound_community(&transport, &community, &[pk]).await.map_err(VectorError::Other)?;
2200 }
2201 return Ok(());
2202 }
2203 }
2204 let community = Self::load_community_hex(community_id)?;
2205 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
2206 }
2207
2208 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
2212 use crate::community::{service, transport::LiveTransport, CommunityId};
2213 if community_id.len() != 64 {
2214 return Err(VectorError::Other("malformed community id".into()));
2215 }
2216 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2217 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2218 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2221 let community = crate::db::community::load_community_v2(&cid)
2222 .map_err(VectorError::Other)?
2223 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2224 return crate::community::v2::service::dissolve_community(&transport, &community)
2225 .await
2226 .map_err(VectorError::Other);
2227 }
2228 let community = Self::load_community_hex(community_id)?;
2229 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
2230 }
2231
2232 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
2235 use crate::community::{service, transport::LiveTransport, CommunityId};
2236 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2237 if community_id.len() == 64 {
2241 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2242 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2243 let community = crate::db::community::load_community_v2(&cid)
2244 .map_err(VectorError::Other)?
2245 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2246 let meta = crate::community::v2::control::CommunityMetadata {
2247 name: name.unwrap_or(&community.name).to_string(),
2248 description: match description {
2249 Some("") => None,
2250 Some(d) => Some(d.to_string()),
2251 None => community.description.clone(),
2252 },
2253 relays: community.relays.clone(),
2254 ..Default::default()
2255 };
2256 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
2257 .await
2258 .map_err(VectorError::Other);
2259 }
2260 }
2261 let mut community = Self::load_community_hex(community_id)?;
2262 if let Some(n) = name { community.name = n.to_string(); }
2263 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
2264 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
2265 }
2266
2267 pub async fn create_community_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
2273 let v2 = Self::load_v2_if_v2(community_id)?
2274 .ok_or_else(|| VectorError::Other("channel creation is available on v2 communities".into()))?;
2275 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2276 let id = if private {
2277 crate::community::v2::service::create_private_channel(&transport, &v2, name).await
2278 } else {
2279 crate::community::v2::service::create_public_channel(&transport, &v2, name).await
2280 }
2281 .map_err(VectorError::Other)?;
2282 if let Some(client) = state::nostr_client() {
2285 crate::community::v2::realtime::refresh_subscription(&client).await;
2286 }
2287 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
2288 }
2289
2290 pub async fn delete_community_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
2292 let v2 = Self::load_v2_if_v2(community_id)?
2293 .ok_or_else(|| VectorError::Other("channel deletion is available on v2 communities".into()))?;
2294 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2295 let name = v2.channels.iter().find(|c| c.id.0 == ch.0).map(|c| c.name.clone()).unwrap_or_default();
2296 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2297 crate::community::v2::service::delete_channel(&transport, &v2, &ch, &name)
2298 .await
2299 .map_err(VectorError::Other)
2300 }
2301
2302 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
2305 use crate::community::{transport::LiveTransport, CommunityId};
2306 if community_id.len() != 64 {
2307 return Err(VectorError::Other("malformed community id".into()));
2308 }
2309 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2310 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2312 let session = state::SessionGuard::capture();
2313 let channel_ids: Vec<String> =
2314 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
2315 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2316 crate::community::v2::service::leave_community(&transport, &v2)
2317 .await
2318 .map_err(VectorError::Other)?;
2319 if !session.is_valid() {
2320 return Err(VectorError::Other("account changed during leave".into()));
2321 }
2322 let mut st = state::STATE.lock().await;
2323 st.chats.retain(|c| !channel_ids.contains(&c.id));
2324 return Ok(());
2325 }
2326 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
2327 let channel_ids: Vec<String> = community
2328 .as_ref()
2329 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
2330 .unwrap_or_default();
2331 if let Some(ref c) = community {
2333 if let Some(primary) = c.channels.first() {
2334 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2335 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
2336 }
2337 }
2338 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
2340 {
2341 let mut st = state::STATE.lock().await;
2342 st.chats.retain(|c| !channel_ids.contains(&c.id));
2343 }
2344 Ok(())
2345 }
2346
2347 fn resolve_channel(
2349 &self,
2350 channel_id: &str,
2351 ) -> Result<(crate::community::Community, crate::community::Channel)> {
2352 use crate::community::CommunityId;
2353 let community_id = crate::db::community::community_id_for_channel(channel_id)
2354 .map_err(VectorError::Other)?
2355 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
2356 if community_id.len() != 64 {
2357 return Err(VectorError::Other("malformed community id".into()));
2358 }
2359 let community = crate::db::community::load_community(&CommunityId(
2360 crate::simd::hex::hex_to_bytes_32(&community_id),
2361 ))
2362 .map_err(VectorError::Other)?
2363 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
2364 let channel = community
2365 .channels
2366 .iter()
2367 .find(|c| c.id.to_hex() == channel_id)
2368 .cloned()
2369 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
2370 Ok((community, channel))
2371 }
2372
2373
2374 pub async fn sync_dms(
2391 &self,
2392 since_days: Option<u64>,
2393 handler: &dyn InboundEventHandler,
2394 ) -> Result<(u32, u32)> {
2395 use futures_util::StreamExt;
2396 use nostr_sdk::prelude::*;
2397
2398 let client = state::nostr_client()
2399 .ok_or(VectorError::Other("Not connected".into()))?;
2400 let my_pk = state::my_public_key()
2401 .ok_or(VectorError::Other("Not logged in".into()))?;
2402
2403 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
2405
2406 let (items, filter) = if let Some(days) = since_days {
2408 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
2409 let items: Vec<(EventId, Timestamp)> = all_items.iter()
2410 .filter(|(_, ts)| ts.as_secs() >= since_ts)
2411 .cloned()
2412 .collect();
2413 let filter = Filter::new()
2414 .pubkey(my_pk)
2415 .kind(Kind::GiftWrap)
2416 .since(Timestamp::from_secs(since_ts));
2417 (items, filter)
2418 } else {
2419 let filter = Filter::new()
2420 .pubkey(my_pk)
2421 .kind(Kind::GiftWrap);
2422 (all_items, filter)
2423 };
2424
2425 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
2426
2427 let sync_opts = nostr_sdk::SyncOptions::new()
2429 .direction(nostr_sdk::SyncDirection::Down)
2430 .initial_timeout(std::time::Duration::from_secs(10))
2431 .dry_run();
2432
2433 let relay_map = client.relays().await;
2435 let all_relays: Vec<(RelayUrl, Relay)> = relay_map.iter()
2436 .map(|(url, relay)| (url.clone(), relay.clone()))
2437 .collect();
2438 drop(relay_map);
2439
2440 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
2441 for (url, relay) in &all_relays {
2442 let url = url.clone();
2443 let relay = relay.clone();
2444 let f = filter.clone();
2445 let i = items.clone();
2446 let o = sync_opts.clone();
2447 relay_futs.push(async move {
2448 let result = tokio::time::timeout(
2449 std::time::Duration::from_secs(10),
2450 relay.sync_with_items(f, i, &o),
2451 ).await;
2452 (url, result)
2453 });
2454 }
2455
2456 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
2458 while let Some((url, result)) = relay_futs.next().await {
2459 match result {
2460 Ok(Ok(recon)) => {
2461 let count = recon.remote.len();
2462 all_missing.extend(recon.remote);
2463 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
2464 }
2465 Ok(Err(e)) => log_warn!("[SyncDMs] {} failed: {}", url, e),
2466 Err(_) => log_warn!("[SyncDMs] {} timed out (10s)", url),
2467 }
2468 }
2469
2470 if all_missing.is_empty() {
2471 log_info!("[SyncDMs] No missing events");
2472 return Ok((0, 0));
2473 }
2474
2475 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
2477 let ids: Vec<EventId> = all_missing.into_iter().collect();
2478 let relay_strs: Vec<String> = client.relays().await.keys()
2479 .map(|u| u.to_string()).collect();
2480
2481 let mut total_events = 0u32;
2482 let mut new_messages = 0u32;
2483 const BATCH_SIZE: usize = 500;
2484
2485 for batch in ids.chunks(BATCH_SIZE) {
2486 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap);
2487 match client.stream_events_from(
2488 relay_strs.clone(), f,
2489 std::time::Duration::from_secs(30),
2490 ).await {
2491 Ok(stream) => {
2492 let client_clone = client.clone();
2493 let prepared_stream = stream
2494 .map(move |event| {
2495 let c = client_clone.clone();
2496 tokio::spawn(async move {
2497 event_handler::prepare_event(event, &c, my_pk).await
2498 })
2499 })
2500 .buffer_unordered(8);
2501 tokio::pin!(prepared_stream);
2502
2503 while let Some(result) = prepared_stream.next().await {
2504 total_events += 1;
2505 if let Ok(prepared) = result {
2506 if event_handler::commit_prepared_event(prepared, false, handler).await {
2507 new_messages += 1;
2508 }
2509 }
2510 }
2511 }
2512 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
2513 }
2514 }
2515
2516 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
2517 Ok((total_events, new_messages))
2518 }
2519
2520 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::SubscriptionId> {
2529 use nostr_sdk::prelude::*;
2530 let client = state::nostr_client()
2531 .ok_or(VectorError::Other("Not connected".into()))?;
2532 let my_pk = state::my_public_key()
2533 .ok_or(VectorError::Other("Not logged in".into()))?;
2534
2535 let filter = Filter::new()
2536 .pubkey(my_pk)
2537 .kind(Kind::GiftWrap)
2538 .limit(0);
2539
2540 let output = client.subscribe(filter, None).await
2541 .map_err(|e| VectorError::Nostr(e.to_string()))?;
2542 Ok(output.val)
2543 }
2544
2545 pub async fn sync_communities(&self) -> Result<()> {
2556 {
2560 use crate::community::{transport::LiveTransport, v2::service as v2};
2561 let bootstrap: Vec<String> = match crate::state::nostr_client() {
2562 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
2563 None => Vec::new(),
2564 };
2565 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2566 if let Ok(joined) = v2::sync_community_list(&transport, &bootstrap).await {
2567 for c in &joined {
2568 if community::v2::realtime::follow_worker_running() {
2569 community::v2::realtime::enqueue_follow(c.id());
2570 } else {
2571 let _ = Self::v2_inline_follow(c.id()).await;
2572 }
2573 }
2574 if !joined.is_empty() {
2575 if let Some(client) = crate::state::nostr_client() {
2576 community::v2::realtime::refresh_subscription(&client).await;
2577 }
2578 }
2579 }
2580 }
2581
2582 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
2583 for id in ids {
2584 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
2585 if community::v2::realtime::follow_worker_running() {
2588 community::v2::realtime::enqueue_follow(&id);
2589 } else {
2590 let _ = Self::v2_inline_follow(&id).await;
2591 }
2592 continue;
2593 }
2594 if let Ok(Some(community)) = db::community::load_community(&id) {
2595 for ch in &community.channels {
2596 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
2597 }
2598 }
2599 }
2600 Ok(())
2601 }
2602
2603
2604 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
2636 use nostr_sdk::prelude::*;
2637
2638 let client = state::nostr_client()
2639 .ok_or(VectorError::Other("Not connected".into()))?;
2640 let my_pk = state::my_public_key()
2641 .ok_or(VectorError::Other("Not logged in".into()))?;
2642
2643 community::v2::streamauth::ensure_responder(&client);
2650
2651 community::v2::realtime::spawn_follow_worker(handler.clone());
2660 let _ = self.sync_communities().await;
2661 let _ = self.sync_dms(None, &NoOpEventHandler).await;
2662
2663 let dm_sub_id = self.subscribe_dms().await?;
2666 community::realtime::refresh_subscription(&client).await;
2667 community::v2::realtime::refresh_subscription(&client).await;
2668
2669 if let Some(monitor) = client.monitor() {
2676 let mut rx = monitor.subscribe();
2677 let session = state::SessionGuard::capture();
2678 tokio::spawn(async move {
2679 let mut last_resync: Option<std::time::Instant> = None;
2682 while let Ok(notification) = rx.recv().await {
2683 if !session.is_valid() {
2684 return;
2685 }
2686 let MonitorNotification::StatusChanged { status, .. } = notification;
2687 if status == RelayStatus::Connected {
2688 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
2689 continue;
2690 }
2691 let _ = VectorCore.sync_communities().await;
2692 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
2693 if let Some(c) = state::nostr_client() {
2694 community::realtime::refresh_subscription(&c).await;
2695 community::v2::realtime::refresh_subscription(&c).await;
2696 }
2697 last_resync = Some(std::time::Instant::now());
2698 }
2699 }
2700 });
2701 }
2702
2703 {
2707 let client_health = client.clone();
2708 let session = state::SessionGuard::capture();
2709 tokio::spawn(async move {
2710 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
2712 if !session.is_valid() {
2713 return;
2714 }
2715 for (url, relay) in client_health.relays().await {
2716 match relay.status() {
2717 RelayStatus::Connected => {
2718 let probe = tokio::time::timeout(
2719 std::time::Duration::from_secs(10),
2720 client_health.fetch_events_from(
2721 vec![url.to_string()],
2722 Filter::new().kind(Kind::Metadata).limit(1),
2723 std::time::Duration::from_secs(8),
2724 ),
2725 )
2726 .await;
2727 if !matches!(probe, Ok(Ok(_))) {
2728 let _ = relay.disconnect();
2729 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2730 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
2731 }
2732 }
2733 RelayStatus::Terminated | RelayStatus::Disconnected => {
2734 let _ = relay.try_connect(std::time::Duration::from_secs(10)).await;
2735 }
2736 _ => {}
2737 }
2738 }
2739 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
2740 }
2741 });
2742 }
2743
2744 let client_for_closure = client.clone();
2745
2746 client.handle_notifications(move |notification| {
2747 let handler = handler.clone();
2748 let c = client_for_closure.clone();
2749 let dm_sid = dm_sub_id.clone();
2750 async move {
2751 if let RelayPoolNotification::Event { event, subscription_id, .. } = notification {
2752 if subscription_id == dm_sid {
2753 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
2755 event_handler::commit_prepared_event(prepared, true, &*handler).await;
2756 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
2757 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
2758 {
2759 let session = state::SessionGuard::capture();
2763 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
2764 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
2765 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
2766 {
2767 let session = state::SessionGuard::capture();
2769 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
2770 }
2771 }
2772 Ok(false)
2773 }
2774 }).await.map_err(|e| VectorError::Nostr(e.to_string()))?;
2775
2776 Ok(())
2777 }
2778
2779 pub async fn logout(&self) {
2781 if let Some(client) = state::nostr_client() {
2782 let _ = client.disconnect().await;
2783 }
2784 db::close_database();
2785 }
2786
2787 pub async fn swap_session(&self) {
2795 state::bump_session_generation();
2797
2798 if let Some(client) = state::take_nostr_client() {
2801 let _ = client.shutdown().await;
2802 }
2803 db::close_database();
2804
2805 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
2807 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
2808 {
2809 use zeroize::Zeroize;
2810 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
2811 if let Some(s) = g.as_mut() { s.zeroize(); }
2812 *g = None;
2813 }
2814 if let Ok(mut g) = state::PENDING_NSEC.lock() {
2815 if let Some(s) = g.as_mut() { s.zeroize(); }
2816 *g = None;
2817 }
2818 }
2819
2820 {
2822 let mut st = state::STATE.lock().await;
2823 st.profiles.clear();
2824 st.chats.clear();
2825 st.db_loaded = false;
2826 st.is_syncing = false;
2827 }
2828 state::WRAPPER_ID_CACHE.lock().await.clear();
2829 state::PENDING_EVENTS.lock().await.clear();
2830 state::set_active_chat(None);
2831 crate::profile::sync::clear_profile_sync_queue();
2832 crate::inbox_relays::clear_inbox_relay_cache();
2833 crate::emoji_packs::clear_nip65_cache();
2834 crate::db::clear_id_caches();
2838 crate::community::cache::clear();
2842 crate::community::realtime::clear().await;
2845 crate::community::v2::realtime::clear().await;
2846 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
2850 }
2851}
2852
2853#[cfg(test)]
2854mod facade_tests {
2855 use super::*;
2856
2857 #[tokio::test]
2860 async fn download_attachment_rejects_private_url() {
2861 let att = crate::types::Attachment {
2862 url: "http://169.254.169.254/latest/meta-data/".to_string(),
2863 ..Default::default()
2864 };
2865 match VectorCore.download_attachment(&att).await {
2866 Err(VectorError::Other(msg)) => {
2867 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
2868 }
2869 other => panic!("expected SSRF rejection, got {other:?}"),
2870 }
2871 }
2872
2873 #[tokio::test]
2874 async fn download_attachment_rejects_empty_url() {
2875 let att = crate::types::Attachment::default();
2876 assert!(VectorCore.download_attachment(&att).await.is_err());
2877 }
2878
2879 #[tokio::test]
2883 async fn list_communities_and_channel_routing_are_protocol_aware() {
2884 use crate::community::transport::memory::MemoryRelay;
2885 use nostr_sdk::prelude::Keys;
2886
2887 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
2888 crate::db::close_database();
2889 crate::db::clear_id_caches();
2890 let tmp = tempfile::tempdir().unwrap();
2891 let acct = {
2893 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
2894 let mut s = String::from("npub1");
2895 for i in 0..58 {
2896 s.push(B[(i * 7 + 3) % 32] as char);
2897 }
2898 s
2899 };
2900 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
2901 crate::db::set_app_data_dir(tmp.path().to_path_buf());
2902 crate::db::set_current_account(acct.clone()).unwrap();
2903 crate::db::init_database(&acct).unwrap();
2904 let _ = crate::state::take_nostr_client();
2905 let me = Keys::generate();
2906 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
2907 crate::state::set_my_public_key(me.public_key());
2908
2909 let relay = MemoryRelay::new();
2911 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
2912 .await
2913 .unwrap();
2914 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
2915
2916 let listed = VectorCore.list_communities().await;
2918 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
2919 assert_eq!(v2["name"], "V2 Guild");
2920 assert_eq!(v2["is_owner"], true);
2921 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
2922
2923 assert_eq!(
2925 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
2926 Some(community.identity.community_id),
2927 "a v2 channel is routed to v2"
2928 );
2929 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
2931 }
2932
2933 #[test]
2938 fn v2_invite_url_base_derivation_round_trips() {
2939 use crate::community::v2::derive::TOKEN_LEN;
2940 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
2941 use nostr_sdk::prelude::Keys;
2942 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
2943 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
2944 let signer = Keys::generate();
2945 let token = [0x07u8; TOKEN_LEN];
2946 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
2947 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
2948 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
2949 let parsed = parse_invite_link(&url).unwrap();
2950 assert_eq!(parsed.link_signer, signer.public_key());
2951 assert_eq!(parsed.token, token);
2952 }
2953}