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 "dissolved": c.dissolved,
1085 "channels": c.channels.iter()
1091 .map(|ch| serde_json::json!({
1092 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1093 "name": ch.name,
1094 "private": ch.private,
1095 "readable": !(ch.private && ch.key.is_none()),
1096 "epoch": ch.epoch.0,
1097 }))
1098 .collect::<Vec<_>>(),
1099 }));
1100 }
1101 }
1102 _ => {
1103 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1104 out.push(serde_json::json!({
1105 "community_id": c.id.to_hex(),
1106 "version": 1,
1107 "name": c.name,
1108 "description": c.description,
1109 "is_owner": crate::community::service::is_proven_owner(&c),
1110 "dissolved": c.dissolved,
1111 "channels": c.channels.iter()
1112 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1113 .collect::<Vec<_>>(),
1114 }));
1115 }
1116 }
1117 }
1118 }
1119 out
1120 }
1121
1122 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1127 use crate::community::{v2::service as v2, transport::LiveTransport};
1128 let relays: Vec<String> = crate::state::active_trusted_relays()
1129 .await
1130 .iter()
1131 .map(|s| s.to_string())
1132 .collect();
1133 if relays.is_empty() {
1134 return Err(VectorError::Other("no relays available to host the Community".into()));
1135 }
1136 let session = state::SessionGuard::capture();
1137 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1138 let community = v2::create_community(&transport, name, relays, None)
1139 .await
1140 .map_err(VectorError::Other)?;
1141 self.register_v2_chats(&community, &session).await;
1142 if let Some(client) = state::nostr_client() {
1144 crate::community::v2::realtime::refresh_subscription(&client).await;
1145 }
1146 Ok(Self::v2_summary(&community))
1147 }
1148
1149 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1155 use crate::community::ConcordProtocol;
1156 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1157 return Ok(None);
1158 };
1159 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1160 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1161 Some(ConcordProtocol::V2) => Some(cid),
1162 _ => None,
1163 })
1164 }
1165
1166 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1168 let me = state::my_public_key();
1169 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1170 serde_json::json!({
1171 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1172 "version": 2,
1173 "name": community.name,
1174 "description": community.description,
1175 "is_owner": is_owner,
1176 "channels": community.channels.iter()
1177 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1178 .collect::<Vec<_>>(),
1179 })
1180 }
1181
1182 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1188 register_v2_chats_inner(community, session).await
1189 }
1190}
1191
1192pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1195 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1196 let me = state::my_public_key();
1197 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1198 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1199 let Some(primary) = community.primary_channel() else { return };
1202 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1203 let slims = {
1208 let mut st = state::STATE.lock().await;
1209 if !session.is_valid() {
1210 return; }
1212 let mut slims = Vec::new();
1213 for ch in &community.channels {
1214 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1215 st.upsert_community_chat(
1216 &ch_hex,
1217 &community.name,
1218 community.description.as_deref().unwrap_or(""),
1219 &id_hex,
1220 is_owner,
1221 community.icon.is_some(),
1222 owner_npub.as_deref(),
1223 Some(community.created_at_ms),
1224 community.dissolved,
1225 crate::community::ConcordProtocol::V2,
1226 &ch.name,
1227 &primary_hex,
1228 );
1229 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1230 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1231 }
1232 }
1233 slims
1234 };
1235 if !session.is_valid() {
1239 return;
1240 }
1241 for slim in &slims {
1242 let _ = crate::db::chats::save_slim_chat(slim);
1243 }
1244}
1245
1246impl VectorCore {
1247 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1251 use crate::community::{public_invite, service, transport::LiveTransport};
1252 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1256 let session = state::SessionGuard::capture();
1257 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1258 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1259 .await
1260 .map_err(VectorError::Other)?;
1261 self.register_v2_chats(&community, &session).await;
1262 if let Some(client) = state::nostr_client() {
1263 crate::community::v2::realtime::refresh_subscription(&client).await;
1264 }
1265 if crate::community::v2::realtime::follow_worker_running() {
1270 crate::community::v2::realtime::enqueue_follow(community.id());
1271 } else {
1272 let seed_session = state::SessionGuard::capture();
1273 let seed_community = community.clone();
1274 tokio::spawn(async move {
1275 if !seed_session.is_valid() {
1276 return;
1277 }
1278 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1279 if matches!(
1280 crate::community::v2::service::sync_guestbook(&transport, &seed_community, &seed_session).await,
1281 Ok(fresh) if !fresh.is_empty()
1282 ) {
1283 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1284 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1285 }
1286 });
1287 }
1288 return Ok(Self::v2_summary(&community));
1289 }
1290 let (relays, token) = public_invite::parse_invite_url(invite_url)
1291 .map_err(|e| VectorError::Other(e.to_string()))?;
1292 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1293 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1294 .await
1295 .map_err(VectorError::Other)?;
1296 let now = std::time::SystemTime::now()
1297 .duration_since(std::time::UNIX_EPOCH)
1298 .map(|d| d.as_secs())
1299 .unwrap_or(0);
1300 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1303 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1304 .await
1305 .map_err(VectorError::Other)?;
1306 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1307 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1310 self.finalize_member_join(community, &transport, attribution).await
1311 }
1312
1313 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1316 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1317 Ok(rows.iter().map(|p| {
1318 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1321 serde_json::json!({
1322 "community_id": p.community_id,
1323 "name": v2.name,
1324 "inviter_npub": p.inviter_npub,
1325 "version": 2,
1326 })
1327 } else {
1328 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1329 .ok().map(|i| i.name).unwrap_or_default();
1330 serde_json::json!({
1331 "community_id": p.community_id,
1332 "name": name,
1333 "inviter_npub": p.inviter_npub,
1334 "version": 1,
1335 })
1336 }
1337 }).collect())
1338 }
1339
1340 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1344 use crate::community::transport::LiveTransport;
1345 let bundle_json = crate::db::community::get_pending_invite(community_id)
1346 .map_err(VectorError::Other)?
1347 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1348 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1349
1350 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1352 let session = state::SessionGuard::capture();
1353 let inviter = crate::db::community::list_pending_invites()
1355 .ok()
1356 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1357 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
1365 .await
1366 .map_err(VectorError::Other)?;
1367 if !session.is_valid() {
1368 return Err(VectorError::Other("account changed during join".into()));
1369 }
1370 self.register_v2_chats(&community, &session).await;
1371 if let Some(client) = state::nostr_client() {
1372 crate::community::v2::realtime::refresh_subscription(&client).await;
1373 }
1374 crate::community::v2::realtime::enqueue_follow(community.id());
1375 let _ = crate::db::community::delete_pending_invite(community_id);
1376 return Ok(Self::v2_summary(&community));
1377 }
1378
1379 use crate::community::invite::{accept_invite, CommunityInvite};
1381 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1382 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1383 let now = std::time::SystemTime::now()
1387 .duration_since(std::time::UNIX_EPOCH)
1388 .map(|d| d.as_secs())
1389 .unwrap_or(0);
1390 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1391 .await
1392 .map_err(VectorError::Other)?;
1393 let summary = self.finalize_member_join(community, &transport, None).await?;
1395 let _ = crate::db::community::delete_pending_invite(community_id);
1396 Ok(summary)
1397 }
1398
1399 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1404 &self,
1405 community: crate::community::Community,
1406 transport: &T,
1407 attribution: Option<(String, Option<String>)>,
1408 ) -> Result<serde_json::Value> {
1409 use crate::community::service;
1410 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1417 return Ok(serde_json::json!({
1418 "community_id": v2,
1419 "version": 2,
1420 "migrated": true,
1421 }));
1422 }
1423 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1427 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1430 if c.removed {
1431 let _ = crate::db::community::delete_community(&community.id.to_hex());
1432 return Err(VectorError::Other("you have been removed from this community".into()));
1433 }
1434 }
1435 let community = crate::db::community::load_community(&community.id)
1436 .map_err(VectorError::Other)?
1437 .unwrap_or(community);
1438 let _ = service::fetch_and_apply_control(transport, &community).await;
1442 if service::am_i_banned(&community) {
1443 let _ = crate::db::community::delete_community(&community.id.to_hex());
1444 return Err(VectorError::Other("you are banned from this community".into()));
1445 }
1446 let community = crate::db::community::load_community(&community.id)
1448 .map_err(VectorError::Other)?
1449 .unwrap_or(community);
1450 let owner_npub = community
1451 .owner_attestation
1452 .as_ref()
1453 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1454 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1455 {
1456 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1457 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1458 let mut st = state::STATE.lock().await;
1459 for ch in &community.channels {
1460 st.upsert_community_chat(
1461 &ch.id.to_hex(),
1462 &community.name,
1463 community.description.as_deref().unwrap_or(""),
1464 &community.id.to_hex(),
1465 crate::community::service::is_proven_owner(&community),
1466 community.icon.is_some(),
1467 owner_npub.as_deref(),
1468 created_at_ms,
1469 community.dissolved,
1470 crate::community::ConcordProtocol::V1,
1471 &ch.name,
1472 &primary_hex,
1473 );
1474 }
1475 }
1476 if let Some(primary) = community.channels.first() {
1479 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1480 }
1481 Ok(serde_json::json!({
1482 "community_id": community.id.to_hex(),
1483 "version": 1,
1484 "name": community.name,
1485 "channels": community.channels.iter()
1486 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1487 .collect::<Vec<_>>(),
1488 }))
1489 }
1490
1491
1492 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1496 use crate::community::CommunityId;
1497 if community_id.len() != 64 {
1498 return Err(VectorError::Other("malformed community id".into()));
1499 }
1500 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1501 match crate::db::community::community_protocol(&cid).ok().flatten() {
1502 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1503 .map_err(VectorError::Other)?
1504 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1505 Some(_) => Err(VectorError::Other(
1506 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1507 )),
1508 None => Err(VectorError::Other("community not found".into())),
1509 }
1510 }
1511
1512 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1513 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1514 .map(crate::community::ChannelId)
1515 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1516 }
1517
1518 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1523 use crate::community::{v2::service, transport::LiveTransport};
1524 let community = Self::v2_community(community_id)?;
1525 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1526 let id = if private {
1527 service::create_private_channel(&transport, &community, name).await
1528 } else {
1529 service::create_public_channel(&transport, &community, name).await
1530 }
1531 .map_err(VectorError::Other)?;
1532 if let Some(client) = state::nostr_client() {
1535 crate::community::v2::realtime::refresh_subscription(&client).await;
1536 }
1537 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1538 }
1539
1540 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &str) -> Result<()> {
1543 use crate::community::{v2::service, transport::LiveTransport};
1544 let community = Self::v2_community(community_id)?;
1545 let id = Self::channel_id_of(channel_id)?;
1546 let mut meta = community
1547 .channel(&id)
1548 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1549 .metadata();
1550 meta.name = name.to_string();
1551 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1552 service::edit_channel_metadata(&transport, &community, &id, &meta)
1553 .await
1554 .map_err(VectorError::Other)
1555 }
1556
1557 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1560 use crate::community::{v2::service, transport::LiveTransport};
1561 let community = Self::v2_community(community_id)?;
1562 let id = Self::channel_id_of(channel_id)?;
1563 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1564 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1565 service::delete_channel(&transport, &community, &id, &name)
1566 .await
1567 .map_err(VectorError::Other)
1568 }
1569
1570 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1573 use crate::community::{v2::service, transport::LiveTransport};
1574 let community = Self::v2_community(community_id)?;
1575 let id = Self::channel_id_of(channel_id)?;
1576 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1577 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1578 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1579 service::grant_channel_access(&transport, &community, &id, &member)
1580 .await
1581 .map_err(VectorError::Other)
1582 }
1583
1584 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1588 use crate::community::{v2::service, transport::LiveTransport};
1589 let community = Self::v2_community(community_id)?;
1590 let id = Self::channel_id_of(channel_id)?;
1591 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1592 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1593 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1594 service::revoke_channel_access(&transport, &community, &id, &member)
1595 .await
1596 .map_err(VectorError::Other)
1597 }
1598
1599 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1607 use nostr_sdk::prelude::{PublicKey, ToBech32};
1608 let community = Self::v2_community(community_id)?;
1609 let id = Self::channel_id_of(channel_id)?;
1610 let ch = community
1611 .channel(&id)
1612 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1613 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1616 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1617 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1618 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1619 let access_ids = roster.channel_role_ids(&chan_hex);
1620 let roles: Vec<serde_json::Value> = roster
1621 .channel_roles(&chan_hex)
1622 .into_iter()
1623 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1624 .collect();
1625 let members: Vec<String> = roster
1626 .grants
1627 .iter()
1628 .filter(|g| !banned.contains(&g.member))
1629 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1630 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1631 .collect();
1632 Ok(serde_json::json!({
1633 "channel_id": chan_hex,
1634 "private": ch.private,
1635 "readable": !(ch.private && ch.key.is_none()),
1636 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1637 "roles": roles,
1638 "members": members,
1639 }))
1640 }
1641
1642 pub async fn create_public_invite(
1647 &self,
1648 community_id: &str,
1649 expires_at_ms: Option<u64>,
1650 label: Option<String>,
1651 ) -> Result<String> {
1652 use crate::community::{service, transport::LiveTransport, CommunityId};
1653 if community_id.len() != 64 {
1654 return Err(VectorError::Other("malformed community id".into()));
1655 }
1656 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1657 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1659 crate::db::community::community_protocol(&cid).ok()
1660 {
1661 let community = crate::db::community::load_community_v2(&cid)
1662 .map_err(VectorError::Other)?
1663 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1664 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1665 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1668 let minted =
1669 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1670 .await
1671 .map_err(VectorError::Other)?;
1672 return Ok(minted.url);
1673 }
1674 let community = crate::db::community::load_community(&CommunityId(
1675 crate::simd::hex::hex_to_bytes_32(community_id),
1676 ))
1677 .map_err(VectorError::Other)?
1678 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1679 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1680 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1681 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1682 .await
1683 .map_err(VectorError::Other)?;
1684 Ok(url)
1685 }
1686
1687 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1691 use crate::community::{service, CommunityId};
1692 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1693
1694 let session = crate::state::SessionGuard::capture();
1695 let my_pk = crate::state::my_public_key()
1696 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1697
1698 if community_id.len() != 64 {
1699 return Err(VectorError::Other("malformed community id".into()));
1700 }
1701 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1702 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1708 crate::db::community::community_protocol(&cid).ok()
1709 {
1710 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1711 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1712 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1713 let bundle = {
1721 let lock = crate::community::v2::realtime::follow_lock(&cid);
1722 let _rotation = lock.lock().await;
1723 let community = crate::db::community::load_community_v2(&cid)
1724 .map_err(VectorError::Other)?
1725 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1726 crate::community::v2::service::bundle_of(
1727 &community,
1728 crate::community::v2::service::BundleAudience::Member(recipient),
1729 Some(my_pk),
1730 None,
1731 None,
1732 )
1733 };
1734 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1735 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1738 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1739 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1740 let rumor = nostr_sdk::prelude::EventBuilder::new(
1741 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1742 bundle_json,
1743 )
1744 .tag(expiry_tag.clone())
1745 .finalize_unsigned_with_id(my_pk);
1746 let k_tag = nostr_sdk::prelude::Tag::custom(
1747 "k",
1748 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1749 );
1750 if !session.is_valid() {
1751 return Err(VectorError::Other("account changed".into()));
1752 }
1753 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1754 .await
1755 .map_err(VectorError::Other)?;
1756 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1757 }
1758 let community = crate::db::community::load_community(&CommunityId(
1759 crate::simd::hex::hex_to_bytes_32(community_id),
1760 ))
1761 .map_err(VectorError::Other)?
1762 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1763
1764 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1765 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1766 }
1767 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1768 .map_err(|_| VectorError::Other("invalid npub".into()))?
1769 .to_hex();
1770 if crate::db::community::get_community_banlist(community_id)
1771 .map_err(VectorError::Other)?
1772 .iter()
1773 .any(|b| b == &invitee_hex)
1774 {
1775 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1776 }
1777
1778 if !session.is_valid() {
1780 return Err(VectorError::Other("account changed during invite".into()));
1781 }
1782
1783 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1784 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1785 .map_err(VectorError::Other)?;
1786 let pending_id = format!("community-invite-{}", community_id);
1787 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1789 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1790
1791 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1792 .await
1793 .map_err(VectorError::Other)?;
1794
1795 Ok(serde_json::json!({
1796 "community_id": community_id,
1797 "invitee": invitee_npub,
1798 "wrap_event_id": result.event_id,
1799 }))
1800 }
1801
1802 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1807 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1808 }
1809
1810 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1815 use crate::community::{service, transport::LiveTransport, CommunityId};
1816 if community_id.len() != 64 {
1817 return Err(VectorError::Other("malformed community id".into()));
1818 }
1819 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1820 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1821 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1824 let community = crate::db::community::load_community_v2(&cid)
1825 .map_err(VectorError::Other)?
1826 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1827 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1828 .await
1829 .map_err(VectorError::Other);
1830 }
1831 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1832 let community = crate::db::community::load_community(&cid)
1833 .map_err(VectorError::Other)?
1834 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1835 service::revoke_public_invite(&transport, &community, &token_bytes)
1836 .await
1837 .map_err(VectorError::Other)
1838 }
1839
1840 pub async fn send_community_message(
1842 &self,
1843 channel_id: &str,
1844 content: &str,
1845 replied_to: Option<&str>,
1846 ) -> Result<String> {
1847 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1848 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1850 let community = crate::db::community::load_community_v2(&id)
1851 .map_err(VectorError::Other)?
1852 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1853 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1854 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1855 let reply = match replied_to.filter(|r| !r.is_empty()) {
1858 Some(parent_id) => {
1859 let author_hex = {
1860 let st = state::STATE.lock().await;
1861 st.find_message(parent_id)
1862 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1863 .map(|pk| pk.to_hex())
1864 .unwrap_or_default()
1865 };
1866 Some((parent_id.to_string(), author_hex))
1867 }
1868 None => None,
1869 };
1870 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1871 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1874 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1875 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1876 .await
1877 .map_err(VectorError::Other);
1878 }
1879 let (community, channel) = self.resolve_channel(channel_id)?;
1880 Self::ensure_v1_writable(&community)?;
1881 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1882 let reply = replied_to.filter(|r| !r.is_empty());
1883 let ms = std::time::SystemTime::now()
1884 .duration_since(std::time::UNIX_EPOCH)
1885 .map(|d| d.as_millis() as u64)
1886 .unwrap_or(0);
1887 let unsigned = envelope::build_inner_typed(
1888 author_pk,
1889 &channel.id,
1890 channel.epoch,
1891 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1892 content,
1893 ms,
1894 reply,
1895 &[],
1896 );
1897 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1898 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1899 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1900 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1901 let session = state::SessionGuard::capture();
1902 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1903 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1904 .await
1905 .map_err(VectorError::Other)?;
1906 if !session.is_valid() {
1909 return Ok(message_id);
1910 }
1911 let echoed = {
1912 let mut st = state::STATE.lock().await;
1913 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1914 };
1915 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1916 let _ = crate::db::events::save_message(channel_id, &msg).await;
1917 }
1918 Ok(message_id)
1919 }
1920
1921 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1925 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1926 let path = std::path::Path::new(file_path);
1927 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1928 if bytes.is_empty() {
1929 return Err(VectorError::Other("Empty file".into()));
1930 }
1931 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1932 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1933
1934 let session = state::SessionGuard::capture();
1937 let v2_target = match self.v2_community_for_channel(channel_id)? {
1940 Some(id) => Some(
1941 crate::db::community::load_community_v2(&id)
1942 .map_err(VectorError::Other)?
1943 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1944 ),
1945 None => None,
1946 };
1947 let v1_target = match v2_target {
1948 Some(_) => None,
1949 None => Some(self.resolve_channel(channel_id)?),
1950 };
1951 match (&v2_target, &v1_target) {
1956 (Some(c), _) => {
1957 let cid = crate::simd::hex::bytes_to_hex_32(&c.id().0);
1958 if crate::db::community::get_community_dissolved(&cid).unwrap_or(false) {
1959 return Err(VectorError::Other("this community has been dissolved".into()));
1960 }
1961 }
1962 (None, Some((c, _))) => Self::ensure_v1_writable(c)?,
1963 _ => {}
1964 }
1965 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1966
1967 let file_hash = crate::crypto::sha256_hex(&bytes);
1968 let mime = crate::crypto::mime_from_extension(&extension);
1969 let img_meta = crate::crypto::generate_image_metadata(&bytes);
1970
1971 let download_dir = crate::db::get_download_dir();
1973 let _ = std::fs::create_dir_all(&download_dir);
1974 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
1975 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
1976 let _ = std::fs::write(&local_path, &bytes);
1977
1978 let params = crate::crypto::generate_encryption_params();
1980 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
1981 let encrypted_size = encrypted.len() as u64;
1982
1983 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1984 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1985 let servers = crate::blossom_servers::compute_enabled_servers();
1986 if servers.is_empty() {
1987 return Err(VectorError::Other("No Blossom servers configured".into()));
1988 }
1989 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
1990 let url = crate::blossom::upload_blob_with_progress_and_failover(
1991 signer.clone(),
1992 servers,
1993 std::sync::Arc::new(encrypted),
1994 Some(mime),
1995 true,
1996 noop_progress,
1997 Some(3),
1998 Some(std::time::Duration::from_secs(2)),
1999 None,
2000 ).await.map_err(VectorError::Other)?;
2001
2002 let attachment = crate::types::Attachment {
2003 id: file_hash.clone(),
2004 key: params.key.clone(),
2005 nonce: params.nonce.clone(),
2006 extension: extension.clone(),
2007 name: filename.clone(),
2008 url,
2009 path: local_path.to_string_lossy().to_string(),
2010 size: encrypted_size,
2011 img_meta,
2012 downloading: false,
2013 downloaded: true,
2014 ..Default::default()
2015 };
2016 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
2017
2018 if !session.is_valid() {
2020 return Err(VectorError::Other("account changed during upload".into()));
2021 }
2022 if let Some(community) = v2_target {
2024 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2025 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2026 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2027 .await
2028 .map_err(VectorError::Other);
2029 }
2030 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2031 let ms = std::time::SystemTime::now()
2032 .duration_since(std::time::UNIX_EPOCH)
2033 .map(|d| d.as_millis() as u64)
2034 .unwrap_or(0);
2035 let unsigned = envelope::build_inner_full(
2036 author_pk, &channel.id, channel.epoch,
2037 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2038 );
2039 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2040 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2041 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2042 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2043 .await.map_err(VectorError::Other)?;
2044 let echoed = {
2046 let mut st = state::STATE.lock().await;
2047 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2048 };
2049 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2050 let _ = crate::db::events::save_message(channel_id, &m).await;
2051 }
2052 Ok(message_id)
2053 }
2054
2055 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2057 use crate::community::{service, transport::LiveTransport};
2058 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2059 let community = crate::db::community::load_community_v2(&id)
2060 .map_err(VectorError::Other)?
2061 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2062 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2063 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2064 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2065 .await
2066 .map_err(VectorError::Other);
2067 }
2068 let (community, channel) = self.resolve_channel(channel_id)?;
2069 Self::ensure_v1_writable(&community)?;
2070 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2071 service::publish_typing_signal(&transport, &community, &channel)
2072 .await
2073 .map_err(VectorError::Other)
2074 }
2075
2076 pub async fn send_community_reaction(
2079 &self,
2080 channel_id: &str,
2081 message_id: &str,
2082 emoji: &str,
2083 emoji_url: Option<&str>,
2084 ) -> Result<()> {
2085 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2086 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2087 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2088 }
2089 _ => Vec::new(),
2090 };
2091 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2092 let session = state::SessionGuard::capture();
2093 let community = crate::db::community::load_community_v2(&id)
2094 .map_err(VectorError::Other)?
2095 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2096 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2097 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2098 let held = {
2103 let st = state::STATE.lock().await;
2104 st.find_message(message_id)
2105 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2106 };
2107 let held = held.or_else(|| {
2108 crate::db::events::event_author(message_id)
2109 .ok()
2110 .flatten()
2111 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2112 });
2113 let target_author = match held {
2114 Some(pk) => pk,
2115 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2116 .await
2117 .map_err(VectorError::Other)?
2118 .iter()
2119 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2120 .map(|f| f.event.opened().author)
2121 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2122 };
2123 if !session.is_valid() {
2125 return Err(VectorError::Other("account changed before send".into()));
2126 }
2127 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2128 return crate::community::v2::service::send_reaction(
2133 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2134 )
2135 .await
2136 .map(|_| ())
2137 .map_err(VectorError::Other);
2138 }
2139 self.publish_community_control(
2140 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2141 ).await
2142 }
2143
2144 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2146 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2147 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2148 let community = crate::db::community::load_community_v2(&id)
2149 .map_err(VectorError::Other)?
2150 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2151 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2152 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2153 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2154 .await
2155 .map(|_| ())
2156 .map_err(VectorError::Other);
2157 }
2158 self.publish_community_control(
2159 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2160 ).await
2161 }
2162
2163 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2167 let channel_id = {
2168 let st = state::STATE.lock().await;
2169 match st.find_message(message_id) {
2170 Some((chat, _)) => chat.id.clone(),
2171 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2172 }
2173 };
2174 self.delete_community_message_in(&channel_id, message_id).await
2175 }
2176
2177 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2181 use crate::community::{service, transport::LiveTransport};
2182 let session = state::SessionGuard::capture();
2183 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2184
2185 let attachment_urls: Vec<String> = {
2188 let st = state::STATE.lock().await;
2189 st.find_message(message_id)
2190 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2191 .unwrap_or_default()
2192 };
2193
2194 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2195 let community = crate::db::community::load_community_v2(&id)
2198 .map_err(VectorError::Other)?
2199 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2200 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2201 crate::community::v2::service::send_delete(
2202 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2203 )
2204 .await
2205 .map_err(VectorError::Other)?;
2206 } else {
2207 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2209 let _ = service::delete_message(&transport, message_id).await;
2210 }
2211 self.publish_community_control(
2213 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2214 ).await?;
2215 }
2216 if !attachment_urls.is_empty() {
2218 if let Some(_client) = state::nostr_client() {
2219 if let Ok(signer) = crate::signer::active_signer() {
2220 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2221 }
2222 }
2223 }
2224 if !session.is_valid() {
2227 return Ok(());
2228 }
2229 let removed_chat = {
2230 let mut st = state::STATE.lock().await;
2231 st.remove_message(message_id).map(|(cid, _)| cid)
2232 };
2233 let _ = crate::db::events::delete_event(message_id).await;
2234 traits::emit_event_json("message_removed", serde_json::json!({
2235 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2236 }));
2237 Ok(())
2238 }
2239
2240 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2246 use crate::community::transport::LiveTransport;
2247 let session = state::SessionGuard::capture();
2248 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2249
2250 let author_npub = {
2253 let st = state::STATE.lock().await;
2254 st.find_message(message_id).and_then(|(_, m)| m.npub)
2255 };
2256 let author_npub = match author_npub {
2257 Some(n) => n,
2258 None => crate::db::events::event_author(message_id)
2259 .ok()
2260 .flatten()
2261 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2262 };
2263 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2264 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2265
2266 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2267 let community = crate::db::community::load_community_v2(&id)
2268 .map_err(VectorError::Other)?
2269 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2270 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2271 crate::community::v2::service::moderation_delete(
2272 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2273 )
2274 .await
2275 .map_err(VectorError::Other)?;
2276 } else {
2277 let cid = crate::db::community::community_id_for_channel(channel_id)
2278 .map_err(VectorError::Other)?
2279 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2280 let community = crate::db::community::load_community(&crate::community::CommunityId(
2281 crate::simd::hex::hex_to_bytes_32(&cid),
2282 ))
2283 .map_err(VectorError::Other)?
2284 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2285 let channel = community
2286 .channels
2287 .iter()
2288 .find(|c| c.id.to_hex() == channel_id)
2289 .cloned()
2290 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2291 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2292 .await
2293 .map_err(VectorError::Other)?;
2294 }
2295
2296 if !session.is_valid() {
2299 return Ok(());
2300 }
2301 let removed_chat = {
2302 let mut st = state::STATE.lock().await;
2303 st.remove_message(message_id).map(|(cid, _)| cid)
2304 };
2305 let _ = crate::db::events::delete_event(message_id).await;
2306 traits::emit_event_json("message_removed", serde_json::json!({
2307 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2308 }));
2309 Ok(())
2310 }
2311
2312 async fn publish_community_control(
2315 &self,
2316 channel_id: &str,
2317 kind: u16,
2318 content: &str,
2319 target: &str,
2320 emoji_tags: &[crate::types::EmojiTag],
2321 ) -> Result<()> {
2322 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2323 let (community, channel) = self.resolve_channel(channel_id)?;
2324 Self::ensure_v1_writable(&community)?;
2325 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2326 let ms = std::time::SystemTime::now()
2327 .duration_since(std::time::UNIX_EPOCH)
2328 .map(|d| d.as_millis() as u64)
2329 .unwrap_or(0);
2330 let unsigned = envelope::build_inner_typed(
2331 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2332 );
2333 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2334 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2335 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2336 let session = state::SessionGuard::capture();
2337 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2338 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2339 .await.map_err(VectorError::Other)?;
2340 if !session.is_valid() {
2343 return Ok(());
2344 }
2345 let outcome = {
2346 let mut st = state::STATE.lock().await;
2347 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2348 };
2349 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2350 if let Some(ev) = edit_event {
2351 let mut ev = (*ev).clone();
2352 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2353 let _ = crate::db::events::save_event(&ev).await;
2354 } else {
2355 let _ = crate::db::events::save_message(channel_id, &message).await;
2356 }
2357 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2358 }
2359 Ok(())
2360 }
2361
2362 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2370 use crate::community::{inbound, send, service, transport::LiveTransport};
2371 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2372 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2377 let warnings = if community::v2::realtime::follow_worker_running() {
2378 community::v2::realtime::enqueue_follow(&id);
2379 Vec::new()
2380 } else {
2381 Self::v2_inline_follow(&id).await
2382 };
2383 let new = Self::v2_backfill_channel(
2388 &id, channel_id, limit, 8, None,
2389 crate::community::transport::Evidence::Fast, 12,
2390 ).await;
2391 return Ok((new, warnings));
2392 }
2393 let (community, _) = self.resolve_channel(channel_id)?;
2394 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2395 let mut warnings: Vec<String> = Vec::new();
2396
2397 match service::catch_up_server_root(&transport, &community).await {
2405 Ok(c) if c.removed => {
2406 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2408 return Ok((0, warnings));
2409 }
2410 Ok(_) => {}
2411 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2412 }
2413 let (community, _) = self.resolve_channel(channel_id)?;
2414
2415 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2421 warnings.push(format!("control fold failed: {e}"));
2422 }
2423 if service::am_i_banned(&community) {
2424 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2426 return Ok((0, warnings));
2427 }
2428 let (community, channel) = self.resolve_channel(channel_id)?;
2431 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2432 warnings.push(format!("channel catch-up failed: {e}"));
2433 }
2434 let (community, _) = self.resolve_channel(channel_id)?;
2438 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2439 warnings.push(format!("read-cut resume failed: {e}"));
2440 }
2441 let (community, channel) = self.resolve_channel(channel_id)?;
2442
2443 let session = state::SessionGuard::capture();
2445 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2446 .await
2447 .map_err(VectorError::Other)?;
2448 let outcomes = {
2449 let mut st = state::STATE.lock().await;
2450 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2451 };
2452 let mut new = 0usize;
2453 let mut pending: Vec<&crate::types::Message> = Vec::new();
2457 for o in &outcomes {
2458 if !session.is_valid() {
2460 pending.clear();
2461 break;
2462 }
2463 match o {
2464 inbound::IncomingEvent::NewMessage(m) => {
2465 pending.push(m);
2466 new += 1;
2467 }
2468 inbound::IncomingEvent::Updated { message, .. } => {
2469 pending.push(message);
2470 }
2471 inbound::IncomingEvent::Removed { target_id } => {
2472 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2473 let _ = crate::db::events::delete_event(target_id).await;
2474 }
2475 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2476 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2479 let _ = crate::db::events::delete_event(reaction_id).await;
2480 }
2481 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2482 let et = if *joined {
2483 crate::stored_event::SystemEventType::MemberJoined
2484 } else {
2485 crate::stored_event::SystemEventType::MemberLeft
2486 };
2487 let note = invited_by.as_ref().map(|by| match invited_label {
2489 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2490 _ => by.clone(),
2491 });
2492 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;
2493 }
2494 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2495 community::service::persist_webxdc_signal(
2498 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2499 ).await;
2500 }
2501 inbound::IncomingEvent::Kicked { community_id }
2502 | inbound::IncomingEvent::SelfLeft { community_id } => {
2503 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2508 let _ = crate::db::community::delete_community_retain_keys(community_id);
2509 break;
2510 }
2511 inbound::IncomingEvent::Typing { .. } => {
2512 }
2514 }
2515 }
2516 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2517 Ok((new, warnings))
2518 }
2519
2520 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2532 use crate::bot_interface::{self, ChatCommandsSnapshot};
2533 use nostr_sdk::prelude::ToBech32;
2534
2535 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2536 let mut relays: Vec<String> = Vec::new();
2537 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2538 if let Some(cid_hex) = community_hex {
2539 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2540 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2541 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2542 relays = community.relays.clone();
2543 } else {
2544 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2545 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2546 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2547 };
2548 relays = community.relays.clone();
2549 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2550 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2551 members.push(pk);
2552 }
2553 }
2554 }
2555 let state = crate::state::STATE.lock().await;
2556 for pk in members {
2557 let Ok(npub) = pk.to_bech32();
2558 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2559 bots.push(pk);
2560 }
2561 }
2562 } else if chat_id.starts_with("npub1") {
2563 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2564 let is_bot = {
2565 let state = crate::state::STATE.lock().await;
2566 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2567 };
2568 if is_bot {
2569 bots.push(pk);
2570 if let Some(client) = crate::state::nostr_client() {
2573 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2574 }
2575 }
2576 }
2577 }
2578
2579 if bots.is_empty() {
2580 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2581 }
2582 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2585 relays.sort();
2586 relays.dedup();
2587 bots.sort_by_key(|p| p.to_hex());
2590 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2591 let commands = bot_interface::assemble_from_store(&bot_hexes);
2592 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2593 if !fresh {
2594 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2595 }
2596 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2597 }
2598
2599 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2604 use nostr_sdk::prelude::ToBech32;
2605 match Self::load_v2_if_v2(community_id) {
2611 Ok(Some(community)) => {
2612 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2613 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2614 if cursor == 0 {
2615 if crate::community::v2::realtime::follow_worker_running() {
2616 crate::community::v2::realtime::enqueue_follow(community.id());
2617 } else {
2618 let session = state::SessionGuard::capture();
2619 let c2 = community.clone();
2620 tokio::spawn(async move {
2621 if !session.is_valid() {
2622 return;
2623 }
2624 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2625 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2, &session).await, Ok(fresh) if !fresh.is_empty()) {
2626 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2627 }
2628 });
2629 }
2630 }
2631 return crate::community::v2::service::stored_memberlist(&community)
2632 .unwrap_or_default()
2633 .into_iter()
2634 .filter_map(|pk| pk.to_bech32().ok())
2635 .map(|npub| serde_json::json!({ "npub": npub }))
2636 .collect();
2637 }
2638 Ok(None) => {} Err(_) => return Vec::new(),
2641 }
2642 crate::db::community::community_member_activity(community_id)
2643 .unwrap_or_default()
2644 .into_iter()
2645 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2646 .collect()
2647 }
2648
2649 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2653 use crate::community::transport::LiveTransport;
2654 let session = state::SessionGuard::capture();
2655 let lock = crate::community::v2::realtime::follow_lock(id);
2660 let _guard = lock.lock().await;
2661 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2662 let mut warnings: Vec<String> = Vec::new();
2663 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2664 warnings.push("v2 community not found".to_string());
2665 return warnings;
2666 };
2667 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2668 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2669 Ok(f) if f.dissolved => return warnings,
2671 Ok(f) if f.self_removed => {
2672 if session.is_valid() {
2675 let _ = crate::db::community::delete_community(&cid_hex);
2676 }
2677 return warnings;
2678 }
2679 Ok(_) => {}
2680 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2681 }
2682 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2683 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
2684 Ok(Some(changed)) => {
2688 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2689 warnings.push(format!("v2 rekey follow failed: {e}"));
2690 }
2691 }
2692 Ok(None) => {}
2693 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2694 }
2695 }
2696 if let Some(me) = crate::my_public_key() {
2701 if crate::db::community::is_author_banned(&cid_hex, &me) && session.is_valid() {
2702 let _ = crate::db::community::delete_community(&cid_hex);
2703 }
2704 }
2705 warnings
2706 }
2707
2708 pub(crate) async fn v2_backfill_channel(
2720 id: &crate::community::CommunityId,
2721 channel_id: &str,
2722 limit: usize,
2723 max_pages: usize,
2724 since: Option<u64>,
2725 evidence: crate::community::transport::Evidence,
2726 transport_secs: u64,
2727 ) -> usize {
2728 let session = state::SessionGuard::capture();
2731 let Some(my_pk) = state::my_public_key() else { return 0 };
2732 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2736 return 0;
2737 }
2738 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
2739 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2740 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
2741 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2742 &transport,
2743 &community,
2744 &ch,
2745 limit.max(50),
2746 max_pages,
2747 since,
2748 evidence,
2749 |page| {
2754 let mut saw_message = false;
2755 for f in page {
2756 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2757 saw_message = true;
2758 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2759 return true;
2760 }
2761 }
2762 }
2763 !saw_message
2764 },
2765 )
2766 .await
2767 else {
2768 return 0;
2769 };
2770 Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await
2771 }
2772
2773 pub(crate) async fn v2_ingest_chat_page(
2777 channel_id: &str,
2778 my_pk: nostr_sdk::prelude::PublicKey,
2779 session: crate::state::SessionGuard,
2780 page: Vec<crate::community::v2::service::FetchedEvent>,
2781 ) -> usize {
2782 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2783 let mut new = 0usize;
2784 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
2786 for f in &page {
2787 if !session.is_valid() {
2789 break;
2790 }
2791 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2796 if opened.author != my_pk {
2797 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2798 let Ok(npub) = ToBech32::to_bech32(&opened.author);
2799 crate::community::service::persist_webxdc_signal(
2800 channel_id,
2801 &npub,
2802 &topic,
2803 addr.as_deref(),
2804 &opened.rumor_id.to_hex(),
2805 opened.at_ms / 1000,
2806 )
2807 .await;
2808 }
2809 }
2810 continue;
2811 }
2812 let outcome = {
2813 let mut st = state::STATE.lock().await;
2814 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2815 };
2816 if let Some(outcome) = outcome {
2817 if matches!(outcome, ChatPersist::New(_)) {
2818 new += 1;
2819 }
2820 outcomes.push(outcome);
2821 }
2822 }
2823 let mut pending: Vec<&crate::types::Message> = Vec::new();
2827 for outcome in &outcomes {
2828 if !session.is_valid() {
2829 pending.clear();
2830 break;
2831 }
2832 match outcome {
2833 ChatPersist::New(m) => pending.push(m),
2834 ChatPersist::Updated { message, edit_event } => match edit_event {
2835 Some(ev) => {
2836 let mut ev = (**ev).clone();
2837 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
2840 ev.chat_id = cid;
2841 }
2842 let _ = crate::db::events::save_event(&ev).await;
2843 }
2844 None => pending.push(message),
2845 },
2846 ChatPersist::Removed(target_id) => {
2847 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2848 let _ = crate::db::events::delete_event(target_id).await;
2849 }
2850 ChatPersist::ReactionRemoved { reaction_id, message } => {
2851 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2852 let _ = crate::db::events::delete_event(reaction_id).await;
2853 pending.push(message);
2854 }
2855 }
2856 }
2857 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2858 if session.is_valid() {
2864 for outcome in &outcomes {
2865 match outcome {
2866 ChatPersist::New(msg) => crate::traits::emit_event(
2867 "message_new",
2868 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
2869 ),
2870 ChatPersist::Updated { message, .. }
2871 | ChatPersist::ReactionRemoved { message, .. } => {
2872 let mut message = message.clone();
2873 let target_id = message.id.clone();
2874 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
2875 }
2876 ChatPersist::Removed(target_id) => crate::traits::emit_event(
2877 "message_removed",
2878 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
2879 ),
2880 }
2881 }
2882 }
2883 new
2884 }
2885
2886 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2890 if community_id.len() != 64 {
2891 return Ok(None);
2892 }
2893 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2894 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
2895 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
2896 _ => Ok(None),
2897 }
2898 }
2899
2900 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
2905 use crate::community::CommunityId;
2906 if community_id.len() != 64 {
2907 return Err(VectorError::Other("malformed community id".into()));
2908 }
2909 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
2910 .map_err(VectorError::Other)?
2911 .ok_or_else(|| VectorError::Other("community not found".into()))
2912 }
2913
2914 fn admin_role_id_of(community_id: &str) -> Result<String> {
2915 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2916 roles.roles.iter()
2917 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
2918 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_ALL))
2919 .map(|r| r.role_id.clone())
2920 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
2921 }
2922
2923 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
2927 use crate::community::service;
2928 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2929 use crate::community::roles::Permissions;
2930 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
2931 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
2932 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2933 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2936 if banned.contains(&me) && me != owner_hex {
2937 return Ok(serde_json::json!({
2938 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
2939 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
2940 }));
2941 }
2942 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
2943 return Ok(serde_json::json!({
2944 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
2945 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
2946 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
2947 "manage_admin_role": me == owner_hex,
2949 }));
2950 }
2951 let community = Self::load_community_hex(community_id)?;
2952 let caps = service::caller_capabilities(&community);
2953 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
2954 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
2955 .unwrap_or(false);
2956 Ok(serde_json::json!({
2957 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
2958 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
2959 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
2960 "manage_admin_role": manage_admin_role,
2961 }))
2962 }
2963
2964 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
2967 use nostr_sdk::prelude::{PublicKey, ToBech32};
2968 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
2969 let owner = v2.owner().map_err(VectorError::Other)?;
2970 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2971 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
2973 let admins: Vec<String> = roster.grants.iter()
2974 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
2975 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2976 .collect();
2977 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
2978 }
2979 let community = Self::load_community_hex(community_id)?;
2980 let owner = community.owner_attestation.as_ref()
2981 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
2982 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
2983 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
2984 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
2985 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
2986 .collect();
2987 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
2988 }
2989
2990 async fn converge_v2_authority(
2999 transport: &crate::community::transport::LiveTransport,
3000 community_id: &str,
3001 session: &crate::state::SessionGuard,
3002 ) {
3003 if !session.is_valid() {
3004 return;
3005 }
3006 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
3009 let _ = crate::community::v2::service::follow_control(transport, &fresh, session).await;
3010 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh, session).await {
3015 if !added.is_empty() && session.is_valid() {
3016 traits::emit_event_json(
3017 "community_refreshed",
3018 serde_json::json!({ "community_id": community_id }),
3019 );
3020 }
3021 }
3022 }
3023 }
3024
3025 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3027 use crate::community::{service, transport::LiveTransport};
3028 let session = crate::state::SessionGuard::capture();
3029 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3030 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3031 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3032 crate::community::v2::service::grant_admin(&transport, &v2, &member)
3033 .await
3034 .map_err(VectorError::Other)?;
3035 Self::converge_v2_authority(&transport, community_id, &session).await;
3036 return Ok(());
3037 }
3038 let community = Self::load_community_hex(community_id)?;
3039 let role_id = Self::admin_role_id_of(community_id)?;
3040 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3041 }
3042
3043 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3045 use crate::community::{service, transport::LiveTransport};
3046 let session = crate::state::SessionGuard::capture();
3047 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3048 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3049 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3050 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
3051 .await
3052 .map_err(VectorError::Other)?;
3053 Self::converge_v2_authority(&transport, community_id, &session).await;
3054 return Ok(());
3055 }
3056 let community = Self::load_community_hex(community_id)?;
3057 let role_id = Self::admin_role_id_of(community_id)?;
3058 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3059 }
3060
3061 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
3063 use crate::community::{service, transport::LiveTransport};
3064 let session = crate::state::SessionGuard::capture();
3065 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3066 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3067 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3068 crate::community::v2::service::kick_member(&transport, &v2, &pk)
3069 .await
3070 .map_err(VectorError::Other)?;
3071 if session.is_valid() {
3076 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2, &session).await {
3077 if !fresh.is_empty() {
3078 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
3079 }
3080 }
3081 }
3082 Self::converge_v2_authority(&transport, community_id, &session).await;
3083 return Ok(());
3084 }
3085 let community = Self::load_community_hex(community_id)?;
3086 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
3087 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
3088 }
3089
3090 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
3093 use crate::community::{service, transport::LiveTransport, CommunityId};
3094 let session = crate::state::SessionGuard::capture();
3095 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3096 let hex = pk.to_hex();
3097 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3098 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
3100 list.retain(|h| h != &hex);
3101 if banned {
3102 list.push(hex);
3103 }
3104 if community_id.len() == 64 {
3108 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3109 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3110 let community = {
3117 let lock = crate::community::v2::realtime::follow_lock(&cid);
3118 let _rotation = lock.lock().await;
3119 let community = crate::db::community::load_community_v2(&cid)
3120 .map_err(VectorError::Other)?
3121 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3122 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
3123 if banned {
3124 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
3125 }
3126 community
3127 };
3128 if banned {
3129 crate::community::v2::service::refound_community(&transport, &community, &[pk]).await.map_err(VectorError::Other)?;
3130 }
3131 Self::converge_v2_authority(&transport, community_id, &session).await;
3132 return Ok(());
3133 }
3134 }
3135 let community = Self::load_community_hex(community_id)?;
3136 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
3137 }
3138
3139 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
3143 use crate::community::{service, transport::LiveTransport, CommunityId};
3144 if community_id.len() != 64 {
3145 return Err(VectorError::Other("malformed community id".into()));
3146 }
3147 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3148 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3149 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 return crate::community::v2::service::dissolve_community(&transport, &community)
3156 .await
3157 .map_err(VectorError::Other);
3158 }
3159 let community = Self::load_community_hex(community_id)?;
3160 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
3161 }
3162
3163 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
3166 use crate::community::{service, transport::LiveTransport, CommunityId};
3167 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3168 if community_id.len() == 64 {
3173 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3174 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3175 let community = crate::db::community::load_community_v2(&cid)
3176 .map_err(VectorError::Other)?
3177 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3178 let mut meta = community.metadata();
3179 if let Some(n) = name {
3180 meta.name = n.to_string();
3181 }
3182 if let Some(d) = description {
3183 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
3184 }
3185 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
3186 .await
3187 .map_err(VectorError::Other);
3188 }
3189 }
3190 let mut community = Self::load_community_hex(community_id)?;
3191 if let Some(n) = name { community.name = n.to_string(); }
3192 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
3193 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
3194 }
3195
3196
3197
3198 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
3201 use crate::community::{transport::LiveTransport, CommunityId};
3202 if community_id.len() != 64 {
3203 return Err(VectorError::Other("malformed community id".into()));
3204 }
3205 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3206 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3208 let session = state::SessionGuard::capture();
3209 let channel_ids: Vec<String> =
3210 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
3211 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3212 crate::community::v2::service::leave_community(&transport, &v2)
3213 .await
3214 .map_err(VectorError::Other)?;
3215 if !session.is_valid() {
3216 return Err(VectorError::Other("account changed during leave".into()));
3217 }
3218 let mut st = state::STATE.lock().await;
3219 st.chats.retain(|c| !channel_ids.contains(&c.id));
3220 return Ok(());
3221 }
3222 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
3223 let channel_ids: Vec<String> = community
3224 .as_ref()
3225 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
3226 .unwrap_or_default();
3227 if let Some(ref c) = community {
3229 if let Some(primary) = c.channels.first() {
3230 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3231 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
3232 }
3233 }
3234 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
3236 {
3237 let mut st = state::STATE.lock().await;
3238 st.chats.retain(|c| !channel_ids.contains(&c.id));
3239 }
3240 Ok(())
3241 }
3242
3243 fn ensure_v1_writable(community: &crate::community::Community) -> Result<()> {
3251 if crate::db::community::get_community_dissolved(&community.id.to_hex()).unwrap_or(false) {
3252 return Err(VectorError::Other("this community has been dissolved".into()));
3253 }
3254 Ok(())
3255 }
3256
3257 fn resolve_channel(
3258 &self,
3259 channel_id: &str,
3260 ) -> Result<(crate::community::Community, crate::community::Channel)> {
3261 use crate::community::CommunityId;
3262 let community_id = crate::db::community::community_id_for_channel(channel_id)
3263 .map_err(VectorError::Other)?
3264 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
3265 if community_id.len() != 64 {
3266 return Err(VectorError::Other("malformed community id".into()));
3267 }
3268 let community = crate::db::community::load_community(&CommunityId(
3269 crate::simd::hex::hex_to_bytes_32(&community_id),
3270 ))
3271 .map_err(VectorError::Other)?
3272 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
3273 let channel = community
3274 .channels
3275 .iter()
3276 .find(|c| c.id.to_hex() == channel_id)
3277 .cloned()
3278 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
3279 Ok((community, channel))
3280 }
3281
3282
3283 pub async fn sync_dms(
3300 &self,
3301 since_days: Option<u64>,
3302 handler: &dyn InboundEventHandler,
3303 ) -> Result<(u32, u32)> {
3304 use futures_util::StreamExt;
3305 use nostr_sdk::prelude::*;
3306
3307 let client = state::nostr_client()
3308 .ok_or(VectorError::Other("Not connected".into()))?;
3309 let my_pk = state::my_public_key()
3310 .ok_or(VectorError::Other("Not logged in".into()))?;
3311
3312 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
3314
3315 let (items, filter) = if let Some(days) = since_days {
3317 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
3318 let items: Vec<(EventId, Timestamp)> = all_items.iter()
3319 .filter(|(_, ts)| ts.as_secs() >= since_ts)
3320 .cloned()
3321 .collect();
3322 let filter = Filter::new()
3323 .pubkey(my_pk)
3324 .kind(Kind::GiftWrap)
3325 .since(Timestamp::from_secs(since_ts));
3326 (items, filter)
3327 } else {
3328 let filter = Filter::new()
3329 .pubkey(my_pk)
3330 .kind(Kind::GiftWrap);
3331 (all_items, filter)
3332 };
3333
3334 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
3335
3336 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
3338 .direction(nostr_sdk::prelude::SyncDirection::Down)
3339 .initial_timeout(std::time::Duration::from_secs(10))
3340 .dry_run();
3341
3342 let relay_map = client.relays().await;
3346 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
3347 relay_map.iter()
3348 .map(|(url, relay)| (url.clone(), relay.clone()))
3349 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
3350 drop(relay_map);
3351 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
3352 if !skipped_no_neg.is_empty() {
3353 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
3354 }
3355
3356 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
3360 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
3361 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
3362 .min(neg_outer);
3363 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
3364 for (url, relay) in &all_relays {
3365 let url = url.clone();
3366 let relay = relay.clone();
3367 let f = filter.clone();
3368 let i = items.clone();
3369 let o = sync_opts.clone();
3370 relay_futs.push(async move {
3371 if !negentropy::wait_connected(&relay, connect_allowance).await {
3372 return (url, None, false);
3373 }
3374 let result = tokio::time::timeout(
3377 neg_outer,
3378 relay.sync(f).items(i).opts(o),
3379 ).await;
3380 let connected = relay.status() == RelayStatus::Connected;
3381 (url, Some(result), connected)
3382 });
3383 }
3384
3385 let cap_session = state::SessionGuard::capture();
3387 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
3388 while let Some((url, result, connected)) = relay_futs.next().await {
3389 let Some(result) = result else {
3390 log_warn!("[SyncDMs] {} skipped: not connected", url);
3391 continue;
3392 };
3393 match result {
3394 Ok(Ok(recon)) => {
3395 let count = recon.remote.len();
3396 all_missing.extend(recon.remote);
3397 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
3398 if cap_session.is_valid() {
3399 negentropy::record_neg_support(url.as_str(), true);
3400 }
3401 }
3402 Ok(Err(e)) => {
3403 log_warn!("[SyncDMs] {} failed: {}", url, e);
3404 if cap_session.is_valid()
3405 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
3406 {
3407 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
3408 negentropy::record_neg_support(url.as_str(), false);
3409 }
3410 }
3411 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
3412 }
3413 }
3414
3415 let mut total_events = 0u32;
3416 let mut new_messages = 0u32;
3417
3418 if !skipped_no_neg.is_empty() {
3423 let req_filter = filter.clone().limit(500);
3424 match client
3425 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3426 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
3427 ))
3428 .timeout(std::time::Duration::from_secs(20))
3429 .await
3430 {
3431 Ok(stream) => {
3432 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
3433 tokio::pin!(stream);
3434 while let Some((_relay, res)) = stream.next().await {
3435 let Ok(event) = res else { continue };
3436 if !cap_session.is_valid() { break; }
3440 if !seen.insert(event.id.to_bytes()) { continue; }
3441 total_events += 1;
3442 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
3443 if event_handler::commit_prepared_event(prepared, false, handler).await {
3444 new_messages += 1;
3445 }
3446 }
3447 }
3448 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
3449 }
3450 }
3451
3452 if all_missing.is_empty() {
3453 log_info!("[SyncDMs] No missing events");
3454 return Ok((total_events, new_messages));
3455 }
3456
3457 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
3459 let ids: Vec<EventId> = all_missing.into_iter().collect();
3460 let relay_strs: Vec<String> = client.relays().await.keys()
3461 .map(|u| u.to_string()).collect();
3462
3463 const BATCH_SIZE: usize = 500;
3464
3465 for batch in ids.chunks(BATCH_SIZE) {
3466 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
3469 match client
3470 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3471 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
3472 ))
3473 .timeout(std::time::Duration::from_secs(30))
3474 .await
3475 {
3476 Ok(stream) => {
3477 let client_clone = client.clone();
3478 let prepared_stream = stream
3479 .filter_map(|(_relay, res)| async move { res.ok() })
3480 .map(move |event| {
3481 let c = client_clone.clone();
3482 tokio::spawn(async move {
3483 event_handler::prepare_event(event, &c, my_pk).await
3484 })
3485 })
3486 .buffer_unordered(8);
3487 tokio::pin!(prepared_stream);
3488
3489 while let Some(result) = prepared_stream.next().await {
3490 total_events += 1;
3491 if let Ok(prepared) = result {
3492 if event_handler::commit_prepared_event(prepared, false, handler).await {
3493 new_messages += 1;
3494 }
3495 }
3496 }
3497 }
3498 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
3499 }
3500 }
3501
3502 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
3503 Ok((total_events, new_messages))
3504 }
3505
3506 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
3515 use nostr_sdk::prelude::*;
3516 let client = state::nostr_client()
3517 .ok_or(VectorError::Other("Not connected".into()))?;
3518 let my_pk = state::my_public_key()
3519 .ok_or(VectorError::Other("Not logged in".into()))?;
3520
3521 let filter = Filter::new()
3522 .pubkey(my_pk)
3523 .kind(Kind::GiftWrap)
3524 .limit(0);
3525
3526 let output = client.subscribe(filter).await
3527 .map_err(|e| VectorError::Nostr(e.to_string()))?;
3528 Ok(output.value)
3529 }
3530
3531 pub async fn sync_communities(&self) -> Result<()> {
3542 {
3546 use crate::community::{transport::LiveTransport, v2::service as v2};
3547 let bootstrap: Vec<String> = match crate::state::nostr_client() {
3548 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
3549 None => Vec::new(),
3550 };
3551 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3552 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
3553 let joined = outcome.joined;
3556 for c in &joined {
3557 if community::v2::realtime::follow_worker_running() {
3558 community::v2::realtime::enqueue_follow(c.id());
3559 } else {
3560 let _ = Self::v2_inline_follow(c.id()).await;
3561 }
3562 }
3563 if !joined.is_empty() {
3564 if let Some(client) = crate::state::nostr_client() {
3565 community::v2::realtime::refresh_subscription(&client).await;
3566 }
3567 }
3568 }
3569 }
3570
3571 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
3572 for id in ids {
3573 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
3574 if community::v2::realtime::follow_worker_running() {
3577 community::v2::realtime::enqueue_follow(&id);
3578 } else {
3579 let _ = Self::v2_inline_follow(&id).await;
3580 }
3581 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
3588 for ch in &c.channels {
3589 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
3590 let _ = Self::v2_backfill_channel(
3591 &id, &hex, 50, 2, None,
3592 crate::community::transport::Evidence::Fast, 12,
3593 ).await;
3594 }
3595 }
3596 continue;
3597 }
3598 if let Ok(Some(community)) = db::community::load_community(&id) {
3599 for ch in &community.channels {
3600 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
3601 }
3602 }
3603 }
3604 Ok(())
3605 }
3606
3607
3608 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
3640 use nostr_sdk::prelude::*;
3641
3642 let client = state::nostr_client()
3643 .ok_or(VectorError::Other("Not connected".into()))?;
3644 let my_pk = state::my_public_key()
3645 .ok_or(VectorError::Other("Not logged in".into()))?;
3646
3647 community::v2::streamauth::ensure_responder(&client);
3654
3655 community::v2::realtime::spawn_follow_worker(handler.clone());
3664 let _ = self.sync_communities().await;
3665 let _ = self.sync_dms(None, &NoOpEventHandler).await;
3666
3667 let dm_sub_id = self.subscribe_dms().await?;
3670 community::realtime::refresh_subscription(&client).await;
3671 community::v2::realtime::refresh_subscription(&client).await;
3672
3673 if let Some(monitor) = client.monitor() {
3680 let mut rx = monitor.subscribe();
3681 let session = state::SessionGuard::capture();
3682 tokio::spawn(async move {
3683 let mut last_resync: Option<std::time::Instant> = None;
3686 while let Ok(notification) = rx.recv().await {
3687 if !session.is_valid() {
3688 return;
3689 }
3690 let MonitorNotification::StatusChanged { status, .. } = notification;
3691 if status == RelayStatus::Connected {
3692 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
3693 continue;
3694 }
3695 let _ = VectorCore.sync_communities().await;
3696 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
3697 if let Some(c) = state::nostr_client() {
3698 community::realtime::refresh_subscription(&c).await;
3699 community::v2::realtime::refresh_subscription(&c).await;
3700 }
3701 last_resync = Some(std::time::Instant::now());
3702 }
3703 }
3704 });
3705 }
3706
3707 {
3711 let client_health = client.clone();
3712 let session = state::SessionGuard::capture();
3713 tokio::spawn(async move {
3714 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
3716 if !session.is_valid() {
3717 return;
3718 }
3719 for (url, relay) in client_health.relays().await {
3720 match relay.status() {
3721 RelayStatus::Connected => {
3722 let probe = tokio::time::timeout(
3723 std::time::Duration::from_secs(10),
3724 client_health
3725 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
3726 url.to_string(),
3727 [Filter::new().kind(Kind::Metadata).limit(1)],
3728 ))
3729 .timeout(std::time::Duration::from_secs(8)),
3730 )
3731 .await;
3732 if !matches!(probe, Ok(Ok(_))) {
3733 let _ = relay.disconnect();
3734 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
3735 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3736 }
3737 }
3738 RelayStatus::Terminated | RelayStatus::Disconnected => {
3739 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
3740 }
3741 _ => {}
3742 }
3743 }
3744 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
3745 }
3746 });
3747 }
3748
3749 let client_for_closure = client.clone();
3750
3751 let mut notifications = client.notifications();
3754 while let Some(notification) = notifications.next().await {
3755 let handler = handler.clone();
3756 let c = client_for_closure.clone();
3757 let dm_sid = dm_sub_id.clone();
3758 {
3759 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
3763 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
3764 sending::note_relay_ok(event_id, *status);
3765 }
3766 }
3767 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
3768 if subscription_id == dm_sid {
3769 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
3771 event_handler::commit_prepared_event(prepared, true, &*handler).await;
3772 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3773 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3774 {
3775 let session = state::SessionGuard::capture();
3779 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
3780 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
3781 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
3782 {
3783 let session = state::SessionGuard::capture();
3785 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
3786 }
3787 }
3788 }
3789 }
3790
3791 Ok(())
3792 }
3793
3794 pub async fn logout(&self) {
3796 if let Some(client) = state::nostr_client() {
3797 let _ = client.disconnect().await;
3798 }
3799 db::close_database();
3800 }
3801
3802 pub async fn swap_session(&self) {
3810 state::bump_session_generation();
3812
3813 if let Some(client) = state::take_nostr_client() {
3816 let _ = client.shutdown().await;
3817 }
3818 db::close_database();
3819
3820 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
3822 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
3823 {
3824 use zeroize::Zeroize;
3825 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
3826 if let Some(s) = g.as_mut() { s.zeroize(); }
3827 *g = None;
3828 }
3829 if let Ok(mut g) = state::PENDING_NSEC.lock() {
3830 if let Some(s) = g.as_mut() { s.zeroize(); }
3831 *g = None;
3832 }
3833 }
3834
3835 {
3837 let mut st = state::STATE.lock().await;
3838 st.profiles.clear();
3839 st.chats.clear();
3840 st.db_loaded = false;
3841 st.is_syncing = false;
3842 }
3843 state::WRAPPER_ID_CACHE.lock().await.clear();
3844 state::PENDING_EVENTS.lock().await.clear();
3845 state::set_active_chat(None);
3846 crate::profile::sync::clear_profile_sync_queue();
3847 crate::inbox_relays::clear_inbox_relay_cache();
3848 crate::sending::clear_wrap_confirms();
3851 crate::emoji_packs::clear_nip65_cache();
3852 crate::db::clear_id_caches();
3856 crate::community::cache::clear();
3860 crate::community::realtime::clear().await;
3863 crate::community::v2::realtime::clear().await;
3864 crate::community::transport::clear_plane_pool();
3866 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
3870 }
3871}
3872
3873#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
3874mod transport_policy_tests {
3875 use std::time::Duration;
3876
3877 #[test]
3880 fn tor_transport_policy() {
3881 let short = Duration::from_secs(5);
3882 let long = Duration::from_secs(300);
3883
3884 crate::tor::set_tor_enabled_pref(false);
3887 assert_eq!(super::tor_proxy_target(), None);
3888 assert_eq!(super::relay_connect_timeout(short), short);
3889 assert_eq!(super::relay_request_timeout(short), short);
3890
3891 crate::tor::set_tor_enabled_pref(true);
3895 assert!(matches!(
3896 crate::tor::transport_state(),
3897 crate::tor::TorTransportState::RequiredButInactive
3898 ));
3899 assert_eq!(
3905 super::tor_proxy_target(),
3906 Some(crate::tor::blackhole_proxy_addr()),
3907 "Tor enabled but inactive must blackhole, never connect direct"
3908 );
3909 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
3910 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
3911
3912 for tor in [true, false] {
3915 crate::tor::set_tor_enabled_pref(tor);
3916 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
3917 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
3918 }
3919 }
3920}
3921
3922#[cfg(test)]
3923mod facade_tests {
3924 use super::*;
3925
3926 #[tokio::test]
3929 async fn download_attachment_rejects_private_url() {
3930 let att = crate::types::Attachment {
3931 url: "http://169.254.169.254/latest/meta-data/".to_string(),
3932 ..Default::default()
3933 };
3934 match VectorCore.download_attachment(&att).await {
3935 Err(VectorError::Other(msg)) => {
3936 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
3937 }
3938 other => panic!("expected SSRF rejection, got {other:?}"),
3939 }
3940 }
3941
3942 #[tokio::test]
3943 async fn download_attachment_rejects_empty_url() {
3944 let att = crate::types::Attachment::default();
3945 assert!(VectorCore.download_attachment(&att).await.is_err());
3946 }
3947
3948 #[tokio::test]
3952 async fn list_communities_and_channel_routing_are_protocol_aware() {
3953 use crate::community::transport::memory::MemoryRelay;
3954 use nostr_sdk::prelude::Keys;
3955
3956 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
3957 crate::db::close_database();
3958 crate::db::clear_id_caches();
3959 let tmp = tempfile::tempdir().unwrap();
3960 let acct = {
3962 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
3963 let mut s = String::from("npub1");
3964 for i in 0..58 {
3965 s.push(B[(i * 7 + 3) % 32] as char);
3966 }
3967 s
3968 };
3969 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
3970 crate::db::set_app_data_dir(tmp.path().to_path_buf());
3971 crate::db::set_current_account(acct.clone()).unwrap();
3972 crate::db::init_database(&acct).unwrap();
3973 let _ = crate::state::take_nostr_client();
3974 let me = Keys::generate();
3975 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
3976 crate::state::set_my_public_key(me.public_key());
3977
3978 let relay = MemoryRelay::new();
3980 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
3981 .await
3982 .unwrap();
3983 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
3984
3985 let listed = VectorCore.list_communities().await;
3987 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
3988 assert_eq!(v2["name"], "V2 Guild");
3989 assert_eq!(v2["is_owner"], true);
3990 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
3991
3992 assert_eq!(
3994 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
3995 Some(community.identity.community_id),
3996 "a v2 channel is routed to v2"
3997 );
3998 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
4000 }
4001
4002 #[test]
4007 fn v2_invite_url_base_derivation_round_trips() {
4008 use crate::community::v2::derive::TOKEN_LEN;
4009 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
4010 use nostr_sdk::prelude::Keys;
4011 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
4012 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
4013 let signer = Keys::generate();
4014 let token = [0x07u8; TOKEN_LEN];
4015 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
4016 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
4017 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
4018 let parsed = parse_invite_link(&url).unwrap();
4019 assert_eq!(parsed.link_signer, signer.public_key());
4020 assert_eq!(parsed.token, token);
4021 }
4022}