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 ) -> signer::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(
107 nostr_sdk::prelude::ClientAuthentication::new(challenge, relay_url.clone())
108 .finalize_async(&signer)
109 .await?,
110 )
111 })
112 }
113}
114
115pub fn nostr_client_builder() -> nostr_sdk::prelude::ClientBuilder {
126 apply_tor_proxy(
127 nostr_sdk::prelude::ClientBuilder::new()
128 .authenticator(VectorAuthenticator)
129 .connect_timeout(relay_connect_timeout(std::time::Duration::from_secs(15))),
132 )
133}
134
135pub trait ClientRelayExt {
147 fn add_managed_relay<'client, 'url, U>(
149 &'client self,
150 url: U,
151 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
152 where
153 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>;
154}
155
156impl ClientRelayExt for nostr_sdk::prelude::Client {
157 fn add_managed_relay<'client, 'url, U>(
158 &'client self,
159 url: U,
160 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
161 where
162 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>,
163 {
164 self.add_relay(url).reconnect(false)
165 }
166}
167
168pub async fn resubscribe_relay_after_reconnect(
183 client: &nostr_sdk::prelude::Client,
184 relay: &nostr_sdk::prelude::RelayUrl,
185) {
186 for (id, per_relay) in client.subscriptions().await {
187 let Some(filters) = per_relay.get(relay) else { continue };
188 if filters.is_empty() {
189 continue;
190 }
191 let _ = client
192 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), filters.clone()))
193 .with_id(id)
194 .await;
195 }
196}
197
198#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
203const TOR_RELAY_CONNECT_FLOOR: std::time::Duration = std::time::Duration::from_secs(60);
204
205pub fn relay_connect_timeout(clearnet: std::time::Duration) -> std::time::Duration {
214 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
215 {
216 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
217 return clearnet.max(TOR_RELAY_CONNECT_FLOOR);
218 }
219 }
220 clearnet
221}
222
223#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
225const TOR_RELAY_REQUEST_FLOOR: std::time::Duration = std::time::Duration::from_secs(30);
226
227pub fn relay_request_timeout(clearnet: std::time::Duration) -> std::time::Duration {
233 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
234 {
235 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
236 return clearnet.max(TOR_RELAY_REQUEST_FLOOR);
237 }
238 }
239 clearnet
240}
241
242pub fn apply_tor_proxy(
249 builder: nostr_sdk::prelude::ClientBuilder,
250) -> nostr_sdk::prelude::ClientBuilder {
251 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
252 let builder = builder.proxy(nostr_sdk::prelude::Proxy::custom(|_url| tor_proxy_target()));
253 builder
254}
255
256#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
262fn tor_proxy_target() -> Option<std::net::SocketAddr> {
263 match tor::transport_state() {
264 tor::TorTransportState::Active(addr) => Some(addr),
265 tor::TorTransportState::RequiredButInactive => Some(tor::blackhole_proxy_addr()),
268 tor::TorTransportState::Disabled => None,
269 }
270}
271
272pub async fn sign_builder(
277 builder: nostr_sdk::prelude::EventBuilder,
278) -> std::result::Result<nostr_sdk::prelude::Event, String> {
279 let signer = signer::active_signer()?;
280 builder
281 .finalize_async(&signer)
282 .await
283 .map_err(|e| e.to_string())
284}
285
286pub async fn sign_and_send(
290 client: &nostr_sdk::prelude::Client,
291 builder: nostr_sdk::prelude::EventBuilder,
292) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String> {
293 let event = sign_builder(builder).await?;
294 client
295 .send_event(&event)
296 .await
297 .map_err(|e| e.to_string())
298}
299
300pub async fn send_gift_wrap<'u, I, U, T>(
306 client: &nostr_sdk::prelude::Client,
307 relays: I,
308 receiver: &nostr_sdk::prelude::PublicKey,
309 rumor: nostr_sdk::prelude::UnsignedEvent,
310 extra_tags: T,
311) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String>
312where
313 I: IntoIterator<Item = U>,
314 U: Into<nostr_sdk::prelude::RelayUrlArg<'u>>,
315 T: IntoIterator<Item = nostr_sdk::prelude::Tag>,
316{
317 let signer = signer::active_signer()?;
318 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(*receiver, rumor)
319 .extra_tags(extra_tags)
320 .finalize_async(&signer)
321 .await
322 .map_err(|e| e.to_string())?;
323 let targets: Vec<nostr_sdk::prelude::RelayUrlArg<'u>> =
324 relays.into_iter().map(Into::into).collect();
325 if targets.is_empty() {
326 client.send_event(&wrap).await.map_err(|e| e.to_string())
327 } else {
328 client
329 .send_event(&wrap)
330 .to(targets)
331 .await
332 .map_err(|e| e.to_string())
333 }
334}
335
336pub fn community_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
349 nostr_sdk::prelude::RelayCapabilities::GOSSIP
350}
351
352pub fn discovery_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
358 community_relay_capabilities()
359}
360
361pub mod stored_event;
363
364pub mod rumor;
366
367pub mod sending;
369
370pub mod wallpaper;
372
373pub mod deletion;
375pub mod self_destruct;
376
377pub mod simd;
379
380pub mod community;
382
383pub mod event_handler;
385
386pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
388pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
389pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
390pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
391pub use state::{
392 ChatState, NOSTR_CLIENT, MY_SECRET_KEY, MY_PUBLIC_KEY, STATE, ENCRYPTION_KEY,
393 nostr_client, my_public_key, has_active_session,
394 set_nostr_client, set_my_public_key,
395 take_nostr_client, clear_my_public_key,
396 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
397 set_pending_nip55_setup, pending_nip55_setup, clear_pending_nip55_setup,
398};
399pub use crypto::{GuardedKey, GuardedSigner};
400pub use signer::{
401 SignerKind, signer_kind, set_signer_kind, is_bunker, is_keyless,
402 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
403 build_bunker_signer, prewarm_bunker, drain_bunker_state,
404 parse_bunker_remote_pubkey, parse_bunker_relays,
405 BunkerConnectionState, bunker_state, set_bunker_state,
406 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
407 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
408 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
409};
410pub use nip55::{
411 Nip55Backend, Nip55Error, Nip55ResolverOutcome, Nip55Signer, Nip55State,
412 set_nip55_backend, nip55_backend, nip55_state, set_nip55_state, drain_nip55_state,
413 nip55_is_installed, nip55_pair, nip55_perms_json,
414 VECTOR_NIP55_SIGN_KINDS, VECTOR_NIP55_ENCRYPT_TYPES,
415};
416pub use error::{VectorError, Result};
417pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
418pub use db::{set_app_data_dir, get_app_data_dir};
419pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
420pub use deletion::{delete_own_dm, DeleteOutcome};
421pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
422pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
423pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
424pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
425
426use std::path::PathBuf;
427use std::sync::Arc;
428
429pub struct CoreConfig {
435 pub data_dir: PathBuf,
437 pub event_emitter: Option<Box<dyn EventEmitter>>,
439}
440
441#[derive(Clone, Copy)]
463pub struct VectorCore;
464
465impl VectorCore {
466 pub fn init(config: CoreConfig) -> Result<Self> {
468 db::set_app_data_dir(config.data_dir);
470
471 if let Some(emitter) = config.event_emitter {
473 traits::set_event_emitter(emitter);
474 }
475
476 let _ = rustls::crypto::ring::default_provider().install_default();
478
479 Ok(VectorCore)
480 }
481
482 pub fn accounts(&self) -> Result<Vec<String>> {
484 db::get_accounts().map_err(VectorError::from)
485 }
486
487 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
489 use nostr_sdk::prelude::*;
490
491 let keys = if key.starts_with("nsec1") {
493 let secret = SecretKey::from_bech32(key)
494 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
495 Keys::new(secret)
496 } else {
497 Keys::from_mnemonic(key, None)
499 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
500 };
501
502 let public_key = keys.public_key();
503 let npub = public_key.to_bech32()
504 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
505
506 let secret_bytes = keys.secret_key().to_secret_bytes();
508 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
509 state::set_my_public_key(public_key);
510
511 db::set_current_account(npub.clone())?;
513 db::init_database(&npub)?;
514
515 {
517 let nsec = keys.secret_key().to_bech32()
518 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
519 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
520
521 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
528 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
529 db::set_pkey(&nsec)?;
530 }
531 }
532
533 let has_encryption = state::resolve_encryption_enabled_from_db();
536
537 if has_encryption {
538 if let Some(pwd) = password {
539 let key = crate::crypto::hash_pass(pwd).await;
540 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
541 }
542 }
543 state::init_encryption_enabled();
546
547 let client = crate::nostr_client_builder()
550 .monitor(Monitor::new(1024))
552 .build();
553
554 for relay in state::TRUSTED_RELAYS {
556 client.add_managed_relay(*relay).await.ok();
557 }
558
559 client.connect().await;
561
562 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
563
564 Ok(LoginResult { npub, has_encryption })
565 }
566
567 pub fn generate_nsec(&self) -> Result<String> {
570 use nostr_sdk::prelude::*;
571 Keys::generate().secret_key().to_bech32()
572 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
573 }
574
575 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
580 let config = SendConfig { self_send: false, ..SendConfig::headless() };
581 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
582 .map_err(|e| VectorError::Other(e))
583 }
584
585 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
587 let config = SendConfig { self_send: false, ..SendConfig::headless() };
588 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
589 .map_err(|e| VectorError::Other(e))
590 }
591
592 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
599 self.download_attachment_from(attachment, None).await
600 }
601
602 pub async fn download_attachment_from(
607 &self,
608 attachment: &Attachment,
609 author_npub: Option<&str>,
610 ) -> Result<Vec<u8>> {
611 use futures_util::StreamExt;
612 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
613 if attachment.url.is_empty() {
614 return Err(VectorError::Other("attachment has no URL".into()));
615 }
616 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
617 let mut last_err = String::from("download failed");
618 let mut candidates: Vec<String> = vec![attachment.url.clone()];
619 candidates.extend(attachment.fallback_urls.iter().cloned());
620 let mut hash_swap_tried = false;
621 let mut i = 0;
622 'sources: while i < candidates.len() {
623 let url = candidates[i].clone();
624 i += 1;
625 let extend_with_swap = |candidates: &mut Vec<String>, servers: &[String]| {
628 let extra = crate::blossom::hash_swap_candidates(&attachment.url, servers);
629 for c in extra {
630 if !candidates.contains(&c) {
631 candidates.push(c);
632 }
633 }
634 };
635 macro_rules! next_source {
636 () => {{
637 log_net_fail!("[Download] source failed ({}): {}", url, last_err);
638 if i == candidates.len() && !hash_swap_tried {
639 hash_swap_tried = true;
640 let servers = crate::blossom_servers::author_swap_servers(author_npub, false).await;
641 extend_with_swap(&mut candidates, &servers);
642 }
643 continue 'sources;
644 }};
645 }
646 if let Err(e) = crate::net::validate_url_not_private(&url) {
650 last_err = e.to_string();
651 next_source!();
652 }
653 let resp = match client.get(&url).send().await {
654 Ok(r) => r,
655 Err(e) => {
656 last_err = format!("download: {e}");
657 next_source!();
658 }
659 };
660 if !resp.status().is_success() {
661 last_err = format!("download failed: HTTP {}", resp.status());
662 next_source!();
663 }
664 let mut encrypted: Vec<u8> = Vec::with_capacity(
667 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
668 );
669 let mut stream = resp.bytes_stream();
670 while let Some(chunk) = stream.next().await {
671 let chunk = match chunk {
672 Ok(c) => c,
673 Err(e) => {
674 last_err = format!("read body: {e}");
675 next_source!();
676 }
677 };
678 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
679 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
680 }
681 encrypted.extend_from_slice(&chunk);
682 }
683 match crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce) {
684 Ok(plain) => {
685 if i > 1 {
686 log_net_info!("[Download] fallback source {}/{} served {}", i, candidates.len(), url);
687 }
688 return Ok(plain);
689 }
690 Err(e) => {
691 last_err = format!("decrypt: {e}");
694 next_source!();
695 }
696 }
697 }
698 log_net_fail!("[Download] all {} source(s) failed for {}: {}", candidates.len(), attachment.url, last_err);
699 Err(VectorError::Other(last_err))
700 }
701
702 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
704 let path = std::path::Path::new(file_path);
705 let bytes = std::fs::read(path)
706 .map_err(|e| VectorError::Io(e))?;
707 let filename = path.file_name()
708 .and_then(|n| n.to_str())
709 .unwrap_or("file");
710 let extension = path.extension()
711 .and_then(|e| e.to_str())
712 .unwrap_or("bin");
713
714 sending::send_file_dm(
715 to_npub,
716 std::sync::Arc::new(bytes),
717 filename,
718 extension,
719 None,
720 &SendConfig::default(),
721 Arc::new(NoOpSendCallback),
722 ).await.map_err(|e| VectorError::Other(e))
723 }
724
725 pub async fn send_reaction(
730 &self,
731 to_npub: &str,
732 reference_id: &str,
733 emoji: &str,
734 emoji_url: Option<&str>,
735 ) -> Result<String> {
736 use nostr_sdk::prelude::*;
737
738 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
739 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
740
741 let reference_event = EventId::from_hex(reference_id)
742 .map_err(|e| VectorError::Nostr(e.to_string()))?;
743 let receiver_pubkey = PublicKey::from_bech32(to_npub)
744 .map_err(|e| VectorError::Nostr(e.to_string()))?;
745
746 let custom_emoji_tag = emoji_url.and_then(|url| {
748 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
749 return None;
750 }
751 let shortcode = &emoji[1..emoji.len() - 1];
752 if shortcode.is_empty() { return None; }
753 Some(Tag::custom("emoji", [shortcode.to_string(), url.to_string()]))
754 });
755
756 let reaction_target = nostr_sdk::prelude::nip25::ReactionTarget {
757 event_id: reference_event,
758 public_key: receiver_pubkey,
759 coordinate: None,
760 kind: Some(Kind::PrivateDirectMessage),
761 relay_hint: None,
762 };
763 let mut builder =
764 nostr_sdk::prelude::nip25::ReactionBuilder::new(reaction_target, emoji)
765 .into_event_builder();
766 if let Some(tag) = custom_emoji_tag {
767 builder = builder.tag(tag);
768 }
769 let rumor = builder.finalize_unsigned_with_id(my_public_key);
770 let inner_rumor_id = rumor.id;
771 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
772
773 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
777 .await.map_err(VectorError::Other)?;
778 if !outcome.output.success.is_empty() {
779 if let Some(rid) = inner_rumor_id {
780 if let Err(e) = db::nip17_keys::store_wrap_key(
781 &outcome.wrap_event_id, &rid, &receiver_pubkey,
782 db::nip17_keys::WrapRole::Recipient,
783 &outcome.wrap_secret, &outcome.targeted_relays,
784 ) {
785 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
786 }
787 }
788 }
789
790 let self_wrap_client = client.clone();
793 let self_wrap_session = state::SessionGuard::capture();
794 tokio::spawn(async move {
795 if !self_wrap_session.is_valid() { return; }
796 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
797 &self_wrap_client, &my_public_key, rumor, [],
798 ).await {
799 if !self_wrap_session.is_valid() { return; }
800 if !self_outcome.output.success.is_empty() {
801 if let Some(rid) = inner_rumor_id {
802 let _ = db::nip17_keys::store_wrap_key(
803 &self_outcome.wrap_event_id, &rid, &my_public_key,
804 db::nip17_keys::WrapRole::SelfSend,
805 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
806 );
807 }
808 }
809 }
810 });
811
812 let reaction = Reaction {
814 id: rumor_id.clone(),
815 reference_id: reference_id.to_string(),
816 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
817 emoji: emoji.to_string(),
818 emoji_url: emoji_url.map(|s| s.to_string()),
819 };
820 let msg_for_save = {
821 let mut st = state::STATE.lock().await;
822 match st.add_reaction_to_message(reference_id, reaction) {
823 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
824 _ => None,
825 }
826 };
827 if let Some((cid, mut msg)) = msg_for_save {
828 let _ = db::events::save_message(&cid, &msg).await;
829 traits::emit_message_update(&cid, reference_id, &mut msg).await;
830 }
831
832 Ok(rumor_id)
833 }
834
835 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
838 use nostr_sdk::prelude::*;
839
840 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
841 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
842 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
843
844 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
845 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
846 .tag(Tag::public_key(pubkey))
847 .tag(Tag::custom("d", vec!["vector"]))
848 .tag(Tag::expiration(expiry))
849 .finalize_unsigned_with_id(my_public_key);
850
851 let signer = signer::active_signer().map_err(VectorError::Other)?;
853 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(pubkey, rumor.clone())
854 .extra_tags([Tag::expiration(expiry)])
855 .finalize_async(&signer)
856 .await
857 .map_err(|e| VectorError::Nostr(e.to_string()))?;
858 client
859 .send_event(&wrap)
860 .to(state::active_trusted_relays().await)
861 .await
862 .map_err(|e| VectorError::Nostr(e.to_string()))?;
863 Ok(())
864 }
865
866 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
870 use nostr_sdk::prelude::*;
871
872 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
873 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
874 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
875 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
876 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
877
878 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
880
881 let mut builder = EventBuilder::new(
882 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
883 new_content,
884 ).tag(Tag::event(reference_event));
885 for et in &emoji_tags {
886 builder = builder.tag(Tag::custom(
887 "emoji",
888 [et.shortcode.clone(), et.url.clone()],
889 ));
890 }
891 let rumor = builder.finalize_unsigned_with_id(my_public_key);
892 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
893 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
894
895 let msg_for_emit = {
897 let mut st = state::STATE.lock().await;
898 st.update_message_in_chat(to_npub, message_id, |msg| {
899 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
900 msg.preview_metadata = None;
901 })
902 };
903 if let Some(mut msg) = msg_for_emit {
904 traits::emit_message_update(to_npub, message_id, &mut msg).await;
905 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
906 let _ = db::events::save_edit_event(
907 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
908 ).await;
909 }
910 }
911
912 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
913 .await.map_err(VectorError::Other)?;
914
915 let self_wrap_client = client.clone();
916 let self_wrap_session = state::SessionGuard::capture();
917 tokio::spawn(async move {
918 if !self_wrap_session.is_valid() { return; }
919 let Ok(signer) = signer::active_signer() else { return };
920 if let Ok(wrap) = nostr_sdk::prelude::GiftWrapBuilder::new(my_public_key, rumor)
921 .finalize_async(&signer)
922 .await
923 {
924 let _ = self_wrap_client.send_event(&wrap).await;
925 }
926 });
927
928 Ok(edit_id)
929 }
930
931 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
933 use nostr_sdk::prelude::*;
934 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
935 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
936 }
937
938 pub async fn get_chats(&self) -> Vec<SerializableChat> {
940 let state = state::STATE.lock().await;
941 state.chats.iter()
942 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
943 .collect()
944 }
945
946 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
948 let state = state::STATE.lock().await;
949 if let Some(chat) = state.get_chat(chat_id) {
950 let msgs = chat.get_all_messages(&state.interner);
951 let start = offset.min(msgs.len());
952 let end = (offset + limit).min(msgs.len());
953 msgs[start..end].to_vec()
954 } else {
955 Vec::new()
956 }
957 }
958
959 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
961 let state = state::STATE.lock().await;
962 state.get_profile(npub)
963 .map(|p| SlimProfile::from_profile(p, &state.interner))
964 }
965
966 pub async fn load_profile(&self, npub: &str) -> bool {
968 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
969 }
970
971 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
973 profile::sync::update_profile(
974 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
975 &NoOpProfileSyncHandler,
976 ).await
977 }
978
979 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
982 profile::sync::update_bot_profile(
983 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
984 &NoOpProfileSyncHandler,
985 ).await
986 }
987
988 pub async fn update_status(&self, status: &str) -> bool {
990 profile::sync::update_status(status.to_string()).await
991 }
992
993 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
999 let path = std::path::Path::new(file_path);
1000 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1001 if bytes.is_empty() {
1002 return Err(VectorError::Other("Empty image file".into()));
1003 }
1004 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1005 let mime = crate::crypto::mime_from_extension(&extension);
1006 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1007 let signer = crate::signer::active_signer()
1008 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1009 let servers = crate::blossom_servers::compute_enabled_servers();
1010 if servers.is_empty() {
1011 return Err(VectorError::Other("No Blossom servers configured".into()));
1012 }
1013 crate::blossom::upload_blob_with_failover(
1016 signer,
1017 servers,
1018 std::sync::Arc::new(bytes),
1019 Some(mime),
1020 Some(std::time::Duration::from_secs(20)),
1021 )
1022 .await
1023 .map_err(VectorError::Other)
1024 }
1025
1026 pub async fn block_user(&self, npub: &str) -> bool {
1028 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1029 }
1030
1031 pub async fn unblock_user(&self, npub: &str) -> bool {
1033 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1034 }
1035
1036 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1038 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1039 }
1040
1041 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1043 profile::sync::get_blocked_users().await
1044 }
1045
1046 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1048 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1049 }
1050
1051 pub fn my_npub(&self) -> Option<String> {
1053 state::my_public_key()
1054 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1055 }
1056
1057 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1064 use crate::community::ConcordProtocol;
1065 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1066 let mut out = Vec::new();
1067 for id in ids {
1068 match crate::db::community::community_protocol(&id).ok().flatten() {
1070 Some(ConcordProtocol::V2) => {
1071 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1072 let me = state::my_public_key();
1073 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1074 out.push(serde_json::json!({
1075 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1076 "version": 2,
1077 "name": c.name,
1078 "description": c.description,
1079 "is_owner": is_owner,
1080 "channels": c.channels.iter()
1086 .map(|ch| serde_json::json!({
1087 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1088 "name": ch.name,
1089 "private": ch.private,
1090 "readable": !(ch.private && ch.key.is_none()),
1091 "epoch": ch.epoch.0,
1092 }))
1093 .collect::<Vec<_>>(),
1094 }));
1095 }
1096 }
1097 _ => {
1098 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1099 out.push(serde_json::json!({
1100 "community_id": c.id.to_hex(),
1101 "version": 1,
1102 "name": c.name,
1103 "description": c.description,
1104 "is_owner": crate::community::service::is_proven_owner(&c),
1105 "channels": c.channels.iter()
1106 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1107 .collect::<Vec<_>>(),
1108 }));
1109 }
1110 }
1111 }
1112 }
1113 out
1114 }
1115
1116 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1121 use crate::community::{v2::service as v2, transport::LiveTransport};
1122 let relays: Vec<String> = crate::state::active_trusted_relays()
1123 .await
1124 .iter()
1125 .map(|s| s.to_string())
1126 .collect();
1127 if relays.is_empty() {
1128 return Err(VectorError::Other("no relays available to host the Community".into()));
1129 }
1130 let session = state::SessionGuard::capture();
1131 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1132 let community = v2::create_community(&transport, name, relays, None)
1133 .await
1134 .map_err(VectorError::Other)?;
1135 self.register_v2_chats(&community, &session).await;
1136 if let Some(client) = state::nostr_client() {
1138 crate::community::v2::realtime::refresh_subscription(&client).await;
1139 }
1140 Ok(Self::v2_summary(&community))
1141 }
1142
1143 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1149 use crate::community::ConcordProtocol;
1150 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1151 return Ok(None);
1152 };
1153 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1154 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1155 Some(ConcordProtocol::V2) => Some(cid),
1156 _ => None,
1157 })
1158 }
1159
1160 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1162 let me = state::my_public_key();
1163 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1164 serde_json::json!({
1165 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1166 "version": 2,
1167 "name": community.name,
1168 "description": community.description,
1169 "is_owner": is_owner,
1170 "channels": community.channels.iter()
1171 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1172 .collect::<Vec<_>>(),
1173 })
1174 }
1175
1176 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1182 register_v2_chats_inner(community, session).await
1183 }
1184}
1185
1186pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1189 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1190 let me = state::my_public_key();
1191 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1192 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1193 let Some(primary) = community.primary_channel() else { return };
1196 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1197 let slims = {
1202 let mut st = state::STATE.lock().await;
1203 if !session.is_valid() {
1204 return; }
1206 let mut slims = Vec::new();
1207 for ch in &community.channels {
1208 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1209 st.upsert_community_chat(
1210 &ch_hex,
1211 &community.name,
1212 community.description.as_deref().unwrap_or(""),
1213 &id_hex,
1214 is_owner,
1215 community.icon.is_some(),
1216 owner_npub.as_deref(),
1217 Some(community.created_at_ms),
1218 community.dissolved,
1219 crate::community::ConcordProtocol::V2,
1220 &ch.name,
1221 &primary_hex,
1222 );
1223 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1224 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1225 }
1226 }
1227 slims
1228 };
1229 if !session.is_valid() {
1233 return;
1234 }
1235 for slim in &slims {
1236 let _ = crate::db::chats::save_slim_chat(slim);
1237 }
1238}
1239
1240impl VectorCore {
1241 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1245 use crate::community::{public_invite, service, transport::LiveTransport};
1246 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1250 let session = state::SessionGuard::capture();
1251 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1252 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1253 .await
1254 .map_err(VectorError::Other)?;
1255 self.register_v2_chats(&community, &session).await;
1256 if let Some(client) = state::nostr_client() {
1257 crate::community::v2::realtime::refresh_subscription(&client).await;
1258 }
1259 if crate::community::v2::realtime::follow_worker_running() {
1264 crate::community::v2::realtime::enqueue_follow(community.id());
1265 } else {
1266 let seed_session = state::SessionGuard::capture();
1267 let seed_community = community.clone();
1268 tokio::spawn(async move {
1269 if !seed_session.is_valid() {
1270 return;
1271 }
1272 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1273 if matches!(
1274 crate::community::v2::service::sync_guestbook(&transport, &seed_community, &seed_session).await,
1275 Ok(fresh) if !fresh.is_empty()
1276 ) {
1277 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1278 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1279 }
1280 });
1281 }
1282 return Ok(Self::v2_summary(&community));
1283 }
1284 let (relays, token) = public_invite::parse_invite_url(invite_url)
1285 .map_err(|e| VectorError::Other(e.to_string()))?;
1286 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1287 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1288 .await
1289 .map_err(VectorError::Other)?;
1290 let now = std::time::SystemTime::now()
1291 .duration_since(std::time::UNIX_EPOCH)
1292 .map(|d| d.as_secs())
1293 .unwrap_or(0);
1294 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1297 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1298 .await
1299 .map_err(VectorError::Other)?;
1300 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1301 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1304 self.finalize_member_join(community, &transport, attribution).await
1305 }
1306
1307 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1310 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1311 Ok(rows.iter().map(|p| {
1312 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1315 serde_json::json!({
1316 "community_id": p.community_id,
1317 "name": v2.name,
1318 "inviter_npub": p.inviter_npub,
1319 "version": 2,
1320 })
1321 } else {
1322 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1323 .ok().map(|i| i.name).unwrap_or_default();
1324 serde_json::json!({
1325 "community_id": p.community_id,
1326 "name": name,
1327 "inviter_npub": p.inviter_npub,
1328 "version": 1,
1329 })
1330 }
1331 }).collect())
1332 }
1333
1334 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1338 use crate::community::transport::LiveTransport;
1339 let bundle_json = crate::db::community::get_pending_invite(community_id)
1340 .map_err(VectorError::Other)?
1341 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1342 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1343
1344 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1346 let session = state::SessionGuard::capture();
1347 let inviter = crate::db::community::list_pending_invites()
1349 .ok()
1350 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1351 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
1359 .await
1360 .map_err(VectorError::Other)?;
1361 if !session.is_valid() {
1362 return Err(VectorError::Other("account changed during join".into()));
1363 }
1364 self.register_v2_chats(&community, &session).await;
1365 if let Some(client) = state::nostr_client() {
1366 crate::community::v2::realtime::refresh_subscription(&client).await;
1367 }
1368 crate::community::v2::realtime::enqueue_follow(community.id());
1369 let _ = crate::db::community::delete_pending_invite(community_id);
1370 return Ok(Self::v2_summary(&community));
1371 }
1372
1373 use crate::community::invite::{accept_invite, CommunityInvite};
1375 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1376 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1377 let now = std::time::SystemTime::now()
1381 .duration_since(std::time::UNIX_EPOCH)
1382 .map(|d| d.as_secs())
1383 .unwrap_or(0);
1384 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1385 .await
1386 .map_err(VectorError::Other)?;
1387 let summary = self.finalize_member_join(community, &transport, None).await?;
1389 let _ = crate::db::community::delete_pending_invite(community_id);
1390 Ok(summary)
1391 }
1392
1393 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1398 &self,
1399 community: crate::community::Community,
1400 transport: &T,
1401 attribution: Option<(String, Option<String>)>,
1402 ) -> Result<serde_json::Value> {
1403 use crate::community::service;
1404 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1411 return Ok(serde_json::json!({
1412 "community_id": v2,
1413 "version": 2,
1414 "migrated": true,
1415 }));
1416 }
1417 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1421 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1424 if c.removed {
1425 let _ = crate::db::community::delete_community(&community.id.to_hex());
1426 return Err(VectorError::Other("you have been removed from this community".into()));
1427 }
1428 }
1429 let community = crate::db::community::load_community(&community.id)
1430 .map_err(VectorError::Other)?
1431 .unwrap_or(community);
1432 let _ = service::fetch_and_apply_control(transport, &community).await;
1436 if service::am_i_banned(&community) {
1437 let _ = crate::db::community::delete_community(&community.id.to_hex());
1438 return Err(VectorError::Other("you are banned from this community".into()));
1439 }
1440 let community = crate::db::community::load_community(&community.id)
1442 .map_err(VectorError::Other)?
1443 .unwrap_or(community);
1444 let owner_npub = community
1445 .owner_attestation
1446 .as_ref()
1447 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1448 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1449 {
1450 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1451 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1452 let mut st = state::STATE.lock().await;
1453 for ch in &community.channels {
1454 st.upsert_community_chat(
1455 &ch.id.to_hex(),
1456 &community.name,
1457 community.description.as_deref().unwrap_or(""),
1458 &community.id.to_hex(),
1459 crate::community::service::is_proven_owner(&community),
1460 community.icon.is_some(),
1461 owner_npub.as_deref(),
1462 created_at_ms,
1463 community.dissolved,
1464 crate::community::ConcordProtocol::V1,
1465 &ch.name,
1466 &primary_hex,
1467 );
1468 }
1469 }
1470 if let Some(primary) = community.channels.first() {
1473 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1474 }
1475 Ok(serde_json::json!({
1476 "community_id": community.id.to_hex(),
1477 "version": 1,
1478 "name": community.name,
1479 "channels": community.channels.iter()
1480 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1481 .collect::<Vec<_>>(),
1482 }))
1483 }
1484
1485
1486 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1490 use crate::community::CommunityId;
1491 if community_id.len() != 64 {
1492 return Err(VectorError::Other("malformed community id".into()));
1493 }
1494 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1495 match crate::db::community::community_protocol(&cid).ok().flatten() {
1496 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1497 .map_err(VectorError::Other)?
1498 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1499 Some(_) => Err(VectorError::Other(
1500 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1501 )),
1502 None => Err(VectorError::Other("community not found".into())),
1503 }
1504 }
1505
1506 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1507 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1508 .map(crate::community::ChannelId)
1509 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1510 }
1511
1512 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1517 use crate::community::{v2::service, transport::LiveTransport};
1518 let community = Self::v2_community(community_id)?;
1519 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1520 let id = if private {
1521 service::create_private_channel(&transport, &community, name).await
1522 } else {
1523 service::create_public_channel(&transport, &community, name).await
1524 }
1525 .map_err(VectorError::Other)?;
1526 if let Some(client) = state::nostr_client() {
1529 crate::community::v2::realtime::refresh_subscription(&client).await;
1530 }
1531 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1532 }
1533
1534 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &str) -> Result<()> {
1537 use crate::community::{v2::service, transport::LiveTransport};
1538 let community = Self::v2_community(community_id)?;
1539 let id = Self::channel_id_of(channel_id)?;
1540 let mut meta = community
1541 .channel(&id)
1542 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1543 .metadata();
1544 meta.name = name.to_string();
1545 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1546 service::edit_channel_metadata(&transport, &community, &id, &meta)
1547 .await
1548 .map_err(VectorError::Other)
1549 }
1550
1551 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1554 use crate::community::{v2::service, transport::LiveTransport};
1555 let community = Self::v2_community(community_id)?;
1556 let id = Self::channel_id_of(channel_id)?;
1557 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1558 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1559 service::delete_channel(&transport, &community, &id, &name)
1560 .await
1561 .map_err(VectorError::Other)
1562 }
1563
1564 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1567 use crate::community::{v2::service, transport::LiveTransport};
1568 let community = Self::v2_community(community_id)?;
1569 let id = Self::channel_id_of(channel_id)?;
1570 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1571 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1572 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1573 service::grant_channel_access(&transport, &community, &id, &member)
1574 .await
1575 .map_err(VectorError::Other)
1576 }
1577
1578 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1582 use crate::community::{v2::service, transport::LiveTransport};
1583 let community = Self::v2_community(community_id)?;
1584 let id = Self::channel_id_of(channel_id)?;
1585 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1586 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1587 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1588 service::revoke_channel_access(&transport, &community, &id, &member)
1589 .await
1590 .map_err(VectorError::Other)
1591 }
1592
1593 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1601 use nostr_sdk::prelude::{PublicKey, ToBech32};
1602 let community = Self::v2_community(community_id)?;
1603 let id = Self::channel_id_of(channel_id)?;
1604 let ch = community
1605 .channel(&id)
1606 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1607 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1610 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1611 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1612 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1613 let access_ids = roster.channel_role_ids(&chan_hex);
1614 let roles: Vec<serde_json::Value> = roster
1615 .channel_roles(&chan_hex)
1616 .into_iter()
1617 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1618 .collect();
1619 let members: Vec<String> = roster
1620 .grants
1621 .iter()
1622 .filter(|g| !banned.contains(&g.member))
1623 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1624 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1625 .collect();
1626 Ok(serde_json::json!({
1627 "channel_id": chan_hex,
1628 "private": ch.private,
1629 "readable": !(ch.private && ch.key.is_none()),
1630 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1631 "roles": roles,
1632 "members": members,
1633 }))
1634 }
1635
1636 pub async fn create_public_invite(
1641 &self,
1642 community_id: &str,
1643 expires_at_ms: Option<u64>,
1644 label: Option<String>,
1645 ) -> Result<String> {
1646 use crate::community::{service, transport::LiveTransport, CommunityId};
1647 if community_id.len() != 64 {
1648 return Err(VectorError::Other("malformed community id".into()));
1649 }
1650 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1651 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1653 crate::db::community::community_protocol(&cid).ok()
1654 {
1655 let community = crate::db::community::load_community_v2(&cid)
1656 .map_err(VectorError::Other)?
1657 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1658 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1659 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1662 let minted =
1663 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1664 .await
1665 .map_err(VectorError::Other)?;
1666 return Ok(minted.url);
1667 }
1668 let community = crate::db::community::load_community(&CommunityId(
1669 crate::simd::hex::hex_to_bytes_32(community_id),
1670 ))
1671 .map_err(VectorError::Other)?
1672 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1673 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1674 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1675 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1676 .await
1677 .map_err(VectorError::Other)?;
1678 Ok(url)
1679 }
1680
1681 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1685 use crate::community::{service, CommunityId};
1686 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1687
1688 let session = crate::state::SessionGuard::capture();
1689 let my_pk = crate::state::my_public_key()
1690 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1691
1692 if community_id.len() != 64 {
1693 return Err(VectorError::Other("malformed community id".into()));
1694 }
1695 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1696 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1702 crate::db::community::community_protocol(&cid).ok()
1703 {
1704 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1705 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1706 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1707 let bundle = {
1715 let lock = crate::community::v2::realtime::follow_lock(&cid);
1716 let _rotation = lock.lock().await;
1717 let community = crate::db::community::load_community_v2(&cid)
1718 .map_err(VectorError::Other)?
1719 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1720 crate::community::v2::service::bundle_of(
1721 &community,
1722 crate::community::v2::service::BundleAudience::Member(recipient),
1723 Some(my_pk),
1724 None,
1725 None,
1726 )
1727 };
1728 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1729 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1732 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1733 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1734 let rumor = nostr_sdk::prelude::EventBuilder::new(
1735 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1736 bundle_json,
1737 )
1738 .tag(expiry_tag.clone())
1739 .finalize_unsigned_with_id(my_pk);
1740 let k_tag = nostr_sdk::prelude::Tag::custom(
1741 "k",
1742 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1743 );
1744 if !session.is_valid() {
1745 return Err(VectorError::Other("account changed".into()));
1746 }
1747 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1748 .await
1749 .map_err(VectorError::Other)?;
1750 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1751 }
1752 let community = crate::db::community::load_community(&CommunityId(
1753 crate::simd::hex::hex_to_bytes_32(community_id),
1754 ))
1755 .map_err(VectorError::Other)?
1756 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1757
1758 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1759 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1760 }
1761 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1762 .map_err(|_| VectorError::Other("invalid npub".into()))?
1763 .to_hex();
1764 if crate::db::community::get_community_banlist(community_id)
1765 .map_err(VectorError::Other)?
1766 .iter()
1767 .any(|b| b == &invitee_hex)
1768 {
1769 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1770 }
1771
1772 if !session.is_valid() {
1774 return Err(VectorError::Other("account changed during invite".into()));
1775 }
1776
1777 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1778 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1779 .map_err(VectorError::Other)?;
1780 let pending_id = format!("community-invite-{}", community_id);
1781 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1783 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1784
1785 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1786 .await
1787 .map_err(VectorError::Other)?;
1788
1789 Ok(serde_json::json!({
1790 "community_id": community_id,
1791 "invitee": invitee_npub,
1792 "wrap_event_id": result.event_id,
1793 }))
1794 }
1795
1796 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1801 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1802 }
1803
1804 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1809 use crate::community::{service, transport::LiveTransport, CommunityId};
1810 if community_id.len() != 64 {
1811 return Err(VectorError::Other("malformed community id".into()));
1812 }
1813 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1814 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1815 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1818 let community = crate::db::community::load_community_v2(&cid)
1819 .map_err(VectorError::Other)?
1820 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1821 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1822 .await
1823 .map_err(VectorError::Other);
1824 }
1825 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1826 let community = crate::db::community::load_community(&cid)
1827 .map_err(VectorError::Other)?
1828 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1829 service::revoke_public_invite(&transport, &community, &token_bytes)
1830 .await
1831 .map_err(VectorError::Other)
1832 }
1833
1834 pub async fn send_community_message(
1836 &self,
1837 channel_id: &str,
1838 content: &str,
1839 replied_to: Option<&str>,
1840 ) -> Result<String> {
1841 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1842 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1844 let community = crate::db::community::load_community_v2(&id)
1845 .map_err(VectorError::Other)?
1846 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1847 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1848 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1849 let reply = match replied_to.filter(|r| !r.is_empty()) {
1852 Some(parent_id) => {
1853 let author_hex = {
1854 let st = state::STATE.lock().await;
1855 st.find_message(parent_id)
1856 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1857 .map(|pk| pk.to_hex())
1858 .unwrap_or_default()
1859 };
1860 Some((parent_id.to_string(), author_hex))
1861 }
1862 None => None,
1863 };
1864 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1865 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1868 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1869 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1870 .await
1871 .map_err(VectorError::Other);
1872 }
1873 let (community, channel) = self.resolve_channel(channel_id)?;
1874 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1875 let reply = replied_to.filter(|r| !r.is_empty());
1876 let ms = std::time::SystemTime::now()
1877 .duration_since(std::time::UNIX_EPOCH)
1878 .map(|d| d.as_millis() as u64)
1879 .unwrap_or(0);
1880 let unsigned = envelope::build_inner_typed(
1881 author_pk,
1882 &channel.id,
1883 channel.epoch,
1884 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1885 content,
1886 ms,
1887 reply,
1888 &[],
1889 );
1890 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1891 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1892 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1893 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1894 let session = state::SessionGuard::capture();
1895 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1896 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1897 .await
1898 .map_err(VectorError::Other)?;
1899 if !session.is_valid() {
1902 return Ok(message_id);
1903 }
1904 let echoed = {
1905 let mut st = state::STATE.lock().await;
1906 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1907 };
1908 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1909 let _ = crate::db::events::save_message(channel_id, &msg).await;
1910 }
1911 Ok(message_id)
1912 }
1913
1914 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1918 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1919 let path = std::path::Path::new(file_path);
1920 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1921 if bytes.is_empty() {
1922 return Err(VectorError::Other("Empty file".into()));
1923 }
1924 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1925 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1926
1927 let session = state::SessionGuard::capture();
1930 let v2_target = match self.v2_community_for_channel(channel_id)? {
1933 Some(id) => Some(
1934 crate::db::community::load_community_v2(&id)
1935 .map_err(VectorError::Other)?
1936 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1937 ),
1938 None => None,
1939 };
1940 let v1_target = match v2_target {
1941 Some(_) => None,
1942 None => Some(self.resolve_channel(channel_id)?),
1943 };
1944 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1945
1946 let file_hash = crate::crypto::sha256_hex(&bytes);
1947 let mime = crate::crypto::mime_from_extension(&extension);
1948 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1949
1950 let download_dir = crate::db::get_download_dir();
1952 let _ = std::fs::create_dir_all(&download_dir);
1953 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1954 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1955 let _ = std::fs::write(&local_path, &bytes);
1956
1957 let params = crate::crypto::generate_encryption_params();
1959 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1960 let encrypted_size = encrypted.len() as u64;
1961
1962 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1963 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1964 let servers = crate::blossom_servers::compute_enabled_servers();
1965 if servers.is_empty() {
1966 return Err(VectorError::Other("No Blossom servers configured".into()));
1967 }
1968 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1969 let url = crate::blossom::upload_blob_with_progress_and_failover(
1970 signer.clone(),
1971 servers,
1972 std::sync::Arc::new(encrypted),
1973 Some(mime),
1974 true,
1975 noop_progress,
1976 Some(3),
1977 Some(std::time::Duration::from_secs(2)),
1978 None,
1979 ).await.map_err(VectorError::Other)?;
1980
1981 let attachment = crate::types::Attachment {
1982 id: file_hash.clone(),
1983 key: params.key.clone(),
1984 nonce: params.nonce.clone(),
1985 extension: extension.clone(),
1986 name: filename.clone(),
1987 url,
1988 path: local_path.to_string_lossy().to_string(),
1989 size: encrypted_size,
1990 img_meta,
1991 downloading: false,
1992 downloaded: true,
1993 ..Default::default()
1994 };
1995 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
1996
1997 if !session.is_valid() {
1999 return Err(VectorError::Other("account changed during upload".into()));
2000 }
2001 if let Some(community) = v2_target {
2003 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2004 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2005 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2006 .await
2007 .map_err(VectorError::Other);
2008 }
2009 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2010 let ms = std::time::SystemTime::now()
2011 .duration_since(std::time::UNIX_EPOCH)
2012 .map(|d| d.as_millis() as u64)
2013 .unwrap_or(0);
2014 let unsigned = envelope::build_inner_full(
2015 author_pk, &channel.id, channel.epoch,
2016 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2017 );
2018 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2019 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2020 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2021 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2022 .await.map_err(VectorError::Other)?;
2023 let echoed = {
2025 let mut st = state::STATE.lock().await;
2026 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2027 };
2028 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2029 let _ = crate::db::events::save_message(channel_id, &m).await;
2030 }
2031 Ok(message_id)
2032 }
2033
2034 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2036 use crate::community::{service, transport::LiveTransport};
2037 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2038 let community = crate::db::community::load_community_v2(&id)
2039 .map_err(VectorError::Other)?
2040 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2041 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2042 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2043 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2044 .await
2045 .map_err(VectorError::Other);
2046 }
2047 let (community, channel) = self.resolve_channel(channel_id)?;
2048 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2049 service::publish_typing_signal(&transport, &community, &channel)
2050 .await
2051 .map_err(VectorError::Other)
2052 }
2053
2054 pub async fn send_community_reaction(
2057 &self,
2058 channel_id: &str,
2059 message_id: &str,
2060 emoji: &str,
2061 emoji_url: Option<&str>,
2062 ) -> Result<()> {
2063 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2064 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2065 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2066 }
2067 _ => Vec::new(),
2068 };
2069 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2070 let session = state::SessionGuard::capture();
2071 let community = crate::db::community::load_community_v2(&id)
2072 .map_err(VectorError::Other)?
2073 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2074 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2075 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2076 let held = {
2081 let st = state::STATE.lock().await;
2082 st.find_message(message_id)
2083 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2084 };
2085 let held = held.or_else(|| {
2086 crate::db::events::event_author(message_id)
2087 .ok()
2088 .flatten()
2089 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2090 });
2091 let target_author = match held {
2092 Some(pk) => pk,
2093 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2094 .await
2095 .map_err(VectorError::Other)?
2096 .iter()
2097 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2098 .map(|f| f.event.opened().author)
2099 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2100 };
2101 if !session.is_valid() {
2103 return Err(VectorError::Other("account changed before send".into()));
2104 }
2105 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2106 return crate::community::v2::service::send_reaction(
2111 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2112 )
2113 .await
2114 .map(|_| ())
2115 .map_err(VectorError::Other);
2116 }
2117 self.publish_community_control(
2118 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2119 ).await
2120 }
2121
2122 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2124 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2125 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2126 let community = crate::db::community::load_community_v2(&id)
2127 .map_err(VectorError::Other)?
2128 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2129 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2130 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2131 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2132 .await
2133 .map(|_| ())
2134 .map_err(VectorError::Other);
2135 }
2136 self.publish_community_control(
2137 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2138 ).await
2139 }
2140
2141 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2145 let channel_id = {
2146 let st = state::STATE.lock().await;
2147 match st.find_message(message_id) {
2148 Some((chat, _)) => chat.id.clone(),
2149 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2150 }
2151 };
2152 self.delete_community_message_in(&channel_id, message_id).await
2153 }
2154
2155 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2159 use crate::community::{service, transport::LiveTransport};
2160 let session = state::SessionGuard::capture();
2161 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2162
2163 let attachment_urls: Vec<String> = {
2166 let st = state::STATE.lock().await;
2167 st.find_message(message_id)
2168 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2169 .unwrap_or_default()
2170 };
2171
2172 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2173 let community = crate::db::community::load_community_v2(&id)
2176 .map_err(VectorError::Other)?
2177 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2178 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2179 crate::community::v2::service::send_delete(
2180 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2181 )
2182 .await
2183 .map_err(VectorError::Other)?;
2184 } else {
2185 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2187 let _ = service::delete_message(&transport, message_id).await;
2188 }
2189 self.publish_community_control(
2191 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2192 ).await?;
2193 }
2194 if !attachment_urls.is_empty() {
2196 if let Some(_client) = state::nostr_client() {
2197 if let Ok(signer) = crate::signer::active_signer() {
2198 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2199 }
2200 }
2201 }
2202 if !session.is_valid() {
2205 return Ok(());
2206 }
2207 let removed_chat = {
2208 let mut st = state::STATE.lock().await;
2209 st.remove_message(message_id).map(|(cid, _)| cid)
2210 };
2211 let _ = crate::db::events::delete_event(message_id).await;
2212 traits::emit_event_json("message_removed", serde_json::json!({
2213 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2214 }));
2215 Ok(())
2216 }
2217
2218 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2224 use crate::community::transport::LiveTransport;
2225 let session = state::SessionGuard::capture();
2226 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2227
2228 let author_npub = {
2231 let st = state::STATE.lock().await;
2232 st.find_message(message_id).and_then(|(_, m)| m.npub)
2233 };
2234 let author_npub = match author_npub {
2235 Some(n) => n,
2236 None => crate::db::events::event_author(message_id)
2237 .ok()
2238 .flatten()
2239 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2240 };
2241 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2242 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2243
2244 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2245 let community = crate::db::community::load_community_v2(&id)
2246 .map_err(VectorError::Other)?
2247 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2248 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2249 crate::community::v2::service::moderation_delete(
2250 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2251 )
2252 .await
2253 .map_err(VectorError::Other)?;
2254 } else {
2255 let cid = crate::db::community::community_id_for_channel(channel_id)
2256 .map_err(VectorError::Other)?
2257 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2258 let community = crate::db::community::load_community(&crate::community::CommunityId(
2259 crate::simd::hex::hex_to_bytes_32(&cid),
2260 ))
2261 .map_err(VectorError::Other)?
2262 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2263 let channel = community
2264 .channels
2265 .iter()
2266 .find(|c| c.id.to_hex() == channel_id)
2267 .cloned()
2268 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2269 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2270 .await
2271 .map_err(VectorError::Other)?;
2272 }
2273
2274 if !session.is_valid() {
2277 return Ok(());
2278 }
2279 let removed_chat = {
2280 let mut st = state::STATE.lock().await;
2281 st.remove_message(message_id).map(|(cid, _)| cid)
2282 };
2283 let _ = crate::db::events::delete_event(message_id).await;
2284 traits::emit_event_json("message_removed", serde_json::json!({
2285 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2286 }));
2287 Ok(())
2288 }
2289
2290 async fn publish_community_control(
2293 &self,
2294 channel_id: &str,
2295 kind: u16,
2296 content: &str,
2297 target: &str,
2298 emoji_tags: &[crate::types::EmojiTag],
2299 ) -> Result<()> {
2300 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2301 let (community, channel) = self.resolve_channel(channel_id)?;
2302 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2303 let ms = std::time::SystemTime::now()
2304 .duration_since(std::time::UNIX_EPOCH)
2305 .map(|d| d.as_millis() as u64)
2306 .unwrap_or(0);
2307 let unsigned = envelope::build_inner_typed(
2308 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2309 );
2310 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2311 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2312 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2313 let session = state::SessionGuard::capture();
2314 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2315 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2316 .await.map_err(VectorError::Other)?;
2317 if !session.is_valid() {
2320 return Ok(());
2321 }
2322 let outcome = {
2323 let mut st = state::STATE.lock().await;
2324 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2325 };
2326 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2327 if let Some(ev) = edit_event {
2328 let mut ev = (*ev).clone();
2329 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2330 let _ = crate::db::events::save_event(&ev).await;
2331 } else {
2332 let _ = crate::db::events::save_message(channel_id, &message).await;
2333 }
2334 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2335 }
2336 Ok(())
2337 }
2338
2339 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2347 use crate::community::{inbound, send, service, transport::LiveTransport};
2348 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2349 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2354 let warnings = if community::v2::realtime::follow_worker_running() {
2355 community::v2::realtime::enqueue_follow(&id);
2356 Vec::new()
2357 } else {
2358 Self::v2_inline_follow(&id).await
2359 };
2360 let new = Self::v2_backfill_channel(
2365 &id, channel_id, limit, 8, None,
2366 crate::community::transport::Evidence::Fast, 12,
2367 ).await;
2368 return Ok((new, warnings));
2369 }
2370 let (community, _) = self.resolve_channel(channel_id)?;
2371 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2372 let mut warnings: Vec<String> = Vec::new();
2373
2374 match service::catch_up_server_root(&transport, &community).await {
2382 Ok(c) if c.removed => {
2383 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2385 return Ok((0, warnings));
2386 }
2387 Ok(_) => {}
2388 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2389 }
2390 let (community, _) = self.resolve_channel(channel_id)?;
2391
2392 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2398 warnings.push(format!("control fold failed: {e}"));
2399 }
2400 if service::am_i_banned(&community) {
2401 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2403 return Ok((0, warnings));
2404 }
2405 let (community, channel) = self.resolve_channel(channel_id)?;
2408 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2409 warnings.push(format!("channel catch-up failed: {e}"));
2410 }
2411 let (community, _) = self.resolve_channel(channel_id)?;
2415 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2416 warnings.push(format!("read-cut resume failed: {e}"));
2417 }
2418 let (community, channel) = self.resolve_channel(channel_id)?;
2419
2420 let session = state::SessionGuard::capture();
2422 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2423 .await
2424 .map_err(VectorError::Other)?;
2425 let outcomes = {
2426 let mut st = state::STATE.lock().await;
2427 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2428 };
2429 let mut new = 0usize;
2430 let mut pending: Vec<&crate::types::Message> = Vec::new();
2434 for o in &outcomes {
2435 if !session.is_valid() {
2437 pending.clear();
2438 break;
2439 }
2440 match o {
2441 inbound::IncomingEvent::NewMessage(m) => {
2442 pending.push(m);
2443 new += 1;
2444 }
2445 inbound::IncomingEvent::Updated { message, .. } => {
2446 pending.push(message);
2447 }
2448 inbound::IncomingEvent::Removed { target_id } => {
2449 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2450 let _ = crate::db::events::delete_event(target_id).await;
2451 }
2452 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2453 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2456 let _ = crate::db::events::delete_event(reaction_id).await;
2457 }
2458 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2459 let et = if *joined {
2460 crate::stored_event::SystemEventType::MemberJoined
2461 } else {
2462 crate::stored_event::SystemEventType::MemberLeft
2463 };
2464 let note = invited_by.as_ref().map(|by| match invited_label {
2466 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2467 _ => by.clone(),
2468 });
2469 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;
2470 }
2471 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2472 community::service::persist_webxdc_signal(
2475 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2476 ).await;
2477 }
2478 inbound::IncomingEvent::Kicked { community_id }
2479 | inbound::IncomingEvent::SelfLeft { community_id } => {
2480 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2485 let _ = crate::db::community::delete_community_retain_keys(community_id);
2486 break;
2487 }
2488 inbound::IncomingEvent::Typing { .. } => {
2489 }
2491 }
2492 }
2493 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2494 Ok((new, warnings))
2495 }
2496
2497 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2509 use crate::bot_interface::{self, ChatCommandsSnapshot};
2510 use nostr_sdk::prelude::ToBech32;
2511
2512 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2513 let mut relays: Vec<String> = Vec::new();
2514 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2515 if let Some(cid_hex) = community_hex {
2516 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2517 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2518 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2519 relays = community.relays.clone();
2520 } else {
2521 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2522 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2523 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2524 };
2525 relays = community.relays.clone();
2526 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2527 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2528 members.push(pk);
2529 }
2530 }
2531 }
2532 let state = crate::state::STATE.lock().await;
2533 for pk in members {
2534 let Ok(npub) = pk.to_bech32();
2535 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2536 bots.push(pk);
2537 }
2538 }
2539 } else if chat_id.starts_with("npub1") {
2540 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2541 let is_bot = {
2542 let state = crate::state::STATE.lock().await;
2543 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2544 };
2545 if is_bot {
2546 bots.push(pk);
2547 if let Some(client) = crate::state::nostr_client() {
2550 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2551 }
2552 }
2553 }
2554 }
2555
2556 if bots.is_empty() {
2557 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2558 }
2559 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2562 relays.sort();
2563 relays.dedup();
2564 bots.sort_by_key(|p| p.to_hex());
2567 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2568 let commands = bot_interface::assemble_from_store(&bot_hexes);
2569 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2570 if !fresh {
2571 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2572 }
2573 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2574 }
2575
2576 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2581 use nostr_sdk::prelude::ToBech32;
2582 match Self::load_v2_if_v2(community_id) {
2588 Ok(Some(community)) => {
2589 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2590 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2591 if cursor == 0 {
2592 if crate::community::v2::realtime::follow_worker_running() {
2593 crate::community::v2::realtime::enqueue_follow(community.id());
2594 } else {
2595 let session = state::SessionGuard::capture();
2596 let c2 = community.clone();
2597 tokio::spawn(async move {
2598 if !session.is_valid() {
2599 return;
2600 }
2601 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2602 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2, &session).await, Ok(fresh) if !fresh.is_empty()) {
2603 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2604 }
2605 });
2606 }
2607 }
2608 return crate::community::v2::service::stored_memberlist(&community)
2609 .unwrap_or_default()
2610 .into_iter()
2611 .filter_map(|pk| pk.to_bech32().ok())
2612 .map(|npub| serde_json::json!({ "npub": npub }))
2613 .collect();
2614 }
2615 Ok(None) => {} Err(_) => return Vec::new(),
2618 }
2619 crate::db::community::community_member_activity(community_id)
2620 .unwrap_or_default()
2621 .into_iter()
2622 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2623 .collect()
2624 }
2625
2626 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2630 use crate::community::transport::LiveTransport;
2631 let session = state::SessionGuard::capture();
2632 let lock = crate::community::v2::realtime::follow_lock(id);
2637 let _guard = lock.lock().await;
2638 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2639 let mut warnings: Vec<String> = Vec::new();
2640 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2641 warnings.push("v2 community not found".to_string());
2642 return warnings;
2643 };
2644 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2645 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2646 Ok(f) if f.dissolved => return warnings,
2648 Ok(f) if f.self_removed => {
2649 if session.is_valid() {
2652 let _ = crate::db::community::delete_community(&cid_hex);
2653 }
2654 return warnings;
2655 }
2656 Ok(_) => {}
2657 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2658 }
2659 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2660 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
2661 Ok(Some(changed)) => {
2665 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2666 warnings.push(format!("v2 rekey follow failed: {e}"));
2667 }
2668 }
2669 Ok(None) => {}
2670 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2671 }
2672 }
2673 if let Some(me) = crate::my_public_key() {
2678 if crate::db::community::is_author_banned(&cid_hex, &me) && session.is_valid() {
2679 let _ = crate::db::community::delete_community(&cid_hex);
2680 }
2681 }
2682 warnings
2683 }
2684
2685 pub(crate) async fn v2_backfill_channel(
2697 id: &crate::community::CommunityId,
2698 channel_id: &str,
2699 limit: usize,
2700 max_pages: usize,
2701 since: Option<u64>,
2702 evidence: crate::community::transport::Evidence,
2703 transport_secs: u64,
2704 ) -> usize {
2705 let session = state::SessionGuard::capture();
2708 let Some(my_pk) = state::my_public_key() else { return 0 };
2709 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2713 return 0;
2714 }
2715 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
2716 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2717 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
2718 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2719 &transport,
2720 &community,
2721 &ch,
2722 limit.max(50),
2723 max_pages,
2724 since,
2725 evidence,
2726 |page| {
2731 let mut saw_message = false;
2732 for f in page {
2733 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2734 saw_message = true;
2735 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2736 return true;
2737 }
2738 }
2739 }
2740 !saw_message
2741 },
2742 )
2743 .await
2744 else {
2745 return 0;
2746 };
2747 Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await
2748 }
2749
2750 pub(crate) async fn v2_ingest_chat_page(
2754 channel_id: &str,
2755 my_pk: nostr_sdk::prelude::PublicKey,
2756 session: crate::state::SessionGuard,
2757 page: Vec<crate::community::v2::service::FetchedEvent>,
2758 ) -> usize {
2759 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2760 let mut new = 0usize;
2761 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
2763 for f in &page {
2764 if !session.is_valid() {
2766 break;
2767 }
2768 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2773 if opened.author != my_pk {
2774 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2775 let Ok(npub) = ToBech32::to_bech32(&opened.author);
2776 crate::community::service::persist_webxdc_signal(
2777 channel_id,
2778 &npub,
2779 &topic,
2780 addr.as_deref(),
2781 &opened.rumor_id.to_hex(),
2782 opened.at_ms / 1000,
2783 )
2784 .await;
2785 }
2786 }
2787 continue;
2788 }
2789 let outcome = {
2790 let mut st = state::STATE.lock().await;
2791 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2792 };
2793 if let Some(outcome) = outcome {
2794 if matches!(outcome, ChatPersist::New(_)) {
2795 new += 1;
2796 }
2797 outcomes.push(outcome);
2798 }
2799 }
2800 let mut pending: Vec<&crate::types::Message> = Vec::new();
2804 for outcome in &outcomes {
2805 if !session.is_valid() {
2806 pending.clear();
2807 break;
2808 }
2809 match outcome {
2810 ChatPersist::New(m) => pending.push(m),
2811 ChatPersist::Updated { message, edit_event } => match edit_event {
2812 Some(ev) => {
2813 let mut ev = (**ev).clone();
2814 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
2817 ev.chat_id = cid;
2818 }
2819 let _ = crate::db::events::save_event(&ev).await;
2820 }
2821 None => pending.push(message),
2822 },
2823 ChatPersist::Removed(target_id) => {
2824 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2825 let _ = crate::db::events::delete_event(target_id).await;
2826 }
2827 ChatPersist::ReactionRemoved { reaction_id, message } => {
2828 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2829 let _ = crate::db::events::delete_event(reaction_id).await;
2830 pending.push(message);
2831 }
2832 }
2833 }
2834 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2835 if session.is_valid() {
2841 for outcome in &outcomes {
2842 match outcome {
2843 ChatPersist::New(msg) => crate::traits::emit_event(
2844 "message_new",
2845 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
2846 ),
2847 ChatPersist::Updated { message, .. }
2848 | ChatPersist::ReactionRemoved { message, .. } => {
2849 let mut message = message.clone();
2850 let target_id = message.id.clone();
2851 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
2852 }
2853 ChatPersist::Removed(target_id) => crate::traits::emit_event(
2854 "message_removed",
2855 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
2856 ),
2857 }
2858 }
2859 }
2860 new
2861 }
2862
2863 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2867 if community_id.len() != 64 {
2868 return Ok(None);
2869 }
2870 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2871 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
2872 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
2873 _ => Ok(None),
2874 }
2875 }
2876
2877 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
2882 use crate::community::CommunityId;
2883 if community_id.len() != 64 {
2884 return Err(VectorError::Other("malformed community id".into()));
2885 }
2886 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
2887 .map_err(VectorError::Other)?
2888 .ok_or_else(|| VectorError::Other("community not found".into()))
2889 }
2890
2891 fn admin_role_id_of(community_id: &str) -> Result<String> {
2892 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2893 roles.roles.iter()
2894 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
2895 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
2896 .map(|r| r.role_id.clone())
2897 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
2898 }
2899
2900 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
2904 use crate::community::service;
2905 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2906 use crate::community::roles::Permissions;
2907 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
2908 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
2909 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2910 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2913 if banned.contains(&me) && me != owner_hex {
2914 return Ok(serde_json::json!({
2915 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
2916 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
2917 }));
2918 }
2919 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
2920 return Ok(serde_json::json!({
2921 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
2922 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
2923 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
2924 "manage_admin_role": me == owner_hex,
2926 }));
2927 }
2928 let community = Self::load_community_hex(community_id)?;
2929 let caps = service::caller_capabilities(&community);
2930 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
2931 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
2932 .unwrap_or(false);
2933 Ok(serde_json::json!({
2934 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
2935 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
2936 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
2937 "manage_admin_role": manage_admin_role,
2938 }))
2939 }
2940
2941 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
2944 use nostr_sdk::prelude::{PublicKey, ToBech32};
2945 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2946 let owner = v2.owner().map_err(VectorError::Other)?;
2947 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2948 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2950 let admins: Vec<String> = roster.grants.iter()
2951 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
2952 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2953 .collect();
2954 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
2955 }
2956 let community = Self::load_community_hex(community_id)?;
2957 let owner = community.owner_attestation.as_ref()
2958 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
2959 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
2960 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2961 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
2962 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2963 .collect();
2964 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
2965 }
2966
2967 async fn converge_v2_authority(
2976 transport: &crate::community::transport::LiveTransport,
2977 community_id: &str,
2978 session: &crate::state::SessionGuard,
2979 ) {
2980 if !session.is_valid() {
2981 return;
2982 }
2983 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
2986 let _ = crate::community::v2::service::follow_control(transport, &fresh, session).await;
2987 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh, session).await {
2992 if !added.is_empty() && session.is_valid() {
2993 traits::emit_event_json(
2994 "community_refreshed",
2995 serde_json::json!({ "community_id": community_id }),
2996 );
2997 }
2998 }
2999 }
3000 }
3001
3002 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3004 use crate::community::{service, transport::LiveTransport};
3005 let session = crate::state::SessionGuard::capture();
3006 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3007 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3008 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3009 crate::community::v2::service::grant_admin(&transport, &v2, &member)
3010 .await
3011 .map_err(VectorError::Other)?;
3012 Self::converge_v2_authority(&transport, community_id, &session).await;
3013 return Ok(());
3014 }
3015 let community = Self::load_community_hex(community_id)?;
3016 let role_id = Self::admin_role_id_of(community_id)?;
3017 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3018 }
3019
3020 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3022 use crate::community::{service, transport::LiveTransport};
3023 let session = crate::state::SessionGuard::capture();
3024 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3025 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3026 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3027 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
3028 .await
3029 .map_err(VectorError::Other)?;
3030 Self::converge_v2_authority(&transport, community_id, &session).await;
3031 return Ok(());
3032 }
3033 let community = Self::load_community_hex(community_id)?;
3034 let role_id = Self::admin_role_id_of(community_id)?;
3035 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3036 }
3037
3038 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
3040 use crate::community::{service, transport::LiveTransport};
3041 let session = crate::state::SessionGuard::capture();
3042 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3043 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3044 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3045 crate::community::v2::service::kick_member(&transport, &v2, &pk)
3046 .await
3047 .map_err(VectorError::Other)?;
3048 if session.is_valid() {
3053 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2, &session).await {
3054 if !fresh.is_empty() {
3055 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
3056 }
3057 }
3058 }
3059 Self::converge_v2_authority(&transport, community_id, &session).await;
3060 return Ok(());
3061 }
3062 let community = Self::load_community_hex(community_id)?;
3063 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
3064 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
3065 }
3066
3067 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
3070 use crate::community::{service, transport::LiveTransport, CommunityId};
3071 let session = crate::state::SessionGuard::capture();
3072 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3073 let hex = pk.to_hex();
3074 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3075 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
3077 list.retain(|h| h != &hex);
3078 if banned {
3079 list.push(hex);
3080 }
3081 if community_id.len() == 64 {
3085 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3086 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3087 let community = {
3094 let lock = crate::community::v2::realtime::follow_lock(&cid);
3095 let _rotation = lock.lock().await;
3096 let community = crate::db::community::load_community_v2(&cid)
3097 .map_err(VectorError::Other)?
3098 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3099 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
3100 if banned {
3101 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
3102 }
3103 community
3104 };
3105 if banned {
3106 crate::community::v2::service::refound_community(&transport, &community, &[pk]).await.map_err(VectorError::Other)?;
3107 }
3108 Self::converge_v2_authority(&transport, community_id, &session).await;
3109 return Ok(());
3110 }
3111 }
3112 let community = Self::load_community_hex(community_id)?;
3113 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
3114 }
3115
3116 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
3120 use crate::community::{service, transport::LiveTransport, CommunityId};
3121 if community_id.len() != 64 {
3122 return Err(VectorError::Other("malformed community id".into()));
3123 }
3124 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3125 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3126 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3129 let community = crate::db::community::load_community_v2(&cid)
3130 .map_err(VectorError::Other)?
3131 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3132 return crate::community::v2::service::dissolve_community(&transport, &community)
3133 .await
3134 .map_err(VectorError::Other);
3135 }
3136 let community = Self::load_community_hex(community_id)?;
3137 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
3138 }
3139
3140 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
3143 use crate::community::{service, transport::LiveTransport, CommunityId};
3144 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3145 if community_id.len() == 64 {
3150 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3151 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3152 let community = crate::db::community::load_community_v2(&cid)
3153 .map_err(VectorError::Other)?
3154 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3155 let mut meta = community.metadata();
3156 if let Some(n) = name {
3157 meta.name = n.to_string();
3158 }
3159 if let Some(d) = description {
3160 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
3161 }
3162 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
3163 .await
3164 .map_err(VectorError::Other);
3165 }
3166 }
3167 let mut community = Self::load_community_hex(community_id)?;
3168 if let Some(n) = name { community.name = n.to_string(); }
3169 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
3170 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
3171 }
3172
3173
3174
3175 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
3178 use crate::community::{transport::LiveTransport, CommunityId};
3179 if community_id.len() != 64 {
3180 return Err(VectorError::Other("malformed community id".into()));
3181 }
3182 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3183 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3185 let session = state::SessionGuard::capture();
3186 let channel_ids: Vec<String> =
3187 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
3188 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3189 crate::community::v2::service::leave_community(&transport, &v2)
3190 .await
3191 .map_err(VectorError::Other)?;
3192 if !session.is_valid() {
3193 return Err(VectorError::Other("account changed during leave".into()));
3194 }
3195 let mut st = state::STATE.lock().await;
3196 st.chats.retain(|c| !channel_ids.contains(&c.id));
3197 return Ok(());
3198 }
3199 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
3200 let channel_ids: Vec<String> = community
3201 .as_ref()
3202 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
3203 .unwrap_or_default();
3204 if let Some(ref c) = community {
3206 if let Some(primary) = c.channels.first() {
3207 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3208 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
3209 }
3210 }
3211 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
3213 {
3214 let mut st = state::STATE.lock().await;
3215 st.chats.retain(|c| !channel_ids.contains(&c.id));
3216 }
3217 Ok(())
3218 }
3219
3220 fn resolve_channel(
3222 &self,
3223 channel_id: &str,
3224 ) -> Result<(crate::community::Community, crate::community::Channel)> {
3225 use crate::community::CommunityId;
3226 let community_id = crate::db::community::community_id_for_channel(channel_id)
3227 .map_err(VectorError::Other)?
3228 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
3229 if community_id.len() != 64 {
3230 return Err(VectorError::Other("malformed community id".into()));
3231 }
3232 let community = crate::db::community::load_community(&CommunityId(
3233 crate::simd::hex::hex_to_bytes_32(&community_id),
3234 ))
3235 .map_err(VectorError::Other)?
3236 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
3237 let channel = community
3238 .channels
3239 .iter()
3240 .find(|c| c.id.to_hex() == channel_id)
3241 .cloned()
3242 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
3243 Ok((community, channel))
3244 }
3245
3246
3247 pub async fn sync_dms(
3264 &self,
3265 since_days: Option<u64>,
3266 handler: &dyn InboundEventHandler,
3267 ) -> Result<(u32, u32)> {
3268 use futures_util::StreamExt;
3269 use nostr_sdk::prelude::*;
3270
3271 let client = state::nostr_client()
3272 .ok_or(VectorError::Other("Not connected".into()))?;
3273 let my_pk = state::my_public_key()
3274 .ok_or(VectorError::Other("Not logged in".into()))?;
3275
3276 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
3278
3279 let (items, filter) = if let Some(days) = since_days {
3281 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
3282 let items: Vec<(EventId, Timestamp)> = all_items.iter()
3283 .filter(|(_, ts)| ts.as_secs() >= since_ts)
3284 .cloned()
3285 .collect();
3286 let filter = Filter::new()
3287 .pubkey(my_pk)
3288 .kind(Kind::GiftWrap)
3289 .since(Timestamp::from_secs(since_ts));
3290 (items, filter)
3291 } else {
3292 let filter = Filter::new()
3293 .pubkey(my_pk)
3294 .kind(Kind::GiftWrap);
3295 (all_items, filter)
3296 };
3297
3298 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
3299
3300 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
3302 .direction(nostr_sdk::prelude::SyncDirection::Down)
3303 .initial_timeout(std::time::Duration::from_secs(10))
3304 .dry_run();
3305
3306 let relay_map = client.relays().await;
3310 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
3311 relay_map.iter()
3312 .map(|(url, relay)| (url.clone(), relay.clone()))
3313 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
3314 drop(relay_map);
3315 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
3316 if !skipped_no_neg.is_empty() {
3317 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
3318 }
3319
3320 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
3324 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
3325 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
3326 .min(neg_outer);
3327 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
3328 for (url, relay) in &all_relays {
3329 let url = url.clone();
3330 let relay = relay.clone();
3331 let f = filter.clone();
3332 let i = items.clone();
3333 let o = sync_opts.clone();
3334 relay_futs.push(async move {
3335 if !negentropy::wait_connected(&relay, connect_allowance).await {
3336 return (url, None, false);
3337 }
3338 let result = tokio::time::timeout(
3341 neg_outer,
3342 relay.sync(f).items(i).opts(o),
3343 ).await;
3344 let connected = relay.status() == RelayStatus::Connected;
3345 (url, Some(result), connected)
3346 });
3347 }
3348
3349 let cap_session = state::SessionGuard::capture();
3351 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
3352 while let Some((url, result, connected)) = relay_futs.next().await {
3353 let Some(result) = result else {
3354 log_warn!("[SyncDMs] {} skipped: not connected", url);
3355 continue;
3356 };
3357 match result {
3358 Ok(Ok(recon)) => {
3359 let count = recon.remote.len();
3360 all_missing.extend(recon.remote);
3361 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
3362 if cap_session.is_valid() {
3363 negentropy::record_neg_support(url.as_str(), true);
3364 }
3365 }
3366 Ok(Err(e)) => {
3367 log_warn!("[SyncDMs] {} failed: {}", url, e);
3368 if cap_session.is_valid()
3369 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
3370 {
3371 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
3372 negentropy::record_neg_support(url.as_str(), false);
3373 }
3374 }
3375 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
3376 }
3377 }
3378
3379 let mut total_events = 0u32;
3380 let mut new_messages = 0u32;
3381
3382 if !skipped_no_neg.is_empty() {
3387 let req_filter = filter.clone().limit(500);
3388 match client
3389 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3390 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
3391 ))
3392 .timeout(std::time::Duration::from_secs(20))
3393 .await
3394 {
3395 Ok(stream) => {
3396 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
3397 tokio::pin!(stream);
3398 while let Some((_relay, res)) = stream.next().await {
3399 let Ok(event) = res else { continue };
3400 if !cap_session.is_valid() { break; }
3404 if !seen.insert(event.id.to_bytes()) { continue; }
3405 total_events += 1;
3406 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
3407 if event_handler::commit_prepared_event(prepared, false, handler).await {
3408 new_messages += 1;
3409 }
3410 }
3411 }
3412 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
3413 }
3414 }
3415
3416 if all_missing.is_empty() {
3417 log_info!("[SyncDMs] No missing events");
3418 return Ok((total_events, new_messages));
3419 }
3420
3421 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
3423 let ids: Vec<EventId> = all_missing.into_iter().collect();
3424 let relay_strs: Vec<String> = client.relays().await.keys()
3425 .map(|u| u.to_string()).collect();
3426
3427 const BATCH_SIZE: usize = 500;
3428
3429 for batch in ids.chunks(BATCH_SIZE) {
3430 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
3433 match client
3434 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3435 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
3436 ))
3437 .timeout(std::time::Duration::from_secs(30))
3438 .await
3439 {
3440 Ok(stream) => {
3441 let client_clone = client.clone();
3442 let prepared_stream = stream
3443 .filter_map(|(_relay, res)| async move { res.ok() })
3444 .map(move |event| {
3445 let c = client_clone.clone();
3446 tokio::spawn(async move {
3447 event_handler::prepare_event(event, &c, my_pk).await
3448 })
3449 })
3450 .buffer_unordered(8);
3451 tokio::pin!(prepared_stream);
3452
3453 while let Some(result) = prepared_stream.next().await {
3454 total_events += 1;
3455 if let Ok(prepared) = result {
3456 if event_handler::commit_prepared_event(prepared, false, handler).await {
3457 new_messages += 1;
3458 }
3459 }
3460 }
3461 }
3462 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
3463 }
3464 }
3465
3466 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
3467 Ok((total_events, new_messages))
3468 }
3469
3470 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
3479 use nostr_sdk::prelude::*;
3480 let client = state::nostr_client()
3481 .ok_or(VectorError::Other("Not connected".into()))?;
3482 let my_pk = state::my_public_key()
3483 .ok_or(VectorError::Other("Not logged in".into()))?;
3484
3485 let filter = Filter::new()
3486 .pubkey(my_pk)
3487 .kind(Kind::GiftWrap)
3488 .limit(0);
3489
3490 let output = client.subscribe(filter).await
3491 .map_err(|e| VectorError::Nostr(e.to_string()))?;
3492 Ok(output.value)
3493 }
3494
3495 pub async fn sync_communities(&self) -> Result<()> {
3506 {
3510 use crate::community::{transport::LiveTransport, v2::service as v2};
3511 let bootstrap: Vec<String> = match crate::state::nostr_client() {
3512 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
3513 None => Vec::new(),
3514 };
3515 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3516 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
3517 let joined = outcome.joined;
3520 for c in &joined {
3521 if community::v2::realtime::follow_worker_running() {
3522 community::v2::realtime::enqueue_follow(c.id());
3523 } else {
3524 let _ = Self::v2_inline_follow(c.id()).await;
3525 }
3526 }
3527 if !joined.is_empty() {
3528 if let Some(client) = crate::state::nostr_client() {
3529 community::v2::realtime::refresh_subscription(&client).await;
3530 }
3531 }
3532 }
3533 }
3534
3535 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
3536 for id in ids {
3537 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
3538 if community::v2::realtime::follow_worker_running() {
3541 community::v2::realtime::enqueue_follow(&id);
3542 } else {
3543 let _ = Self::v2_inline_follow(&id).await;
3544 }
3545 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
3552 for ch in &c.channels {
3553 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
3554 let _ = Self::v2_backfill_channel(
3555 &id, &hex, 50, 2, None,
3556 crate::community::transport::Evidence::Fast, 12,
3557 ).await;
3558 }
3559 }
3560 continue;
3561 }
3562 if let Ok(Some(community)) = db::community::load_community(&id) {
3563 for ch in &community.channels {
3564 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
3565 }
3566 }
3567 }
3568 Ok(())
3569 }
3570
3571
3572 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
3604 use nostr_sdk::prelude::*;
3605
3606 let client = state::nostr_client()
3607 .ok_or(VectorError::Other("Not connected".into()))?;
3608 let my_pk = state::my_public_key()
3609 .ok_or(VectorError::Other("Not logged in".into()))?;
3610
3611 community::v2::streamauth::ensure_responder(&client);
3618
3619 community::v2::realtime::spawn_follow_worker(handler.clone());
3628 let _ = self.sync_communities().await;
3629 let _ = self.sync_dms(None, &NoOpEventHandler).await;
3630
3631 let dm_sub_id = self.subscribe_dms().await?;
3634 community::realtime::refresh_subscription(&client).await;
3635 community::v2::realtime::refresh_subscription(&client).await;
3636
3637 if let Some(monitor) = client.monitor() {
3644 let mut rx = monitor.subscribe();
3645 let session = state::SessionGuard::capture();
3646 tokio::spawn(async move {
3647 let mut last_resync: Option<std::time::Instant> = None;
3650 while let Ok(notification) = rx.recv().await {
3651 if !session.is_valid() {
3652 return;
3653 }
3654 let MonitorNotification::StatusChanged { status, .. } = notification;
3655 if status == RelayStatus::Connected {
3656 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
3657 continue;
3658 }
3659 let _ = VectorCore.sync_communities().await;
3660 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
3661 if let Some(c) = state::nostr_client() {
3662 community::realtime::refresh_subscription(&c).await;
3663 community::v2::realtime::refresh_subscription(&c).await;
3664 }
3665 last_resync = Some(std::time::Instant::now());
3666 }
3667 }
3668 });
3669 }
3670
3671 {
3675 let client_health = client.clone();
3676 let session = state::SessionGuard::capture();
3677 tokio::spawn(async move {
3678 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
3680 if !session.is_valid() {
3681 return;
3682 }
3683 for (url, relay) in client_health.relays().await {
3684 match relay.status() {
3685 RelayStatus::Connected => {
3686 let probe = tokio::time::timeout(
3687 std::time::Duration::from_secs(10),
3688 client_health
3689 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
3690 url.to_string(),
3691 [Filter::new().kind(Kind::Metadata).limit(1)],
3692 ))
3693 .timeout(std::time::Duration::from_secs(8)),
3694 )
3695 .await;
3696 if !matches!(probe, Ok(Ok(_))) {
3697 let _ = relay.disconnect();
3698 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
3699 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3700 }
3701 }
3702 RelayStatus::Terminated | RelayStatus::Disconnected => {
3703 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3704 }
3705 _ => {}
3706 }
3707 }
3708 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
3709 }
3710 });
3711 }
3712
3713 let client_for_closure = client.clone();
3714
3715 let mut notifications = client.notifications();
3718 while let Some(notification) = notifications.next().await {
3719 let handler = handler.clone();
3720 let c = client_for_closure.clone();
3721 let dm_sid = dm_sub_id.clone();
3722 {
3723 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
3727 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
3728 sending::note_relay_ok(event_id, *status);
3729 }
3730 }
3731 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
3732 if subscription_id == dm_sid {
3733 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
3735 event_handler::commit_prepared_event(prepared, true, &*handler).await;
3736 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3737 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3738 {
3739 let session = state::SessionGuard::capture();
3743 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
3744 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3745 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3746 {
3747 let session = state::SessionGuard::capture();
3749 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
3750 }
3751 }
3752 }
3753 }
3754
3755 Ok(())
3756 }
3757
3758 pub async fn logout(&self) {
3760 if let Some(client) = state::nostr_client() {
3761 let _ = client.disconnect().await;
3762 }
3763 db::close_database();
3764 }
3765
3766 pub async fn swap_session(&self) {
3774 state::bump_session_generation();
3776
3777 if let Some(client) = state::take_nostr_client() {
3780 let _ = client.shutdown().await;
3781 }
3782 db::close_database();
3783
3784 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
3786 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
3787 {
3788 use zeroize::Zeroize;
3789 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
3790 if let Some(s) = g.as_mut() { s.zeroize(); }
3791 *g = None;
3792 }
3793 if let Ok(mut g) = state::PENDING_NSEC.lock() {
3794 if let Some(s) = g.as_mut() { s.zeroize(); }
3795 *g = None;
3796 }
3797 }
3798
3799 {
3801 let mut st = state::STATE.lock().await;
3802 st.profiles.clear();
3803 st.chats.clear();
3804 st.db_loaded = false;
3805 st.is_syncing = false;
3806 }
3807 state::WRAPPER_ID_CACHE.lock().await.clear();
3808 state::PENDING_EVENTS.lock().await.clear();
3809 state::set_active_chat(None);
3810 crate::profile::sync::clear_profile_sync_queue();
3811 crate::inbox_relays::clear_inbox_relay_cache();
3812 crate::sending::clear_wrap_confirms();
3815 crate::emoji_packs::clear_nip65_cache();
3816 crate::db::clear_id_caches();
3820 crate::community::cache::clear();
3824 crate::community::realtime::clear().await;
3827 crate::community::v2::realtime::clear().await;
3828 crate::community::transport::clear_plane_pool();
3830 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
3834 }
3835}
3836
3837#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
3838mod transport_policy_tests {
3839 use std::time::Duration;
3840
3841 #[test]
3844 fn tor_transport_policy() {
3845 let short = Duration::from_secs(5);
3846 let long = Duration::from_secs(300);
3847
3848 crate::tor::set_tor_enabled_pref(false);
3851 assert_eq!(super::tor_proxy_target(), None);
3852 assert_eq!(super::relay_connect_timeout(short), short);
3853 assert_eq!(super::relay_request_timeout(short), short);
3854
3855 crate::tor::set_tor_enabled_pref(true);
3859 assert!(matches!(
3860 crate::tor::transport_state(),
3861 crate::tor::TorTransportState::RequiredButInactive
3862 ));
3863 assert_eq!(
3869 super::tor_proxy_target(),
3870 Some(crate::tor::blackhole_proxy_addr()),
3871 "Tor enabled but inactive must blackhole, never connect direct"
3872 );
3873 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
3874 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
3875
3876 for tor in [true, false] {
3879 crate::tor::set_tor_enabled_pref(tor);
3880 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
3881 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
3882 }
3883 }
3884}
3885
3886#[cfg(test)]
3887mod facade_tests {
3888 use super::*;
3889
3890 #[tokio::test]
3893 async fn download_attachment_rejects_private_url() {
3894 let att = crate::types::Attachment {
3895 url: "http://169.254.169.254/latest/meta-data/".to_string(),
3896 ..Default::default()
3897 };
3898 match VectorCore.download_attachment(&att).await {
3899 Err(VectorError::Other(msg)) => {
3900 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
3901 }
3902 other => panic!("expected SSRF rejection, got {other:?}"),
3903 }
3904 }
3905
3906 #[tokio::test]
3907 async fn download_attachment_rejects_empty_url() {
3908 let att = crate::types::Attachment::default();
3909 assert!(VectorCore.download_attachment(&att).await.is_err());
3910 }
3911
3912 #[tokio::test]
3916 async fn list_communities_and_channel_routing_are_protocol_aware() {
3917 use crate::community::transport::memory::MemoryRelay;
3918 use nostr_sdk::prelude::Keys;
3919
3920 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
3921 crate::db::close_database();
3922 crate::db::clear_id_caches();
3923 let tmp = tempfile::tempdir().unwrap();
3924 let acct = {
3926 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3927 let mut s = String::from("npub1");
3928 for i in 0..58 {
3929 s.push(B[(i * 7 + 3) % 32] as char);
3930 }
3931 s
3932 };
3933 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
3934 crate::db::set_app_data_dir(tmp.path().to_path_buf());
3935 crate::db::set_current_account(acct.clone()).unwrap();
3936 crate::db::init_database(&acct).unwrap();
3937 let _ = crate::state::take_nostr_client();
3938 let me = Keys::generate();
3939 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
3940 crate::state::set_my_public_key(me.public_key());
3941
3942 let relay = MemoryRelay::new();
3944 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
3945 .await
3946 .unwrap();
3947 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
3948
3949 let listed = VectorCore.list_communities().await;
3951 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
3952 assert_eq!(v2["name"], "V2 Guild");
3953 assert_eq!(v2["is_owner"], true);
3954 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
3955
3956 assert_eq!(
3958 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
3959 Some(community.identity.community_id),
3960 "a v2 channel is routed to v2"
3961 );
3962 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
3964 }
3965
3966 #[test]
3971 fn v2_invite_url_base_derivation_round_trips() {
3972 use crate::community::v2::derive::TOKEN_LEN;
3973 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
3974 use nostr_sdk::prelude::Keys;
3975 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
3976 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
3977 let signer = Keys::generate();
3978 let token = [0x07u8; TOKEN_LEN];
3979 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
3980 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
3981 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
3982 let parsed = parse_invite_link(&url).unwrap();
3983 assert_eq!(parsed.link_signer, signer.public_key());
3984 assert_eq!(parsed.token, token);
3985 }
3986}