1#[macro_use]
29mod macros;
30
31pub mod logging;
33pub mod error;
34pub mod traits;
35
36use crate::event_ext::FinalizeUnsignedWithId;
38use nostr_sdk::prelude::{FinalizeEventAsync, ToBech32};
39
40pub mod event_ext;
42pub mod tags;
43pub mod types;
44pub mod profile;
45pub mod chat;
46pub mod compact;
47
48pub mod state;
50
51#[cfg(debug_assertions)]
53pub mod stats;
54
55pub mod crypto;
57
58pub mod signer;
60
61pub mod nip55;
63
64pub mod db;
66
67pub mod net;
69pub mod negentropy;
70pub mod blossom;
71pub mod blossom_servers;
72pub mod blossom_capabilities;
73pub mod inbox_relays;
74pub mod emoji_packs;
75pub mod emoji_usage;
76pub mod badges;
77pub mod bot_interface;
78pub mod webxdc;
79#[cfg(feature = "tor")]
80pub mod tor;
81
82#[derive(Debug)]
94pub struct VectorAuthenticator;
95
96impl nostr_sdk::prelude::Authenticator for VectorAuthenticator {
97 fn make_auth_event<'a>(
98 &'a self,
99 relay_url: &'a nostr_sdk::prelude::RelayUrl,
100 challenge: &'a str,
101 ) -> nostr_sdk::prelude::BoxedFuture<'a, std::result::Result<nostr_sdk::prelude::Event, nostr_sdk::prelude::Error>>
102 {
103 Box::pin(async move {
104 let signer =
105 signer::active_signer().map_err(nostr_sdk::prelude::Error::other)?;
106 Ok(nostr_sdk::prelude::EventBuilder::auth(challenge, relay_url.clone())
107 .finalize_async(&signer)
108 .await?)
109 })
110 }
111}
112
113pub fn nostr_client_builder() -> nostr_sdk::prelude::ClientBuilder {
124 apply_tor_proxy(
125 nostr_sdk::prelude::ClientBuilder::new()
126 .authenticator(VectorAuthenticator)
127 .connect_timeout(relay_connect_timeout(std::time::Duration::from_secs(15))),
130 )
131}
132
133pub trait ClientRelayExt {
145 fn add_managed_relay<'client, 'url, U>(
147 &'client self,
148 url: U,
149 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
150 where
151 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>;
152}
153
154impl ClientRelayExt for nostr_sdk::prelude::Client {
155 fn add_managed_relay<'client, 'url, U>(
156 &'client self,
157 url: U,
158 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
159 where
160 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>,
161 {
162 self.add_relay(url).reconnect(false)
163 }
164}
165
166pub async fn resubscribe_relay_after_reconnect(
181 client: &nostr_sdk::prelude::Client,
182 relay: &nostr_sdk::prelude::RelayUrl,
183) {
184 for (id, per_relay) in client.subscriptions().await {
185 let Some(filters) = per_relay.get(relay) else { continue };
186 if filters.is_empty() {
187 continue;
188 }
189 let _ = client
190 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), filters.clone()))
191 .with_id(id)
192 .await;
193 }
194}
195
196#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
201const TOR_RELAY_CONNECT_FLOOR: std::time::Duration = std::time::Duration::from_secs(60);
202
203pub fn relay_connect_timeout(clearnet: std::time::Duration) -> std::time::Duration {
212 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
213 {
214 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
215 return clearnet.max(TOR_RELAY_CONNECT_FLOOR);
216 }
217 }
218 clearnet
219}
220
221#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
223const TOR_RELAY_REQUEST_FLOOR: std::time::Duration = std::time::Duration::from_secs(30);
224
225pub fn relay_request_timeout(clearnet: std::time::Duration) -> std::time::Duration {
231 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
232 {
233 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
234 return clearnet.max(TOR_RELAY_REQUEST_FLOOR);
235 }
236 }
237 clearnet
238}
239
240pub fn apply_tor_proxy(
247 builder: nostr_sdk::prelude::ClientBuilder,
248) -> nostr_sdk::prelude::ClientBuilder {
249 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
250 let builder = builder.proxy(nostr_sdk::prelude::Proxy::custom(|_url| tor_proxy_target()));
251 builder
252}
253
254#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
260fn tor_proxy_target() -> Option<std::net::SocketAddr> {
261 match tor::transport_state() {
262 tor::TorTransportState::Active(addr) => Some(addr),
263 tor::TorTransportState::RequiredButInactive => Some(tor::blackhole_proxy_addr()),
266 tor::TorTransportState::Disabled => None,
267 }
268}
269
270pub async fn sign_builder(
275 builder: nostr_sdk::prelude::EventBuilder,
276) -> std::result::Result<nostr_sdk::prelude::Event, String> {
277 let signer = signer::active_signer()?;
278 builder
279 .finalize_async(&signer)
280 .await
281 .map_err(|e| e.to_string())
282}
283
284pub async fn sign_and_send(
288 client: &nostr_sdk::prelude::Client,
289 builder: nostr_sdk::prelude::EventBuilder,
290) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String> {
291 let event = sign_builder(builder).await?;
292 client
293 .send_event(&event)
294 .await
295 .map_err(|e| e.to_string())
296}
297
298pub async fn send_gift_wrap<'u, I, U, T>(
304 client: &nostr_sdk::prelude::Client,
305 relays: I,
306 receiver: &nostr_sdk::prelude::PublicKey,
307 rumor: nostr_sdk::prelude::UnsignedEvent,
308 extra_tags: T,
309) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String>
310where
311 I: IntoIterator<Item = U>,
312 U: Into<nostr_sdk::prelude::RelayUrlArg<'u>>,
313 T: IntoIterator<Item = nostr_sdk::prelude::Tag>,
314{
315 let signer = signer::active_signer()?;
316 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(*receiver, rumor)
317 .extra_tags(extra_tags)
318 .finalize_async(&signer)
319 .await
320 .map_err(|e| e.to_string())?;
321 let targets: Vec<nostr_sdk::prelude::RelayUrlArg<'u>> =
322 relays.into_iter().map(Into::into).collect();
323 if targets.is_empty() {
324 client.send_event(&wrap).await.map_err(|e| e.to_string())
325 } else {
326 client
327 .send_event(&wrap)
328 .to(targets)
329 .await
330 .map_err(|e| e.to_string())
331 }
332}
333
334pub fn community_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
347 nostr_sdk::prelude::RelayCapabilities::GOSSIP
348}
349
350pub fn discovery_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
356 community_relay_capabilities()
357}
358
359pub mod stored_event;
361
362pub mod rumor;
364
365pub mod sending;
367
368pub mod wallpaper;
370
371pub mod deletion;
373pub mod self_destruct;
374
375pub mod simd;
377
378pub mod community;
380
381pub mod event_handler;
383
384pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
386pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
387pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
388pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
389pub use state::{
390 ChatState, NOSTR_CLIENT, MY_SECRET_KEY, MY_PUBLIC_KEY, STATE, ENCRYPTION_KEY,
391 nostr_client, my_public_key, has_active_session,
392 set_nostr_client, set_my_public_key,
393 take_nostr_client, clear_my_public_key,
394 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
395 set_pending_nip55_setup, pending_nip55_setup, clear_pending_nip55_setup,
396};
397pub use crypto::{GuardedKey, GuardedSigner};
398pub use signer::{
399 SignerKind, signer_kind, set_signer_kind, is_bunker, is_keyless,
400 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
401 build_bunker_signer, prewarm_bunker, drain_bunker_state,
402 parse_bunker_remote_pubkey, parse_bunker_relays,
403 BunkerConnectionState, bunker_state, set_bunker_state,
404 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
405 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
406 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
407};
408pub use nip55::{
409 Nip55Backend, Nip55Error, Nip55ResolverOutcome, Nip55Signer, Nip55State,
410 set_nip55_backend, nip55_backend, nip55_state, set_nip55_state, drain_nip55_state,
411 nip55_is_installed, nip55_pair, nip55_perms_json,
412 VECTOR_NIP55_SIGN_KINDS, VECTOR_NIP55_ENCRYPT_TYPES,
413};
414pub use error::{VectorError, Result};
415pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
416pub use db::{set_app_data_dir, get_app_data_dir};
417pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
418pub use deletion::{delete_own_dm, DeleteOutcome};
419pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
420pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
421pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
422pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
423
424use std::path::PathBuf;
425use std::sync::Arc;
426
427pub struct CoreConfig {
433 pub data_dir: PathBuf,
435 pub event_emitter: Option<Box<dyn EventEmitter>>,
437}
438
439#[derive(Clone, Copy)]
461pub struct VectorCore;
462
463impl VectorCore {
464 pub fn init(config: CoreConfig) -> Result<Self> {
466 db::set_app_data_dir(config.data_dir);
468
469 if let Some(emitter) = config.event_emitter {
471 traits::set_event_emitter(emitter);
472 }
473
474 let _ = rustls::crypto::ring::default_provider().install_default();
476
477 Ok(VectorCore)
478 }
479
480 pub fn accounts(&self) -> Result<Vec<String>> {
482 db::get_accounts().map_err(VectorError::from)
483 }
484
485 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
487 use nostr_sdk::prelude::*;
488
489 let keys = if key.starts_with("nsec1") {
491 let secret = SecretKey::from_bech32(key)
492 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
493 Keys::new(secret)
494 } else {
495 Keys::from_mnemonic(key, None)
497 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
498 };
499
500 let public_key = keys.public_key();
501 let npub = public_key.to_bech32()
502 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
503
504 let secret_bytes = keys.secret_key().to_secret_bytes();
506 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
507 state::set_my_public_key(public_key);
508
509 db::set_current_account(npub.clone())?;
511 db::init_database(&npub)?;
512
513 {
515 let nsec = keys.secret_key().to_bech32()
516 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
517 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
518
519 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
526 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
527 db::set_pkey(&nsec)?;
528 }
529 }
530
531 let has_encryption = state::resolve_encryption_enabled_from_db();
534
535 if has_encryption {
536 if let Some(pwd) = password {
537 let key = crate::crypto::hash_pass(pwd).await;
538 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
539 }
540 }
541 state::init_encryption_enabled();
544
545 let client = crate::nostr_client_builder()
548 .monitor(Monitor::new(1024))
550 .build();
551
552 for relay in state::TRUSTED_RELAYS {
554 client.add_managed_relay(*relay).await.ok();
555 }
556
557 client.connect().await;
559
560 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
561
562 Ok(LoginResult { npub, has_encryption })
563 }
564
565 pub fn generate_nsec(&self) -> Result<String> {
568 use nostr_sdk::prelude::*;
569 Keys::generate().secret_key().to_bech32()
570 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
571 }
572
573 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
578 let config = SendConfig { self_send: false, ..SendConfig::headless() };
579 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
580 .map_err(|e| VectorError::Other(e))
581 }
582
583 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
585 let config = SendConfig { self_send: false, ..SendConfig::headless() };
586 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
587 .map_err(|e| VectorError::Other(e))
588 }
589
590 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
597 self.download_attachment_from(attachment, None).await
598 }
599
600 pub async fn download_attachment_from(
605 &self,
606 attachment: &Attachment,
607 author_npub: Option<&str>,
608 ) -> Result<Vec<u8>> {
609 use futures_util::StreamExt;
610 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
611 if attachment.url.is_empty() {
612 return Err(VectorError::Other("attachment has no URL".into()));
613 }
614 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
615 let mut last_err = String::from("download failed");
616 let mut candidates: Vec<String> = vec![attachment.url.clone()];
617 candidates.extend(attachment.fallback_urls.iter().cloned());
618 let mut hash_swap_tried = false;
619 let mut i = 0;
620 'sources: while i < candidates.len() {
621 let url = candidates[i].clone();
622 i += 1;
623 let extend_with_swap = |candidates: &mut Vec<String>, servers: &[String]| {
626 let extra = crate::blossom::hash_swap_candidates(&attachment.url, servers);
627 for c in extra {
628 if !candidates.contains(&c) {
629 candidates.push(c);
630 }
631 }
632 };
633 macro_rules! next_source {
634 () => {{
635 log_net_fail!("[Download] source failed ({}): {}", url, last_err);
636 if i == candidates.len() && !hash_swap_tried {
637 hash_swap_tried = true;
638 let servers = crate::blossom_servers::author_swap_servers(author_npub, false).await;
639 extend_with_swap(&mut candidates, &servers);
640 }
641 continue 'sources;
642 }};
643 }
644 if let Err(e) = crate::net::validate_url_not_private(&url) {
648 last_err = e.to_string();
649 next_source!();
650 }
651 let resp = match client.get(&url).send().await {
652 Ok(r) => r,
653 Err(e) => {
654 last_err = format!("download: {e}");
655 next_source!();
656 }
657 };
658 if !resp.status().is_success() {
659 last_err = format!("download failed: HTTP {}", resp.status());
660 next_source!();
661 }
662 let mut encrypted: Vec<u8> = Vec::with_capacity(
665 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
666 );
667 let mut stream = resp.bytes_stream();
668 while let Some(chunk) = stream.next().await {
669 let chunk = match chunk {
670 Ok(c) => c,
671 Err(e) => {
672 last_err = format!("read body: {e}");
673 next_source!();
674 }
675 };
676 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
677 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
678 }
679 encrypted.extend_from_slice(&chunk);
680 }
681 match crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce) {
682 Ok(plain) => {
683 if i > 1 {
684 log_net_info!("[Download] fallback source {}/{} served {}", i, candidates.len(), url);
685 }
686 return Ok(plain);
687 }
688 Err(e) => {
689 last_err = format!("decrypt: {e}");
692 next_source!();
693 }
694 }
695 }
696 log_net_fail!("[Download] all {} source(s) failed for {}: {}", candidates.len(), attachment.url, last_err);
697 Err(VectorError::Other(last_err))
698 }
699
700 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
702 let path = std::path::Path::new(file_path);
703 let bytes = std::fs::read(path)
704 .map_err(|e| VectorError::Io(e))?;
705 let filename = path.file_name()
706 .and_then(|n| n.to_str())
707 .unwrap_or("file");
708 let extension = path.extension()
709 .and_then(|e| e.to_str())
710 .unwrap_or("bin");
711
712 sending::send_file_dm(
713 to_npub,
714 std::sync::Arc::new(bytes),
715 filename,
716 extension,
717 None,
718 &SendConfig::default(),
719 Arc::new(NoOpSendCallback),
720 ).await.map_err(|e| VectorError::Other(e))
721 }
722
723 pub async fn send_reaction(
728 &self,
729 to_npub: &str,
730 reference_id: &str,
731 emoji: &str,
732 emoji_url: Option<&str>,
733 ) -> Result<String> {
734 use nostr_sdk::prelude::*;
735
736 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
737 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
738
739 let reference_event = EventId::from_hex(reference_id)
740 .map_err(|e| VectorError::Nostr(e.to_string()))?;
741 let receiver_pubkey = PublicKey::from_bech32(to_npub)
742 .map_err(|e| VectorError::Nostr(e.to_string()))?;
743
744 let custom_emoji_tag = emoji_url.and_then(|url| {
746 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
747 return None;
748 }
749 let shortcode = &emoji[1..emoji.len() - 1];
750 if shortcode.is_empty() { return None; }
751 Some(Tag::custom("emoji", [shortcode.to_string(), url.to_string()]))
752 });
753
754 let reaction_target = nostr_sdk::prelude::nip25::ReactionTarget {
755 event_id: reference_event,
756 public_key: receiver_pubkey,
757 coordinate: None,
758 kind: Some(Kind::PrivateDirectMessage),
759 relay_hint: None,
760 };
761 let mut builder = EventBuilder::reaction(reaction_target, emoji);
762 if let Some(tag) = custom_emoji_tag {
763 builder = builder.tag(tag);
764 }
765 let rumor = builder.finalize_unsigned_with_id(my_public_key);
766 let inner_rumor_id = rumor.id;
767 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
768
769 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
773 .await.map_err(VectorError::Other)?;
774 if !outcome.output.success.is_empty() {
775 if let Some(rid) = inner_rumor_id {
776 if let Err(e) = db::nip17_keys::store_wrap_key(
777 &outcome.wrap_event_id, &rid, &receiver_pubkey,
778 db::nip17_keys::WrapRole::Recipient,
779 &outcome.wrap_secret, &outcome.targeted_relays,
780 ) {
781 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
782 }
783 }
784 }
785
786 let self_wrap_client = client.clone();
789 let self_wrap_session = state::SessionGuard::capture();
790 tokio::spawn(async move {
791 if !self_wrap_session.is_valid() { return; }
792 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
793 &self_wrap_client, &my_public_key, rumor, [],
794 ).await {
795 if !self_wrap_session.is_valid() { return; }
796 if !self_outcome.output.success.is_empty() {
797 if let Some(rid) = inner_rumor_id {
798 let _ = db::nip17_keys::store_wrap_key(
799 &self_outcome.wrap_event_id, &rid, &my_public_key,
800 db::nip17_keys::WrapRole::SelfSend,
801 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
802 );
803 }
804 }
805 }
806 });
807
808 let reaction = Reaction {
810 id: rumor_id.clone(),
811 reference_id: reference_id.to_string(),
812 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
813 emoji: emoji.to_string(),
814 emoji_url: emoji_url.map(|s| s.to_string()),
815 };
816 let msg_for_save = {
817 let mut st = state::STATE.lock().await;
818 match st.add_reaction_to_message(reference_id, reaction) {
819 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
820 _ => None,
821 }
822 };
823 if let Some((cid, mut msg)) = msg_for_save {
824 let _ = db::events::save_message(&cid, &msg).await;
825 traits::emit_message_update(&cid, reference_id, &mut msg).await;
826 }
827
828 Ok(rumor_id)
829 }
830
831 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
834 use nostr_sdk::prelude::*;
835
836 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
837 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
838 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
839
840 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
841 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
842 .tag(Tag::public_key(pubkey))
843 .tag(Tag::custom("d", vec!["vector"]))
844 .tag(Tag::expiration(expiry))
845 .finalize_unsigned_with_id(my_public_key);
846
847 let signer = signer::active_signer().map_err(VectorError::Other)?;
849 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(pubkey, rumor.clone())
850 .extra_tags([Tag::expiration(expiry)])
851 .finalize_async(&signer)
852 .await
853 .map_err(|e| VectorError::Nostr(e.to_string()))?;
854 client
855 .send_event(&wrap)
856 .to(state::active_trusted_relays().await)
857 .await
858 .map_err(|e| VectorError::Nostr(e.to_string()))?;
859 Ok(())
860 }
861
862 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
866 use nostr_sdk::prelude::*;
867
868 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
869 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
870 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
871 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
872 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
873
874 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
876
877 let mut builder = EventBuilder::new(
878 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
879 new_content,
880 ).tag(Tag::event(reference_event));
881 for et in &emoji_tags {
882 builder = builder.tag(Tag::custom(
883 "emoji",
884 [et.shortcode.clone(), et.url.clone()],
885 ));
886 }
887 let rumor = builder.finalize_unsigned_with_id(my_public_key);
888 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
889 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
890
891 let msg_for_emit = {
893 let mut st = state::STATE.lock().await;
894 st.update_message_in_chat(to_npub, message_id, |msg| {
895 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
896 msg.preview_metadata = None;
897 })
898 };
899 if let Some(mut msg) = msg_for_emit {
900 traits::emit_message_update(to_npub, message_id, &mut msg).await;
901 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
902 let _ = db::events::save_edit_event(
903 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
904 ).await;
905 }
906 }
907
908 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
909 .await.map_err(VectorError::Other)?;
910
911 let self_wrap_client = client.clone();
912 let self_wrap_session = state::SessionGuard::capture();
913 tokio::spawn(async move {
914 if !self_wrap_session.is_valid() { return; }
915 let Ok(signer) = signer::active_signer() else { return };
916 if let Ok(wrap) = nostr_sdk::prelude::GiftWrapBuilder::new(my_public_key, rumor)
917 .finalize_async(&signer)
918 .await
919 {
920 let _ = self_wrap_client.send_event(&wrap).await;
921 }
922 });
923
924 Ok(edit_id)
925 }
926
927 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
929 use nostr_sdk::prelude::*;
930 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
931 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
932 }
933
934 pub async fn get_chats(&self) -> Vec<SerializableChat> {
936 let state = state::STATE.lock().await;
937 state.chats.iter()
938 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
939 .collect()
940 }
941
942 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
944 let state = state::STATE.lock().await;
945 if let Some(chat) = state.get_chat(chat_id) {
946 let msgs = chat.get_all_messages(&state.interner);
947 let start = offset.min(msgs.len());
948 let end = (offset + limit).min(msgs.len());
949 msgs[start..end].to_vec()
950 } else {
951 Vec::new()
952 }
953 }
954
955 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
957 let state = state::STATE.lock().await;
958 state.get_profile(npub)
959 .map(|p| SlimProfile::from_profile(p, &state.interner))
960 }
961
962 pub async fn load_profile(&self, npub: &str) -> bool {
964 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
965 }
966
967 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
969 profile::sync::update_profile(
970 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
971 &NoOpProfileSyncHandler,
972 ).await
973 }
974
975 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
978 profile::sync::update_bot_profile(
979 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
980 &NoOpProfileSyncHandler,
981 ).await
982 }
983
984 pub async fn update_status(&self, status: &str) -> bool {
986 profile::sync::update_status(status.to_string()).await
987 }
988
989 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
995 let path = std::path::Path::new(file_path);
996 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
997 if bytes.is_empty() {
998 return Err(VectorError::Other("Empty image file".into()));
999 }
1000 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1001 let mime = crate::crypto::mime_from_extension(&extension);
1002 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1003 let signer = crate::signer::active_signer()
1004 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1005 let servers = crate::blossom_servers::compute_enabled_servers();
1006 if servers.is_empty() {
1007 return Err(VectorError::Other("No Blossom servers configured".into()));
1008 }
1009 crate::blossom::upload_blob_with_failover(
1012 signer,
1013 servers,
1014 std::sync::Arc::new(bytes),
1015 Some(mime),
1016 Some(std::time::Duration::from_secs(20)),
1017 )
1018 .await
1019 .map_err(VectorError::Other)
1020 }
1021
1022 pub async fn block_user(&self, npub: &str) -> bool {
1024 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1025 }
1026
1027 pub async fn unblock_user(&self, npub: &str) -> bool {
1029 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1030 }
1031
1032 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1034 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1035 }
1036
1037 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1039 profile::sync::get_blocked_users().await
1040 }
1041
1042 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1044 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1045 }
1046
1047 pub fn my_npub(&self) -> Option<String> {
1049 state::my_public_key()
1050 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1051 }
1052
1053 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1060 use crate::community::ConcordProtocol;
1061 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1062 let mut out = Vec::new();
1063 for id in ids {
1064 match crate::db::community::community_protocol(&id).ok().flatten() {
1066 Some(ConcordProtocol::V2) => {
1067 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1068 let me = state::my_public_key();
1069 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1070 out.push(serde_json::json!({
1071 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1072 "version": 2,
1073 "name": c.name,
1074 "description": c.description,
1075 "is_owner": is_owner,
1076 "channels": c.channels.iter()
1077 .map(|ch| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0), "name": ch.name, "private": ch.private }))
1078 .collect::<Vec<_>>(),
1079 }));
1080 }
1081 }
1082 _ => {
1083 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1084 out.push(serde_json::json!({
1085 "community_id": c.id.to_hex(),
1086 "version": 1,
1087 "name": c.name,
1088 "description": c.description,
1089 "is_owner": crate::community::service::is_proven_owner(&c),
1090 "channels": c.channels.iter()
1091 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1092 .collect::<Vec<_>>(),
1093 }));
1094 }
1095 }
1096 }
1097 }
1098 out
1099 }
1100
1101 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1106 use crate::community::{v2::service as v2, transport::LiveTransport};
1107 let relays: Vec<String> = crate::state::active_trusted_relays()
1108 .await
1109 .iter()
1110 .map(|s| s.to_string())
1111 .collect();
1112 if relays.is_empty() {
1113 return Err(VectorError::Other("no relays available to host the Community".into()));
1114 }
1115 let session = state::SessionGuard::capture();
1116 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1117 let community = v2::create_community(&transport, name, relays, None)
1118 .await
1119 .map_err(VectorError::Other)?;
1120 self.register_v2_chats(&community, &session).await;
1121 if let Some(client) = state::nostr_client() {
1123 crate::community::v2::realtime::refresh_subscription(&client).await;
1124 }
1125 Ok(Self::v2_summary(&community))
1126 }
1127
1128 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1134 use crate::community::ConcordProtocol;
1135 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1136 return Ok(None);
1137 };
1138 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1139 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1140 Some(ConcordProtocol::V2) => Some(cid),
1141 _ => None,
1142 })
1143 }
1144
1145 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1147 let me = state::my_public_key();
1148 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1149 serde_json::json!({
1150 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1151 "version": 2,
1152 "name": community.name,
1153 "description": community.description,
1154 "is_owner": is_owner,
1155 "channels": community.channels.iter()
1156 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1157 .collect::<Vec<_>>(),
1158 })
1159 }
1160
1161 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1167 register_v2_chats_inner(community, session).await
1168 }
1169}
1170
1171pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1174 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1175 let me = state::my_public_key();
1176 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1177 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1178 let Some(primary) = community.primary_channel() else { return };
1181 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1182 let slims = {
1187 let mut st = state::STATE.lock().await;
1188 if !session.is_valid() {
1189 return; }
1191 let mut slims = Vec::new();
1192 for ch in &community.channels {
1193 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1194 st.upsert_community_chat(
1195 &ch_hex,
1196 &community.name,
1197 community.description.as_deref().unwrap_or(""),
1198 &id_hex,
1199 is_owner,
1200 community.icon.is_some(),
1201 owner_npub.as_deref(),
1202 Some(community.created_at_ms),
1203 community.dissolved,
1204 crate::community::ConcordProtocol::V2,
1205 &ch.name,
1206 &primary_hex,
1207 );
1208 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1209 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1210 }
1211 }
1212 slims
1213 };
1214 if !session.is_valid() {
1218 return;
1219 }
1220 for slim in &slims {
1221 let _ = crate::db::chats::save_slim_chat(slim);
1222 }
1223}
1224
1225impl VectorCore {
1226 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1230 use crate::community::{public_invite, service, transport::LiveTransport};
1231 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1235 let session = state::SessionGuard::capture();
1236 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1237 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1238 .await
1239 .map_err(VectorError::Other)?;
1240 self.register_v2_chats(&community, &session).await;
1241 if let Some(client) = state::nostr_client() {
1242 crate::community::v2::realtime::refresh_subscription(&client).await;
1243 }
1244 if crate::community::v2::realtime::follow_worker_running() {
1249 crate::community::v2::realtime::enqueue_follow(community.id());
1250 } else {
1251 let seed_session = state::SessionGuard::capture();
1252 let seed_community = community.clone();
1253 tokio::spawn(async move {
1254 if !seed_session.is_valid() {
1255 return;
1256 }
1257 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1258 if matches!(
1259 crate::community::v2::service::sync_guestbook(&transport, &seed_community, &seed_session).await,
1260 Ok(fresh) if !fresh.is_empty()
1261 ) {
1262 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1263 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1264 }
1265 });
1266 }
1267 return Ok(Self::v2_summary(&community));
1268 }
1269 let (relays, token) = public_invite::parse_invite_url(invite_url)
1270 .map_err(|e| VectorError::Other(e.to_string()))?;
1271 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1272 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1273 .await
1274 .map_err(VectorError::Other)?;
1275 let now = std::time::SystemTime::now()
1276 .duration_since(std::time::UNIX_EPOCH)
1277 .map(|d| d.as_secs())
1278 .unwrap_or(0);
1279 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1282 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1283 .await
1284 .map_err(VectorError::Other)?;
1285 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1286 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1289 self.finalize_member_join(community, &transport, attribution).await
1290 }
1291
1292 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1295 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1296 Ok(rows.iter().map(|p| {
1297 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1300 serde_json::json!({
1301 "community_id": p.community_id,
1302 "name": v2.name,
1303 "inviter_npub": p.inviter_npub,
1304 "version": 2,
1305 })
1306 } else {
1307 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1308 .ok().map(|i| i.name).unwrap_or_default();
1309 serde_json::json!({
1310 "community_id": p.community_id,
1311 "name": name,
1312 "inviter_npub": p.inviter_npub,
1313 "version": 1,
1314 })
1315 }
1316 }).collect())
1317 }
1318
1319 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1323 use crate::community::transport::LiveTransport;
1324 let bundle_json = crate::db::community::get_pending_invite(community_id)
1325 .map_err(VectorError::Other)?
1326 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1327 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1328
1329 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1331 let session = state::SessionGuard::capture();
1332 let inviter = crate::db::community::list_pending_invites()
1334 .ok()
1335 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1336 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
1344 .await
1345 .map_err(VectorError::Other)?;
1346 if !session.is_valid() {
1347 return Err(VectorError::Other("account changed during join".into()));
1348 }
1349 self.register_v2_chats(&community, &session).await;
1350 if let Some(client) = state::nostr_client() {
1351 crate::community::v2::realtime::refresh_subscription(&client).await;
1352 }
1353 crate::community::v2::realtime::enqueue_follow(community.id());
1354 let _ = crate::db::community::delete_pending_invite(community_id);
1355 return Ok(Self::v2_summary(&community));
1356 }
1357
1358 use crate::community::invite::{accept_invite, CommunityInvite};
1360 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1361 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1362 let now = std::time::SystemTime::now()
1366 .duration_since(std::time::UNIX_EPOCH)
1367 .map(|d| d.as_secs())
1368 .unwrap_or(0);
1369 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1370 .await
1371 .map_err(VectorError::Other)?;
1372 let summary = self.finalize_member_join(community, &transport, None).await?;
1374 let _ = crate::db::community::delete_pending_invite(community_id);
1375 Ok(summary)
1376 }
1377
1378 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1383 &self,
1384 community: crate::community::Community,
1385 transport: &T,
1386 attribution: Option<(String, Option<String>)>,
1387 ) -> Result<serde_json::Value> {
1388 use crate::community::service;
1389 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1396 return Ok(serde_json::json!({
1397 "community_id": v2,
1398 "version": 2,
1399 "migrated": true,
1400 }));
1401 }
1402 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1406 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1409 if c.removed {
1410 let _ = crate::db::community::delete_community(&community.id.to_hex());
1411 return Err(VectorError::Other("you have been removed from this community".into()));
1412 }
1413 }
1414 let community = crate::db::community::load_community(&community.id)
1415 .map_err(VectorError::Other)?
1416 .unwrap_or(community);
1417 let _ = service::fetch_and_apply_control(transport, &community).await;
1421 if service::am_i_banned(&community) {
1422 let _ = crate::db::community::delete_community(&community.id.to_hex());
1423 return Err(VectorError::Other("you are banned from this community".into()));
1424 }
1425 let community = crate::db::community::load_community(&community.id)
1427 .map_err(VectorError::Other)?
1428 .unwrap_or(community);
1429 let owner_npub = community
1430 .owner_attestation
1431 .as_ref()
1432 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1433 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1434 {
1435 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1436 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1437 let mut st = state::STATE.lock().await;
1438 for ch in &community.channels {
1439 st.upsert_community_chat(
1440 &ch.id.to_hex(),
1441 &community.name,
1442 community.description.as_deref().unwrap_or(""),
1443 &community.id.to_hex(),
1444 crate::community::service::is_proven_owner(&community),
1445 community.icon.is_some(),
1446 owner_npub.as_deref(),
1447 created_at_ms,
1448 community.dissolved,
1449 crate::community::ConcordProtocol::V1,
1450 &ch.name,
1451 &primary_hex,
1452 );
1453 }
1454 }
1455 if let Some(primary) = community.channels.first() {
1458 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1459 }
1460 Ok(serde_json::json!({
1461 "community_id": community.id.to_hex(),
1462 "version": 1,
1463 "name": community.name,
1464 "channels": community.channels.iter()
1465 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1466 .collect::<Vec<_>>(),
1467 }))
1468 }
1469
1470 pub async fn create_community(&self, name: &str) -> Result<serde_json::Value> {
1474 use crate::community::{service, transport::LiveTransport};
1475 let relays: Vec<String> = crate::state::active_trusted_relays()
1476 .await
1477 .iter()
1478 .map(|s| s.to_string())
1479 .collect();
1480 if relays.is_empty() {
1481 return Err(VectorError::Other("no relays available to host the Community".into()));
1482 }
1483 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1484 let community = service::create_community(&transport, name, "general", relays)
1485 .await
1486 .map_err(VectorError::Other)?;
1487 let owner_npub = community
1488 .owner_attestation
1489 .as_ref()
1490 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1491 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1492 {
1493 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1494 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1495 let mut st = state::STATE.lock().await;
1496 for ch in &community.channels {
1497 st.upsert_community_chat(
1498 &ch.id.to_hex(),
1499 &community.name,
1500 community.description.as_deref().unwrap_or(""),
1501 &community.id.to_hex(),
1502 crate::community::service::is_proven_owner(&community),
1503 community.icon.is_some(),
1504 owner_npub.as_deref(),
1505 created_at_ms,
1506 community.dissolved,
1507 crate::community::ConcordProtocol::V1,
1508 &ch.name,
1509 &primary_hex,
1510 );
1511 }
1512 }
1513 Ok(serde_json::json!({
1514 "community_id": community.id.to_hex(),
1515 "version": 1,
1516 "name": community.name,
1517 "channels": community.channels.iter()
1518 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1519 .collect::<Vec<_>>(),
1520 }))
1521 }
1522
1523 pub async fn create_public_invite(&self, community_id: &str) -> Result<String> {
1525 use crate::community::{service, transport::LiveTransport, CommunityId};
1526 if community_id.len() != 64 {
1527 return Err(VectorError::Other("malformed community id".into()));
1528 }
1529 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1530 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1532 crate::db::community::community_protocol(&cid).ok()
1533 {
1534 let community = crate::db::community::load_community_v2(&cid)
1535 .map_err(VectorError::Other)?
1536 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1537 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1538 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1541 let minted = crate::community::v2::service::mint_public_link(&transport, &community, base, None, None)
1542 .await
1543 .map_err(VectorError::Other)?;
1544 return Ok(minted.url);
1545 }
1546 let community = crate::db::community::load_community(&CommunityId(
1547 crate::simd::hex::hex_to_bytes_32(community_id),
1548 ))
1549 .map_err(VectorError::Other)?
1550 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1551 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1552 let (_token, url) = service::create_public_invite(&transport, &community, None, None)
1553 .await
1554 .map_err(VectorError::Other)?;
1555 Ok(url)
1556 }
1557
1558 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1562 use crate::community::{service, CommunityId};
1563 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1564
1565 let session = crate::state::SessionGuard::capture();
1566 let my_pk = crate::state::my_public_key()
1567 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1568
1569 if community_id.len() != 64 {
1570 return Err(VectorError::Other("malformed community id".into()));
1571 }
1572 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1573 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1579 crate::db::community::community_protocol(&cid).ok()
1580 {
1581 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1582 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1583 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1584 let bundle = {
1592 let lock = crate::community::v2::realtime::follow_lock(&cid);
1593 let _rotation = lock.lock().await;
1594 let community = crate::db::community::load_community_v2(&cid)
1595 .map_err(VectorError::Other)?
1596 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1597 crate::community::v2::service::bundle_of(&community, Some(my_pk), None, None)
1598 };
1599 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1600 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1603 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1604 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1605 let rumor = nostr_sdk::prelude::EventBuilder::new(
1606 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1607 bundle_json,
1608 )
1609 .tag(expiry_tag.clone())
1610 .finalize_unsigned_with_id(my_pk);
1611 let k_tag = nostr_sdk::prelude::Tag::custom(
1612 "k",
1613 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1614 );
1615 if !session.is_valid() {
1616 return Err(VectorError::Other("account changed".into()));
1617 }
1618 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1619 .await
1620 .map_err(VectorError::Other)?;
1621 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1622 }
1623 let community = crate::db::community::load_community(&CommunityId(
1624 crate::simd::hex::hex_to_bytes_32(community_id),
1625 ))
1626 .map_err(VectorError::Other)?
1627 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1628
1629 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1630 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1631 }
1632 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1633 .map_err(|_| VectorError::Other("invalid npub".into()))?
1634 .to_hex();
1635 if crate::db::community::get_community_banlist(community_id)
1636 .map_err(VectorError::Other)?
1637 .iter()
1638 .any(|b| b == &invitee_hex)
1639 {
1640 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1641 }
1642
1643 if !session.is_valid() {
1645 return Err(VectorError::Other("account changed during invite".into()));
1646 }
1647
1648 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1649 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1650 .map_err(VectorError::Other)?;
1651 let pending_id = format!("community-invite-{}", community_id);
1652 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1654 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1655
1656 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1657 .await
1658 .map_err(VectorError::Other)?;
1659
1660 Ok(serde_json::json!({
1661 "community_id": community_id,
1662 "invitee": invitee_npub,
1663 "wrap_event_id": result.event_id,
1664 }))
1665 }
1666
1667 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1672 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1673 }
1674
1675 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1680 use crate::community::{service, transport::LiveTransport, CommunityId};
1681 if community_id.len() != 64 {
1682 return Err(VectorError::Other("malformed community id".into()));
1683 }
1684 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1685 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1686 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1689 let community = crate::db::community::load_community_v2(&cid)
1690 .map_err(VectorError::Other)?
1691 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1692 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1693 .await
1694 .map_err(VectorError::Other);
1695 }
1696 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1697 let community = crate::db::community::load_community(&cid)
1698 .map_err(VectorError::Other)?
1699 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1700 service::revoke_public_invite(&transport, &community, &token_bytes)
1701 .await
1702 .map_err(VectorError::Other)
1703 }
1704
1705 pub async fn send_community_message(
1707 &self,
1708 channel_id: &str,
1709 content: &str,
1710 replied_to: Option<&str>,
1711 ) -> Result<String> {
1712 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1713 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1715 let community = crate::db::community::load_community_v2(&id)
1716 .map_err(VectorError::Other)?
1717 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1718 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1719 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1720 let reply = match replied_to.filter(|r| !r.is_empty()) {
1723 Some(parent_id) => {
1724 let author_hex = {
1725 let st = state::STATE.lock().await;
1726 st.find_message(parent_id)
1727 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1728 .map(|pk| pk.to_hex())
1729 .unwrap_or_default()
1730 };
1731 Some((parent_id.to_string(), author_hex))
1732 }
1733 None => None,
1734 };
1735 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1736 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1739 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1740 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1741 .await
1742 .map_err(VectorError::Other);
1743 }
1744 let (community, channel) = self.resolve_channel(channel_id)?;
1745 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1746 let reply = replied_to.filter(|r| !r.is_empty());
1747 let ms = std::time::SystemTime::now()
1748 .duration_since(std::time::UNIX_EPOCH)
1749 .map(|d| d.as_millis() as u64)
1750 .unwrap_or(0);
1751 let unsigned = envelope::build_inner_typed(
1752 author_pk,
1753 &channel.id,
1754 channel.epoch,
1755 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1756 content,
1757 ms,
1758 reply,
1759 &[],
1760 );
1761 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1762 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1763 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1764 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1765 let session = state::SessionGuard::capture();
1766 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1767 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1768 .await
1769 .map_err(VectorError::Other)?;
1770 if !session.is_valid() {
1773 return Ok(message_id);
1774 }
1775 let echoed = {
1776 let mut st = state::STATE.lock().await;
1777 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1778 };
1779 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1780 let _ = crate::db::events::save_message(channel_id, &msg).await;
1781 }
1782 Ok(message_id)
1783 }
1784
1785 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1789 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1790 let path = std::path::Path::new(file_path);
1791 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1792 if bytes.is_empty() {
1793 return Err(VectorError::Other("Empty file".into()));
1794 }
1795 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1796 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1797
1798 let session = state::SessionGuard::capture();
1801 let v2_target = match self.v2_community_for_channel(channel_id)? {
1804 Some(id) => Some(
1805 crate::db::community::load_community_v2(&id)
1806 .map_err(VectorError::Other)?
1807 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1808 ),
1809 None => None,
1810 };
1811 let v1_target = match v2_target {
1812 Some(_) => None,
1813 None => Some(self.resolve_channel(channel_id)?),
1814 };
1815 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1816
1817 let file_hash = crate::crypto::sha256_hex(&bytes);
1818 let mime = crate::crypto::mime_from_extension(&extension);
1819 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1820
1821 let download_dir = crate::db::get_download_dir();
1823 let _ = std::fs::create_dir_all(&download_dir);
1824 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1825 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1826 let _ = std::fs::write(&local_path, &bytes);
1827
1828 let params = crate::crypto::generate_encryption_params();
1830 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1831 let encrypted_size = encrypted.len() as u64;
1832
1833 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1834 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1835 let servers = crate::blossom_servers::compute_enabled_servers();
1836 if servers.is_empty() {
1837 return Err(VectorError::Other("No Blossom servers configured".into()));
1838 }
1839 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1840 let url = crate::blossom::upload_blob_with_progress_and_failover(
1841 signer.clone(),
1842 servers,
1843 std::sync::Arc::new(encrypted),
1844 Some(mime),
1845 true,
1846 noop_progress,
1847 Some(3),
1848 Some(std::time::Duration::from_secs(2)),
1849 None,
1850 ).await.map_err(VectorError::Other)?;
1851
1852 let attachment = crate::types::Attachment {
1853 id: file_hash.clone(),
1854 key: params.key.clone(),
1855 nonce: params.nonce.clone(),
1856 extension: extension.clone(),
1857 name: filename.clone(),
1858 url,
1859 path: local_path.to_string_lossy().to_string(),
1860 size: encrypted_size,
1861 img_meta,
1862 downloading: false,
1863 downloaded: true,
1864 ..Default::default()
1865 };
1866 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
1867
1868 if !session.is_valid() {
1870 return Err(VectorError::Other("account changed during upload".into()));
1871 }
1872 if let Some(community) = v2_target {
1874 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1875 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1876 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
1877 .await
1878 .map_err(VectorError::Other);
1879 }
1880 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
1881 let ms = std::time::SystemTime::now()
1882 .duration_since(std::time::UNIX_EPOCH)
1883 .map(|d| d.as_millis() as u64)
1884 .unwrap_or(0);
1885 let unsigned = envelope::build_inner_full(
1886 author_pk, &channel.id, channel.epoch,
1887 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
1888 );
1889 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1890 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1891 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
1892 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1893 .await.map_err(VectorError::Other)?;
1894 let echoed = {
1896 let mut st = state::STATE.lock().await;
1897 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1898 };
1899 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
1900 let _ = crate::db::events::save_message(channel_id, &m).await;
1901 }
1902 Ok(message_id)
1903 }
1904
1905 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
1907 use crate::community::{service, transport::LiveTransport};
1908 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1909 let community = crate::db::community::load_community_v2(&id)
1910 .map_err(VectorError::Other)?
1911 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1912 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1913 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1914 return crate::community::v2::service::send_typing(&transport, &community, &ch)
1915 .await
1916 .map_err(VectorError::Other);
1917 }
1918 let (community, channel) = self.resolve_channel(channel_id)?;
1919 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
1920 service::publish_typing_signal(&transport, &community, &channel)
1921 .await
1922 .map_err(VectorError::Other)
1923 }
1924
1925 pub async fn send_community_reaction(
1928 &self,
1929 channel_id: &str,
1930 message_id: &str,
1931 emoji: &str,
1932 emoji_url: Option<&str>,
1933 ) -> Result<()> {
1934 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
1935 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
1936 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
1937 }
1938 _ => Vec::new(),
1939 };
1940 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1941 let session = state::SessionGuard::capture();
1942 let community = crate::db::community::load_community_v2(&id)
1943 .map_err(VectorError::Other)?
1944 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1945 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1946 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1947 let held = {
1952 let st = state::STATE.lock().await;
1953 st.find_message(message_id)
1954 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1955 };
1956 let held = held.or_else(|| {
1957 crate::db::events::event_author(message_id)
1958 .ok()
1959 .flatten()
1960 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
1961 });
1962 let target_author = match held {
1963 Some(pk) => pk,
1964 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
1965 .await
1966 .map_err(VectorError::Other)?
1967 .iter()
1968 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
1969 .map(|f| f.event.opened().author)
1970 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
1971 };
1972 if !session.is_valid() {
1974 return Err(VectorError::Other("account changed before send".into()));
1975 }
1976 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
1977 return crate::community::v2::service::send_reaction(
1982 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
1983 )
1984 .await
1985 .map(|_| ())
1986 .map_err(VectorError::Other);
1987 }
1988 self.publish_community_control(
1989 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
1990 ).await
1991 }
1992
1993 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
1995 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
1996 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1997 let community = crate::db::community::load_community_v2(&id)
1998 .map_err(VectorError::Other)?
1999 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2000 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2001 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2002 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2003 .await
2004 .map(|_| ())
2005 .map_err(VectorError::Other);
2006 }
2007 self.publish_community_control(
2008 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2009 ).await
2010 }
2011
2012 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2016 let channel_id = {
2017 let st = state::STATE.lock().await;
2018 match st.find_message(message_id) {
2019 Some((chat, _)) => chat.id.clone(),
2020 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2021 }
2022 };
2023 self.delete_community_message_in(&channel_id, message_id).await
2024 }
2025
2026 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2030 use crate::community::{service, transport::LiveTransport};
2031 let session = state::SessionGuard::capture();
2032 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2033
2034 let attachment_urls: Vec<String> = {
2037 let st = state::STATE.lock().await;
2038 st.find_message(message_id)
2039 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2040 .unwrap_or_default()
2041 };
2042
2043 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2044 let community = crate::db::community::load_community_v2(&id)
2047 .map_err(VectorError::Other)?
2048 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2049 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2050 crate::community::v2::service::send_delete(
2051 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2052 )
2053 .await
2054 .map_err(VectorError::Other)?;
2055 } else {
2056 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2058 let _ = service::delete_message(&transport, message_id).await;
2059 }
2060 self.publish_community_control(
2062 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2063 ).await?;
2064 }
2065 if !attachment_urls.is_empty() {
2067 if let Some(_client) = state::nostr_client() {
2068 if let Ok(signer) = crate::signer::active_signer() {
2069 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2070 }
2071 }
2072 }
2073 if !session.is_valid() {
2076 return Ok(());
2077 }
2078 let removed_chat = {
2079 let mut st = state::STATE.lock().await;
2080 st.remove_message(message_id).map(|(cid, _)| cid)
2081 };
2082 let _ = crate::db::events::delete_event(message_id).await;
2083 traits::emit_event_json("message_removed", serde_json::json!({
2084 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2085 }));
2086 Ok(())
2087 }
2088
2089 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2095 use crate::community::transport::LiveTransport;
2096 let session = state::SessionGuard::capture();
2097 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2098
2099 let author_npub = {
2102 let st = state::STATE.lock().await;
2103 st.find_message(message_id).and_then(|(_, m)| m.npub)
2104 };
2105 let author_npub = match author_npub {
2106 Some(n) => n,
2107 None => crate::db::events::event_author(message_id)
2108 .ok()
2109 .flatten()
2110 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2111 };
2112 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2113 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2114
2115 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2116 let community = crate::db::community::load_community_v2(&id)
2117 .map_err(VectorError::Other)?
2118 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2119 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2120 crate::community::v2::service::moderation_delete(
2121 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2122 )
2123 .await
2124 .map_err(VectorError::Other)?;
2125 } else {
2126 let cid = crate::db::community::community_id_for_channel(channel_id)
2127 .map_err(VectorError::Other)?
2128 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2129 let community = crate::db::community::load_community(&crate::community::CommunityId(
2130 crate::simd::hex::hex_to_bytes_32(&cid),
2131 ))
2132 .map_err(VectorError::Other)?
2133 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2134 let channel = community
2135 .channels
2136 .iter()
2137 .find(|c| c.id.to_hex() == channel_id)
2138 .cloned()
2139 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2140 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2141 .await
2142 .map_err(VectorError::Other)?;
2143 }
2144
2145 if !session.is_valid() {
2148 return Ok(());
2149 }
2150 let removed_chat = {
2151 let mut st = state::STATE.lock().await;
2152 st.remove_message(message_id).map(|(cid, _)| cid)
2153 };
2154 let _ = crate::db::events::delete_event(message_id).await;
2155 traits::emit_event_json("message_removed", serde_json::json!({
2156 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2157 }));
2158 Ok(())
2159 }
2160
2161 async fn publish_community_control(
2164 &self,
2165 channel_id: &str,
2166 kind: u16,
2167 content: &str,
2168 target: &str,
2169 emoji_tags: &[crate::types::EmojiTag],
2170 ) -> Result<()> {
2171 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2172 let (community, channel) = self.resolve_channel(channel_id)?;
2173 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2174 let ms = std::time::SystemTime::now()
2175 .duration_since(std::time::UNIX_EPOCH)
2176 .map(|d| d.as_millis() as u64)
2177 .unwrap_or(0);
2178 let unsigned = envelope::build_inner_typed(
2179 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2180 );
2181 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2182 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2183 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2184 let session = state::SessionGuard::capture();
2185 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2186 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2187 .await.map_err(VectorError::Other)?;
2188 if !session.is_valid() {
2191 return Ok(());
2192 }
2193 let outcome = {
2194 let mut st = state::STATE.lock().await;
2195 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2196 };
2197 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2198 if let Some(ev) = edit_event {
2199 let mut ev = (*ev).clone();
2200 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2201 let _ = crate::db::events::save_event(&ev).await;
2202 } else {
2203 let _ = crate::db::events::save_message(channel_id, &message).await;
2204 }
2205 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2206 }
2207 Ok(())
2208 }
2209
2210 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2218 use crate::community::{inbound, send, service, transport::LiveTransport};
2219 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2220 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2225 let warnings = if community::v2::realtime::follow_worker_running() {
2226 community::v2::realtime::enqueue_follow(&id);
2227 Vec::new()
2228 } else {
2229 Self::v2_inline_follow(&id).await
2230 };
2231 let new = Self::v2_backfill_channel(
2236 &id, channel_id, limit, 8, None,
2237 crate::community::transport::Evidence::Fast, 12,
2238 ).await;
2239 return Ok((new, warnings));
2240 }
2241 let (community, _) = self.resolve_channel(channel_id)?;
2242 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2243 let mut warnings: Vec<String> = Vec::new();
2244
2245 match service::catch_up_server_root(&transport, &community).await {
2253 Ok(c) if c.removed => {
2254 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2256 return Ok((0, warnings));
2257 }
2258 Ok(_) => {}
2259 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2260 }
2261 let (community, _) = self.resolve_channel(channel_id)?;
2262
2263 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2269 warnings.push(format!("control fold failed: {e}"));
2270 }
2271 if service::am_i_banned(&community) {
2272 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2274 return Ok((0, warnings));
2275 }
2276 let (community, channel) = self.resolve_channel(channel_id)?;
2279 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2280 warnings.push(format!("channel catch-up failed: {e}"));
2281 }
2282 let (community, _) = self.resolve_channel(channel_id)?;
2286 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2287 warnings.push(format!("read-cut resume failed: {e}"));
2288 }
2289 let (community, channel) = self.resolve_channel(channel_id)?;
2290
2291 let session = state::SessionGuard::capture();
2293 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2294 .await
2295 .map_err(VectorError::Other)?;
2296 let outcomes = {
2297 let mut st = state::STATE.lock().await;
2298 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2299 };
2300 let mut new = 0usize;
2301 let mut pending: Vec<&crate::types::Message> = Vec::new();
2305 for o in &outcomes {
2306 if !session.is_valid() {
2308 pending.clear();
2309 break;
2310 }
2311 match o {
2312 inbound::IncomingEvent::NewMessage(m) => {
2313 pending.push(m);
2314 new += 1;
2315 }
2316 inbound::IncomingEvent::Updated { message, .. } => {
2317 pending.push(message);
2318 }
2319 inbound::IncomingEvent::Removed { target_id } => {
2320 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2321 let _ = crate::db::events::delete_event(target_id).await;
2322 }
2323 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2324 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2327 let _ = crate::db::events::delete_event(reaction_id).await;
2328 }
2329 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2330 let et = if *joined {
2331 crate::stored_event::SystemEventType::MemberJoined
2332 } else {
2333 crate::stored_event::SystemEventType::MemberLeft
2334 };
2335 let note = invited_by.as_ref().map(|by| match invited_label {
2337 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2338 _ => by.clone(),
2339 });
2340 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;
2341 }
2342 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2343 community::service::persist_webxdc_signal(
2346 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2347 ).await;
2348 }
2349 inbound::IncomingEvent::Kicked { community_id }
2350 | inbound::IncomingEvent::SelfLeft { community_id } => {
2351 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2356 let _ = crate::db::community::delete_community_retain_keys(community_id);
2357 break;
2358 }
2359 inbound::IncomingEvent::Typing { .. } => {
2360 }
2362 }
2363 }
2364 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2365 Ok((new, warnings))
2366 }
2367
2368 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2380 use crate::bot_interface::{self, ChatCommandsSnapshot};
2381 use nostr_sdk::prelude::ToBech32;
2382
2383 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2384 let mut relays: Vec<String> = Vec::new();
2385 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2386 if let Some(cid_hex) = community_hex {
2387 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2388 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2389 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2390 relays = community.relays.clone();
2391 } else {
2392 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2393 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2394 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2395 };
2396 relays = community.relays.clone();
2397 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2398 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2399 members.push(pk);
2400 }
2401 }
2402 }
2403 let state = crate::state::STATE.lock().await;
2404 for pk in members {
2405 let Ok(npub) = pk.to_bech32();
2406 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2407 bots.push(pk);
2408 }
2409 }
2410 } else if chat_id.starts_with("npub1") {
2411 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2412 let is_bot = {
2413 let state = crate::state::STATE.lock().await;
2414 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2415 };
2416 if is_bot {
2417 bots.push(pk);
2418 if let Some(client) = crate::state::nostr_client() {
2421 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2422 }
2423 }
2424 }
2425 }
2426
2427 if bots.is_empty() {
2428 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2429 }
2430 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2433 relays.sort();
2434 relays.dedup();
2435 bots.sort_by_key(|p| p.to_hex());
2438 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2439 let commands = bot_interface::assemble_from_store(&bot_hexes);
2440 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2441 if !fresh {
2442 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2443 }
2444 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2445 }
2446
2447 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2452 use nostr_sdk::prelude::ToBech32;
2453 match Self::load_v2_if_v2(community_id) {
2459 Ok(Some(community)) => {
2460 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2461 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2462 if cursor == 0 {
2463 if crate::community::v2::realtime::follow_worker_running() {
2464 crate::community::v2::realtime::enqueue_follow(community.id());
2465 } else {
2466 let session = state::SessionGuard::capture();
2467 let c2 = community.clone();
2468 tokio::spawn(async move {
2469 if !session.is_valid() {
2470 return;
2471 }
2472 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2473 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2, &session).await, Ok(fresh) if !fresh.is_empty()) {
2474 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2475 }
2476 });
2477 }
2478 }
2479 return crate::community::v2::service::stored_memberlist(&community)
2480 .unwrap_or_default()
2481 .into_iter()
2482 .filter_map(|pk| pk.to_bech32().ok())
2483 .map(|npub| serde_json::json!({ "npub": npub }))
2484 .collect();
2485 }
2486 Ok(None) => {} Err(_) => return Vec::new(),
2489 }
2490 crate::db::community::community_member_activity(community_id)
2491 .unwrap_or_default()
2492 .into_iter()
2493 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2494 .collect()
2495 }
2496
2497 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2501 use crate::community::transport::LiveTransport;
2502 let session = state::SessionGuard::capture();
2503 let lock = crate::community::v2::realtime::follow_lock(id);
2508 let _guard = lock.lock().await;
2509 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2510 let mut warnings: Vec<String> = Vec::new();
2511 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2512 warnings.push("v2 community not found".to_string());
2513 return warnings;
2514 };
2515 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2516 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2517 Ok(f) if f.dissolved => return warnings,
2519 Ok(f) if f.self_removed => {
2520 if session.is_valid() {
2523 let _ = crate::db::community::delete_community(&cid_hex);
2524 }
2525 return warnings;
2526 }
2527 Ok(_) => {}
2528 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2529 }
2530 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2531 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
2532 Ok(Some(changed)) => {
2536 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2537 warnings.push(format!("v2 rekey follow failed: {e}"));
2538 }
2539 }
2540 Ok(None) => {}
2541 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2542 }
2543 }
2544 if let Some(me) = crate::my_public_key() {
2549 if crate::db::community::is_author_banned(&cid_hex, &me) && session.is_valid() {
2550 let _ = crate::db::community::delete_community(&cid_hex);
2551 }
2552 }
2553 warnings
2554 }
2555
2556 async fn v2_backfill_channel(
2568 id: &crate::community::CommunityId,
2569 channel_id: &str,
2570 limit: usize,
2571 max_pages: usize,
2572 since: Option<u64>,
2573 evidence: crate::community::transport::Evidence,
2574 transport_secs: u64,
2575 ) -> usize {
2576 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2577 let session = state::SessionGuard::capture();
2580 let Some(my_pk) = state::my_public_key() else { return 0 };
2581 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2585 return 0;
2586 }
2587 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
2588 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2589 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
2590 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2591 &transport,
2592 &community,
2593 &ch,
2594 limit.max(50),
2595 max_pages,
2596 since,
2597 evidence,
2598 |page| {
2603 let mut saw_message = false;
2604 for f in page {
2605 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2606 saw_message = true;
2607 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2608 return true;
2609 }
2610 }
2611 }
2612 !saw_message
2613 },
2614 )
2615 .await
2616 else {
2617 return 0;
2618 };
2619 Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await
2620 }
2621
2622 pub(crate) async fn v2_ingest_chat_page(
2626 channel_id: &str,
2627 my_pk: nostr_sdk::prelude::PublicKey,
2628 session: crate::state::SessionGuard,
2629 page: Vec<crate::community::v2::service::FetchedEvent>,
2630 ) -> usize {
2631 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2632 let mut new = 0usize;
2633 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
2635 for f in &page {
2636 if !session.is_valid() {
2638 break;
2639 }
2640 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2645 if opened.author != my_pk {
2646 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2647 let Ok(npub) = ToBech32::to_bech32(&opened.author);
2648 crate::community::service::persist_webxdc_signal(
2649 channel_id,
2650 &npub,
2651 &topic,
2652 addr.as_deref(),
2653 &opened.rumor_id.to_hex(),
2654 opened.at_ms / 1000,
2655 )
2656 .await;
2657 }
2658 }
2659 continue;
2660 }
2661 let outcome = {
2662 let mut st = state::STATE.lock().await;
2663 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2664 };
2665 if let Some(outcome) = outcome {
2666 if matches!(outcome, ChatPersist::New(_)) {
2667 new += 1;
2668 }
2669 outcomes.push(outcome);
2670 }
2671 }
2672 let mut pending: Vec<&crate::types::Message> = Vec::new();
2676 for outcome in &outcomes {
2677 if !session.is_valid() {
2678 pending.clear();
2679 break;
2680 }
2681 match outcome {
2682 ChatPersist::New(m) => pending.push(m),
2683 ChatPersist::Updated { message, edit_event } => match edit_event {
2684 Some(ev) => {
2685 let mut ev = (**ev).clone();
2686 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
2689 ev.chat_id = cid;
2690 }
2691 let _ = crate::db::events::save_event(&ev).await;
2692 }
2693 None => pending.push(message),
2694 },
2695 ChatPersist::Removed(target_id) => {
2696 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2697 let _ = crate::db::events::delete_event(target_id).await;
2698 }
2699 ChatPersist::ReactionRemoved { reaction_id, message } => {
2700 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2701 let _ = crate::db::events::delete_event(reaction_id).await;
2702 pending.push(message);
2703 }
2704 }
2705 }
2706 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2707 if session.is_valid() {
2713 for outcome in &outcomes {
2714 match outcome {
2715 ChatPersist::New(msg) => crate::traits::emit_event(
2716 "message_new",
2717 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
2718 ),
2719 ChatPersist::Updated { message, .. }
2720 | ChatPersist::ReactionRemoved { message, .. } => {
2721 let mut message = message.clone();
2722 let target_id = message.id.clone();
2723 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
2724 }
2725 ChatPersist::Removed(target_id) => crate::traits::emit_event(
2726 "message_removed",
2727 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
2728 ),
2729 }
2730 }
2731 }
2732 new
2733 }
2734
2735 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2739 if community_id.len() != 64 {
2740 return Ok(None);
2741 }
2742 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2743 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
2744 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
2745 _ => Ok(None),
2746 }
2747 }
2748
2749 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
2754 use crate::community::CommunityId;
2755 if community_id.len() != 64 {
2756 return Err(VectorError::Other("malformed community id".into()));
2757 }
2758 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
2759 .map_err(VectorError::Other)?
2760 .ok_or_else(|| VectorError::Other("community not found".into()))
2761 }
2762
2763 fn admin_role_id_of(community_id: &str) -> Result<String> {
2764 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2765 roles.roles.iter()
2766 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
2767 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
2768 .map(|r| r.role_id.clone())
2769 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
2770 }
2771
2772 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
2776 use crate::community::service;
2777 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2778 use crate::community::roles::Permissions;
2779 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
2780 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
2781 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2782 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2785 if banned.contains(&me) && me != owner_hex {
2786 return Ok(serde_json::json!({
2787 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
2788 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
2789 }));
2790 }
2791 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
2792 return Ok(serde_json::json!({
2793 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
2794 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
2795 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
2796 "manage_admin_role": me == owner_hex,
2798 }));
2799 }
2800 let community = Self::load_community_hex(community_id)?;
2801 let caps = service::caller_capabilities(&community);
2802 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
2803 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
2804 .unwrap_or(false);
2805 Ok(serde_json::json!({
2806 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
2807 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
2808 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
2809 "manage_admin_role": manage_admin_role,
2810 }))
2811 }
2812
2813 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
2816 use nostr_sdk::prelude::{PublicKey, ToBech32};
2817 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2818 let owner = v2.owner().map_err(VectorError::Other)?;
2819 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2820 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2822 let admins: Vec<String> = roster.grants.iter()
2823 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
2824 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2825 .collect();
2826 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
2827 }
2828 let community = Self::load_community_hex(community_id)?;
2829 let owner = community.owner_attestation.as_ref()
2830 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
2831 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
2832 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2833 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
2834 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2835 .collect();
2836 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
2837 }
2838
2839 async fn converge_v2_authority(
2848 transport: &crate::community::transport::LiveTransport,
2849 community_id: &str,
2850 session: &crate::state::SessionGuard,
2851 ) {
2852 if !session.is_valid() {
2853 return;
2854 }
2855 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
2858 let _ = crate::community::v2::service::follow_control(transport, &fresh, session).await;
2859 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh, session).await {
2864 if !added.is_empty() && session.is_valid() {
2865 traits::emit_event_json(
2866 "community_refreshed",
2867 serde_json::json!({ "community_id": community_id }),
2868 );
2869 }
2870 }
2871 }
2872 }
2873
2874 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2876 use crate::community::{service, transport::LiveTransport};
2877 let session = crate::state::SessionGuard::capture();
2878 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2879 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2880 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2881 crate::community::v2::service::grant_admin(&transport, &v2, &member)
2882 .await
2883 .map_err(VectorError::Other)?;
2884 Self::converge_v2_authority(&transport, community_id, &session).await;
2885 return Ok(());
2886 }
2887 let community = Self::load_community_hex(community_id)?;
2888 let role_id = Self::admin_role_id_of(community_id)?;
2889 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2890 }
2891
2892 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
2894 use crate::community::{service, transport::LiveTransport};
2895 let session = crate::state::SessionGuard::capture();
2896 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2897 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2898 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2899 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
2900 .await
2901 .map_err(VectorError::Other)?;
2902 Self::converge_v2_authority(&transport, community_id, &session).await;
2903 return Ok(());
2904 }
2905 let community = Self::load_community_hex(community_id)?;
2906 let role_id = Self::admin_role_id_of(community_id)?;
2907 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
2908 }
2909
2910 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
2912 use crate::community::{service, transport::LiveTransport};
2913 let session = crate::state::SessionGuard::capture();
2914 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2915 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2916 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2917 crate::community::v2::service::kick_member(&transport, &v2, &pk)
2918 .await
2919 .map_err(VectorError::Other)?;
2920 if session.is_valid() {
2925 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2, &session).await {
2926 if !fresh.is_empty() {
2927 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
2928 }
2929 }
2930 }
2931 Self::converge_v2_authority(&transport, community_id, &session).await;
2932 return Ok(());
2933 }
2934 let community = Self::load_community_hex(community_id)?;
2935 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
2936 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
2937 }
2938
2939 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
2942 use crate::community::{service, transport::LiveTransport, CommunityId};
2943 let session = crate::state::SessionGuard::capture();
2944 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
2945 let hex = pk.to_hex();
2946 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2947 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
2949 list.retain(|h| h != &hex);
2950 if banned {
2951 list.push(hex);
2952 }
2953 if community_id.len() == 64 {
2957 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2958 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2959 let community = {
2966 let lock = crate::community::v2::realtime::follow_lock(&cid);
2967 let _rotation = lock.lock().await;
2968 let community = crate::db::community::load_community_v2(&cid)
2969 .map_err(VectorError::Other)?
2970 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2971 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
2972 if banned {
2973 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
2974 }
2975 community
2976 };
2977 if banned {
2978 crate::community::v2::service::refound_community(&transport, &community, &[pk]).await.map_err(VectorError::Other)?;
2979 }
2980 Self::converge_v2_authority(&transport, community_id, &session).await;
2981 return Ok(());
2982 }
2983 }
2984 let community = Self::load_community_hex(community_id)?;
2985 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
2986 }
2987
2988 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
2992 use crate::community::{service, transport::LiveTransport, CommunityId};
2993 if community_id.len() != 64 {
2994 return Err(VectorError::Other("malformed community id".into()));
2995 }
2996 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2997 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2998 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3001 let community = crate::db::community::load_community_v2(&cid)
3002 .map_err(VectorError::Other)?
3003 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3004 return crate::community::v2::service::dissolve_community(&transport, &community)
3005 .await
3006 .map_err(VectorError::Other);
3007 }
3008 let community = Self::load_community_hex(community_id)?;
3009 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
3010 }
3011
3012 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
3015 use crate::community::{service, transport::LiveTransport, CommunityId};
3016 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3017 if community_id.len() == 64 {
3022 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3023 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3024 let community = crate::db::community::load_community_v2(&cid)
3025 .map_err(VectorError::Other)?
3026 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3027 let mut meta = community.metadata();
3028 if let Some(n) = name {
3029 meta.name = n.to_string();
3030 }
3031 if let Some(d) = description {
3032 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
3033 }
3034 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
3035 .await
3036 .map_err(VectorError::Other);
3037 }
3038 }
3039 let mut community = Self::load_community_hex(community_id)?;
3040 if let Some(n) = name { community.name = n.to_string(); }
3041 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
3042 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
3043 }
3044
3045 pub async fn create_community_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
3051 let v2 = Self::load_v2_if_v2(community_id)?
3052 .ok_or_else(|| VectorError::Other("channel creation is available on v2 communities".into()))?;
3053 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3054 let id = if private {
3055 crate::community::v2::service::create_private_channel(&transport, &v2, name).await
3056 } else {
3057 crate::community::v2::service::create_public_channel(&transport, &v2, name).await
3058 }
3059 .map_err(VectorError::Other)?;
3060 if let Some(client) = state::nostr_client() {
3063 crate::community::v2::realtime::refresh_subscription(&client).await;
3064 }
3065 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
3066 }
3067
3068 pub async fn delete_community_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
3070 let v2 = Self::load_v2_if_v2(community_id)?
3071 .ok_or_else(|| VectorError::Other("channel deletion is available on v2 communities".into()))?;
3072 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3073 let name = v2.channels.iter().find(|c| c.id.0 == ch.0).map(|c| c.name.clone()).unwrap_or_default();
3074 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3075 crate::community::v2::service::delete_channel(&transport, &v2, &ch, &name)
3076 .await
3077 .map_err(VectorError::Other)
3078 }
3079
3080 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
3083 use crate::community::{transport::LiveTransport, CommunityId};
3084 if community_id.len() != 64 {
3085 return Err(VectorError::Other("malformed community id".into()));
3086 }
3087 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3088 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3090 let session = state::SessionGuard::capture();
3091 let channel_ids: Vec<String> =
3092 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
3093 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3094 crate::community::v2::service::leave_community(&transport, &v2)
3095 .await
3096 .map_err(VectorError::Other)?;
3097 if !session.is_valid() {
3098 return Err(VectorError::Other("account changed during leave".into()));
3099 }
3100 let mut st = state::STATE.lock().await;
3101 st.chats.retain(|c| !channel_ids.contains(&c.id));
3102 return Ok(());
3103 }
3104 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
3105 let channel_ids: Vec<String> = community
3106 .as_ref()
3107 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
3108 .unwrap_or_default();
3109 if let Some(ref c) = community {
3111 if let Some(primary) = c.channels.first() {
3112 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3113 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
3114 }
3115 }
3116 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
3118 {
3119 let mut st = state::STATE.lock().await;
3120 st.chats.retain(|c| !channel_ids.contains(&c.id));
3121 }
3122 Ok(())
3123 }
3124
3125 fn resolve_channel(
3127 &self,
3128 channel_id: &str,
3129 ) -> Result<(crate::community::Community, crate::community::Channel)> {
3130 use crate::community::CommunityId;
3131 let community_id = crate::db::community::community_id_for_channel(channel_id)
3132 .map_err(VectorError::Other)?
3133 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
3134 if community_id.len() != 64 {
3135 return Err(VectorError::Other("malformed community id".into()));
3136 }
3137 let community = crate::db::community::load_community(&CommunityId(
3138 crate::simd::hex::hex_to_bytes_32(&community_id),
3139 ))
3140 .map_err(VectorError::Other)?
3141 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
3142 let channel = community
3143 .channels
3144 .iter()
3145 .find(|c| c.id.to_hex() == channel_id)
3146 .cloned()
3147 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
3148 Ok((community, channel))
3149 }
3150
3151
3152 pub async fn sync_dms(
3169 &self,
3170 since_days: Option<u64>,
3171 handler: &dyn InboundEventHandler,
3172 ) -> Result<(u32, u32)> {
3173 use futures_util::StreamExt;
3174 use nostr_sdk::prelude::*;
3175
3176 let client = state::nostr_client()
3177 .ok_or(VectorError::Other("Not connected".into()))?;
3178 let my_pk = state::my_public_key()
3179 .ok_or(VectorError::Other("Not logged in".into()))?;
3180
3181 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
3183
3184 let (items, filter) = if let Some(days) = since_days {
3186 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
3187 let items: Vec<(EventId, Timestamp)> = all_items.iter()
3188 .filter(|(_, ts)| ts.as_secs() >= since_ts)
3189 .cloned()
3190 .collect();
3191 let filter = Filter::new()
3192 .pubkey(my_pk)
3193 .kind(Kind::GiftWrap)
3194 .since(Timestamp::from_secs(since_ts));
3195 (items, filter)
3196 } else {
3197 let filter = Filter::new()
3198 .pubkey(my_pk)
3199 .kind(Kind::GiftWrap);
3200 (all_items, filter)
3201 };
3202
3203 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
3204
3205 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
3207 .direction(nostr_sdk::prelude::SyncDirection::Down)
3208 .initial_timeout(std::time::Duration::from_secs(10))
3209 .dry_run();
3210
3211 let relay_map = client.relays().await;
3215 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
3216 relay_map.iter()
3217 .map(|(url, relay)| (url.clone(), relay.clone()))
3218 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
3219 drop(relay_map);
3220 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
3221 if !skipped_no_neg.is_empty() {
3222 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
3223 }
3224
3225 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
3229 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
3230 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
3231 .min(neg_outer);
3232 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
3233 for (url, relay) in &all_relays {
3234 let url = url.clone();
3235 let relay = relay.clone();
3236 let f = filter.clone();
3237 let i = items.clone();
3238 let o = sync_opts.clone();
3239 relay_futs.push(async move {
3240 if !negentropy::wait_connected(&relay, connect_allowance).await {
3241 return (url, None, false);
3242 }
3243 let result = tokio::time::timeout(
3246 neg_outer,
3247 relay.sync(f).items(i).opts(o),
3248 ).await;
3249 let connected = relay.status() == RelayStatus::Connected;
3250 (url, Some(result), connected)
3251 });
3252 }
3253
3254 let cap_session = state::SessionGuard::capture();
3256 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
3257 while let Some((url, result, connected)) = relay_futs.next().await {
3258 let Some(result) = result else {
3259 log_warn!("[SyncDMs] {} skipped: not connected", url);
3260 continue;
3261 };
3262 match result {
3263 Ok(Ok(recon)) => {
3264 let count = recon.remote.len();
3265 all_missing.extend(recon.remote);
3266 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
3267 if cap_session.is_valid() {
3268 negentropy::record_neg_support(url.as_str(), true);
3269 }
3270 }
3271 Ok(Err(e)) => {
3272 log_warn!("[SyncDMs] {} failed: {}", url, e);
3273 if cap_session.is_valid()
3274 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
3275 {
3276 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
3277 negentropy::record_neg_support(url.as_str(), false);
3278 }
3279 }
3280 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
3281 }
3282 }
3283
3284 let mut total_events = 0u32;
3285 let mut new_messages = 0u32;
3286
3287 if !skipped_no_neg.is_empty() {
3292 let req_filter = filter.clone().limit(500);
3293 match client
3294 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3295 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
3296 ))
3297 .timeout(std::time::Duration::from_secs(20))
3298 .await
3299 {
3300 Ok(stream) => {
3301 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
3302 tokio::pin!(stream);
3303 while let Some((_relay, res)) = stream.next().await {
3304 let Ok(event) = res else { continue };
3305 if !cap_session.is_valid() { break; }
3309 if !seen.insert(event.id.to_bytes()) { continue; }
3310 total_events += 1;
3311 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
3312 if event_handler::commit_prepared_event(prepared, false, handler).await {
3313 new_messages += 1;
3314 }
3315 }
3316 }
3317 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
3318 }
3319 }
3320
3321 if all_missing.is_empty() {
3322 log_info!("[SyncDMs] No missing events");
3323 return Ok((total_events, new_messages));
3324 }
3325
3326 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
3328 let ids: Vec<EventId> = all_missing.into_iter().collect();
3329 let relay_strs: Vec<String> = client.relays().await.keys()
3330 .map(|u| u.to_string()).collect();
3331
3332 const BATCH_SIZE: usize = 500;
3333
3334 for batch in ids.chunks(BATCH_SIZE) {
3335 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
3338 match client
3339 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3340 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
3341 ))
3342 .timeout(std::time::Duration::from_secs(30))
3343 .await
3344 {
3345 Ok(stream) => {
3346 let client_clone = client.clone();
3347 let prepared_stream = stream
3348 .filter_map(|(_relay, res)| async move { res.ok() })
3349 .map(move |event| {
3350 let c = client_clone.clone();
3351 tokio::spawn(async move {
3352 event_handler::prepare_event(event, &c, my_pk).await
3353 })
3354 })
3355 .buffer_unordered(8);
3356 tokio::pin!(prepared_stream);
3357
3358 while let Some(result) = prepared_stream.next().await {
3359 total_events += 1;
3360 if let Ok(prepared) = result {
3361 if event_handler::commit_prepared_event(prepared, false, handler).await {
3362 new_messages += 1;
3363 }
3364 }
3365 }
3366 }
3367 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
3368 }
3369 }
3370
3371 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
3372 Ok((total_events, new_messages))
3373 }
3374
3375 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
3384 use nostr_sdk::prelude::*;
3385 let client = state::nostr_client()
3386 .ok_or(VectorError::Other("Not connected".into()))?;
3387 let my_pk = state::my_public_key()
3388 .ok_or(VectorError::Other("Not logged in".into()))?;
3389
3390 let filter = Filter::new()
3391 .pubkey(my_pk)
3392 .kind(Kind::GiftWrap)
3393 .limit(0);
3394
3395 let output = client.subscribe(filter).await
3396 .map_err(|e| VectorError::Nostr(e.to_string()))?;
3397 Ok(output.value)
3398 }
3399
3400 pub async fn sync_communities(&self) -> Result<()> {
3411 {
3415 use crate::community::{transport::LiveTransport, v2::service as v2};
3416 let bootstrap: Vec<String> = match crate::state::nostr_client() {
3417 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
3418 None => Vec::new(),
3419 };
3420 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3421 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
3422 let joined = outcome.joined;
3425 for c in &joined {
3426 if community::v2::realtime::follow_worker_running() {
3427 community::v2::realtime::enqueue_follow(c.id());
3428 } else {
3429 let _ = Self::v2_inline_follow(c.id()).await;
3430 }
3431 }
3432 if !joined.is_empty() {
3433 if let Some(client) = crate::state::nostr_client() {
3434 community::v2::realtime::refresh_subscription(&client).await;
3435 }
3436 }
3437 }
3438 }
3439
3440 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
3441 for id in ids {
3442 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
3443 if community::v2::realtime::follow_worker_running() {
3446 community::v2::realtime::enqueue_follow(&id);
3447 } else {
3448 let _ = Self::v2_inline_follow(&id).await;
3449 }
3450 continue;
3451 }
3452 if let Ok(Some(community)) = db::community::load_community(&id) {
3453 for ch in &community.channels {
3454 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
3455 }
3456 }
3457 }
3458 Ok(())
3459 }
3460
3461
3462 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
3494 use nostr_sdk::prelude::*;
3495
3496 let client = state::nostr_client()
3497 .ok_or(VectorError::Other("Not connected".into()))?;
3498 let my_pk = state::my_public_key()
3499 .ok_or(VectorError::Other("Not logged in".into()))?;
3500
3501 community::v2::streamauth::ensure_responder(&client);
3508
3509 community::v2::realtime::spawn_follow_worker(handler.clone());
3518 let _ = self.sync_communities().await;
3519 let _ = self.sync_dms(None, &NoOpEventHandler).await;
3520
3521 let dm_sub_id = self.subscribe_dms().await?;
3524 community::realtime::refresh_subscription(&client).await;
3525 community::v2::realtime::refresh_subscription(&client).await;
3526
3527 if let Some(monitor) = client.monitor() {
3534 let mut rx = monitor.subscribe();
3535 let session = state::SessionGuard::capture();
3536 tokio::spawn(async move {
3537 let mut last_resync: Option<std::time::Instant> = None;
3540 while let Ok(notification) = rx.recv().await {
3541 if !session.is_valid() {
3542 return;
3543 }
3544 let MonitorNotification::StatusChanged { status, .. } = notification;
3545 if status == RelayStatus::Connected {
3546 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
3547 continue;
3548 }
3549 let _ = VectorCore.sync_communities().await;
3550 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
3551 if let Some(c) = state::nostr_client() {
3552 community::realtime::refresh_subscription(&c).await;
3553 community::v2::realtime::refresh_subscription(&c).await;
3554 }
3555 last_resync = Some(std::time::Instant::now());
3556 }
3557 }
3558 });
3559 }
3560
3561 {
3565 let client_health = client.clone();
3566 let session = state::SessionGuard::capture();
3567 tokio::spawn(async move {
3568 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
3570 if !session.is_valid() {
3571 return;
3572 }
3573 for (url, relay) in client_health.relays().await {
3574 match relay.status() {
3575 RelayStatus::Connected => {
3576 let probe = tokio::time::timeout(
3577 std::time::Duration::from_secs(10),
3578 client_health
3579 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
3580 url.to_string(),
3581 [Filter::new().kind(Kind::Metadata).limit(1)],
3582 ))
3583 .timeout(std::time::Duration::from_secs(8)),
3584 )
3585 .await;
3586 if !matches!(probe, Ok(Ok(_))) {
3587 let _ = relay.disconnect();
3588 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
3589 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3590 }
3591 }
3592 RelayStatus::Terminated | RelayStatus::Disconnected => {
3593 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3594 }
3595 _ => {}
3596 }
3597 }
3598 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
3599 }
3600 });
3601 }
3602
3603 let client_for_closure = client.clone();
3604
3605 let mut notifications = client.notifications();
3608 while let Some(notification) = notifications.next().await {
3609 let handler = handler.clone();
3610 let c = client_for_closure.clone();
3611 let dm_sid = dm_sub_id.clone();
3612 {
3613 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
3617 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
3618 sending::note_relay_ok(event_id, *status);
3619 }
3620 }
3621 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
3622 if subscription_id == dm_sid {
3623 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
3625 event_handler::commit_prepared_event(prepared, true, &*handler).await;
3626 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3627 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3628 {
3629 let session = state::SessionGuard::capture();
3633 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
3634 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3635 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3636 {
3637 let session = state::SessionGuard::capture();
3639 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
3640 }
3641 }
3642 }
3643 }
3644
3645 Ok(())
3646 }
3647
3648 pub async fn logout(&self) {
3650 if let Some(client) = state::nostr_client() {
3651 let _ = client.disconnect().await;
3652 }
3653 db::close_database();
3654 }
3655
3656 pub async fn swap_session(&self) {
3664 state::bump_session_generation();
3666
3667 if let Some(client) = state::take_nostr_client() {
3670 let _ = client.shutdown().await;
3671 }
3672 db::close_database();
3673
3674 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
3676 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
3677 {
3678 use zeroize::Zeroize;
3679 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
3680 if let Some(s) = g.as_mut() { s.zeroize(); }
3681 *g = None;
3682 }
3683 if let Ok(mut g) = state::PENDING_NSEC.lock() {
3684 if let Some(s) = g.as_mut() { s.zeroize(); }
3685 *g = None;
3686 }
3687 }
3688
3689 {
3691 let mut st = state::STATE.lock().await;
3692 st.profiles.clear();
3693 st.chats.clear();
3694 st.db_loaded = false;
3695 st.is_syncing = false;
3696 }
3697 state::WRAPPER_ID_CACHE.lock().await.clear();
3698 state::PENDING_EVENTS.lock().await.clear();
3699 state::set_active_chat(None);
3700 crate::profile::sync::clear_profile_sync_queue();
3701 crate::inbox_relays::clear_inbox_relay_cache();
3702 crate::sending::clear_wrap_confirms();
3705 crate::emoji_packs::clear_nip65_cache();
3706 crate::db::clear_id_caches();
3710 crate::community::cache::clear();
3714 crate::community::realtime::clear().await;
3717 crate::community::v2::realtime::clear().await;
3718 crate::community::transport::clear_plane_pool();
3720 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
3724 }
3725}
3726
3727#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
3728mod transport_policy_tests {
3729 use std::time::Duration;
3730
3731 #[test]
3734 fn tor_transport_policy() {
3735 let short = Duration::from_secs(5);
3736 let long = Duration::from_secs(300);
3737
3738 crate::tor::set_tor_enabled_pref(false);
3741 assert_eq!(super::tor_proxy_target(), None);
3742 assert_eq!(super::relay_connect_timeout(short), short);
3743 assert_eq!(super::relay_request_timeout(short), short);
3744
3745 crate::tor::set_tor_enabled_pref(true);
3749 assert!(matches!(
3750 crate::tor::transport_state(),
3751 crate::tor::TorTransportState::RequiredButInactive
3752 ));
3753 assert_eq!(
3759 super::tor_proxy_target(),
3760 Some(crate::tor::blackhole_proxy_addr()),
3761 "Tor enabled but inactive must blackhole, never connect direct"
3762 );
3763 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
3764 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
3765
3766 for tor in [true, false] {
3769 crate::tor::set_tor_enabled_pref(tor);
3770 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
3771 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
3772 }
3773 }
3774}
3775
3776#[cfg(test)]
3777mod facade_tests {
3778 use super::*;
3779
3780 #[tokio::test]
3783 async fn download_attachment_rejects_private_url() {
3784 let att = crate::types::Attachment {
3785 url: "http://169.254.169.254/latest/meta-data/".to_string(),
3786 ..Default::default()
3787 };
3788 match VectorCore.download_attachment(&att).await {
3789 Err(VectorError::Other(msg)) => {
3790 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
3791 }
3792 other => panic!("expected SSRF rejection, got {other:?}"),
3793 }
3794 }
3795
3796 #[tokio::test]
3797 async fn download_attachment_rejects_empty_url() {
3798 let att = crate::types::Attachment::default();
3799 assert!(VectorCore.download_attachment(&att).await.is_err());
3800 }
3801
3802 #[tokio::test]
3806 async fn list_communities_and_channel_routing_are_protocol_aware() {
3807 use crate::community::transport::memory::MemoryRelay;
3808 use nostr_sdk::prelude::Keys;
3809
3810 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
3811 crate::db::close_database();
3812 crate::db::clear_id_caches();
3813 let tmp = tempfile::tempdir().unwrap();
3814 let acct = {
3816 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3817 let mut s = String::from("npub1");
3818 for i in 0..58 {
3819 s.push(B[(i * 7 + 3) % 32] as char);
3820 }
3821 s
3822 };
3823 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
3824 crate::db::set_app_data_dir(tmp.path().to_path_buf());
3825 crate::db::set_current_account(acct.clone()).unwrap();
3826 crate::db::init_database(&acct).unwrap();
3827 let _ = crate::state::take_nostr_client();
3828 let me = Keys::generate();
3829 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
3830 crate::state::set_my_public_key(me.public_key());
3831
3832 let relay = MemoryRelay::new();
3834 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
3835 .await
3836 .unwrap();
3837 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
3838
3839 let listed = VectorCore.list_communities().await;
3841 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
3842 assert_eq!(v2["name"], "V2 Guild");
3843 assert_eq!(v2["is_owner"], true);
3844 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
3845
3846 assert_eq!(
3848 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
3849 Some(community.identity.community_id),
3850 "a v2 channel is routed to v2"
3851 );
3852 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
3854 }
3855
3856 #[test]
3861 fn v2_invite_url_base_derivation_round_trips() {
3862 use crate::community::v2::derive::TOKEN_LEN;
3863 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
3864 use nostr_sdk::prelude::Keys;
3865 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
3866 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
3867 let signer = Keys::generate();
3868 let token = [0x07u8; TOKEN_LEN];
3869 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
3870 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
3871 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
3872 let parsed = parse_invite_link(&url).unwrap();
3873 assert_eq!(parsed.link_signer, signer.public_key());
3874 assert_eq!(parsed.token, token);
3875 }
3876}