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_messages_before(
969 &self,
970 chat_id: &str,
971 before: Option<(u64, &str)>,
972 limit: usize,
973 ) -> Vec<Message> {
974 let state = state::STATE.lock().await;
975 let Some(chat) = state.get_chat(chat_id) else {
976 return Vec::new();
977 };
978 let mut msgs = chat.get_all_messages(&state.interner);
979 if let Some((at, id)) = before {
980 msgs.retain(|m| (m.at, m.id.as_str()) < (at, id));
981 }
982 msgs.sort_by(|a, b| (a.at, a.id.as_str()).cmp(&(b.at, b.id.as_str())));
983 if msgs.len() > limit {
984 msgs.drain(..msgs.len() - limit);
985 }
986 msgs
987 }
988
989 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
991 let state = state::STATE.lock().await;
992 state.get_profile(npub)
993 .map(|p| SlimProfile::from_profile(p, &state.interner))
994 }
995
996 pub async fn load_profile(&self, npub: &str) -> bool {
998 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
999 }
1000
1001 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1003 profile::sync::update_profile(
1004 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1005 &NoOpProfileSyncHandler,
1006 ).await
1007 }
1008
1009 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1012 profile::sync::update_bot_profile(
1013 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1014 &NoOpProfileSyncHandler,
1015 ).await
1016 }
1017
1018 pub async fn update_status(&self, status: &str) -> bool {
1020 profile::sync::update_status(status.to_string()).await
1021 }
1022
1023 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
1029 let path = std::path::Path::new(file_path);
1030 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1031 if bytes.is_empty() {
1032 return Err(VectorError::Other("Empty image file".into()));
1033 }
1034 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1035 let mime = crate::crypto::mime_from_extension(&extension);
1036 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1037 let signer = crate::signer::active_signer()
1038 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1039 let servers = crate::blossom_servers::compute_enabled_servers();
1040 if servers.is_empty() {
1041 return Err(VectorError::Other("No Blossom servers configured".into()));
1042 }
1043 crate::blossom::upload_blob_with_failover(
1046 signer,
1047 servers,
1048 std::sync::Arc::new(bytes),
1049 Some(mime),
1050 Some(std::time::Duration::from_secs(20)),
1051 )
1052 .await
1053 .map_err(VectorError::Other)
1054 }
1055
1056 pub async fn block_user(&self, npub: &str) -> bool {
1058 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1059 }
1060
1061 pub async fn unblock_user(&self, npub: &str) -> bool {
1063 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1064 }
1065
1066 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1068 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1069 }
1070
1071 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1073 profile::sync::get_blocked_users().await
1074 }
1075
1076 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1078 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1079 }
1080
1081 pub fn my_npub(&self) -> Option<String> {
1083 state::my_public_key()
1084 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1085 }
1086
1087 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1094 use crate::community::ConcordProtocol;
1095 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1096 let mut out = Vec::new();
1097 for id in ids {
1098 match crate::db::community::community_protocol(&id).ok().flatten() {
1100 Some(ConcordProtocol::V2) => {
1101 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1102 let me = state::my_public_key();
1103 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1104 out.push(serde_json::json!({
1105 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1106 "version": 2,
1107 "name": c.name,
1108 "description": c.description,
1109 "is_owner": is_owner,
1110 "dissolved": c.dissolved,
1115 "channels": c.channels.iter()
1121 .map(|ch| serde_json::json!({
1122 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1123 "name": ch.name,
1124 "private": ch.private,
1125 "readable": !(ch.private && ch.key.is_none()),
1126 "epoch": ch.epoch.0,
1127 }))
1128 .collect::<Vec<_>>(),
1129 }));
1130 }
1131 }
1132 _ => {
1133 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1134 out.push(serde_json::json!({
1135 "community_id": c.id.to_hex(),
1136 "version": 1,
1137 "name": c.name,
1138 "description": c.description,
1139 "is_owner": crate::community::service::is_proven_owner(&c),
1140 "dissolved": c.dissolved,
1141 "channels": c.channels.iter()
1142 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1143 .collect::<Vec<_>>(),
1144 }));
1145 }
1146 }
1147 }
1148 }
1149 out
1150 }
1151
1152 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1157 use crate::community::{v2::service as v2, transport::LiveTransport};
1158 let relays: Vec<String> = crate::state::active_trusted_relays()
1159 .await
1160 .iter()
1161 .map(|s| s.to_string())
1162 .collect();
1163 if relays.is_empty() {
1164 return Err(VectorError::Other("no relays available to host the Community".into()));
1165 }
1166 let session = state::SessionGuard::capture();
1167 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1168 let community = v2::create_community(&transport, name, relays, None)
1169 .await
1170 .map_err(VectorError::Other)?;
1171 self.register_v2_chats(&community, &session).await;
1172 if let Some(client) = state::nostr_client() {
1174 crate::community::v2::realtime::refresh_subscription(&client).await;
1175 }
1176 Ok(Self::v2_summary(&community))
1177 }
1178
1179 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1185 use crate::community::ConcordProtocol;
1186 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1187 return Ok(None);
1188 };
1189 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1190 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1191 Some(ConcordProtocol::V2) => Some(cid),
1192 _ => None,
1193 })
1194 }
1195
1196 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1198 let me = state::my_public_key();
1199 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1200 serde_json::json!({
1201 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1202 "version": 2,
1203 "name": community.name,
1204 "description": community.description,
1205 "is_owner": is_owner,
1206 "channels": community.channels.iter()
1207 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1208 .collect::<Vec<_>>(),
1209 })
1210 }
1211
1212 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1218 register_v2_chats_inner(community, session).await
1219 }
1220}
1221
1222pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2, session: &state::SessionGuard) {
1225 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1226 let me = state::my_public_key();
1227 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1228 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1229 let Some(primary) = community.primary_channel() else { return };
1232 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1233 let slims = {
1238 let mut st = state::STATE.lock().await;
1239 if !session.is_valid() {
1240 return; }
1242 let mut slims = Vec::new();
1243 for ch in &community.channels {
1244 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1245 st.upsert_community_chat(
1246 &ch_hex,
1247 &community.name,
1248 community.description.as_deref().unwrap_or(""),
1249 &id_hex,
1250 is_owner,
1251 community.icon.is_some(),
1252 owner_npub.as_deref(),
1253 Some(community.created_at_ms),
1254 community.dissolved,
1255 crate::community::ConcordProtocol::V2,
1256 &ch.name,
1257 &primary_hex,
1258 );
1259 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1260 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1261 }
1262 }
1263 slims
1264 };
1265 if !session.is_valid() {
1269 return;
1270 }
1271 for slim in &slims {
1272 let _ = crate::db::chats::save_slim_chat(slim);
1273 }
1274}
1275
1276impl VectorCore {
1277 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1281 use crate::community::{public_invite, service, transport::LiveTransport};
1282 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1286 let session = state::SessionGuard::capture();
1287 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1288 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1289 .await
1290 .map_err(VectorError::Other)?;
1291 self.register_v2_chats(&community, &session).await;
1292 if let Some(client) = state::nostr_client() {
1293 crate::community::v2::realtime::refresh_subscription(&client).await;
1294 }
1295 if crate::community::v2::realtime::follow_worker_running() {
1300 crate::community::v2::realtime::enqueue_follow(community.id());
1301 } else {
1302 let seed_session = state::SessionGuard::capture();
1303 let seed_community = community.clone();
1304 tokio::spawn(async move {
1305 if !seed_session.is_valid() {
1306 return;
1307 }
1308 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1309 if matches!(
1310 crate::community::v2::service::sync_guestbook(&transport, &seed_community, &seed_session).await,
1311 Ok(fresh) if !fresh.is_empty()
1312 ) {
1313 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1314 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1315 }
1316 });
1317 }
1318 return Ok(Self::v2_summary(&community));
1319 }
1320 let (relays, token) = public_invite::parse_invite_url(invite_url)
1321 .map_err(|e| VectorError::Other(e.to_string()))?;
1322 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1323 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1324 .await
1325 .map_err(VectorError::Other)?;
1326 let now = std::time::SystemTime::now()
1327 .duration_since(std::time::UNIX_EPOCH)
1328 .map(|d| d.as_secs())
1329 .unwrap_or(0);
1330 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1333 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1334 .await
1335 .map_err(VectorError::Other)?;
1336 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1337 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1340 self.finalize_member_join(community, &transport, attribution).await
1341 }
1342
1343 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1346 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1347 Ok(rows.iter().map(|p| {
1348 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1351 serde_json::json!({
1352 "community_id": p.community_id,
1353 "name": v2.name,
1354 "inviter_npub": p.inviter_npub,
1355 "version": 2,
1356 })
1357 } else {
1358 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1359 .ok().map(|i| i.name).unwrap_or_default();
1360 serde_json::json!({
1361 "community_id": p.community_id,
1362 "name": name,
1363 "inviter_npub": p.inviter_npub,
1364 "version": 1,
1365 })
1366 }
1367 }).collect())
1368 }
1369
1370 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1374 use crate::community::transport::LiveTransport;
1375 let bundle_json = crate::db::community::get_pending_invite(community_id)
1376 .map_err(VectorError::Other)?
1377 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1378 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1379
1380 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1382 let session = state::SessionGuard::capture();
1383 let inviter = crate::db::community::list_pending_invites()
1385 .ok()
1386 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1387 let community = crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref())
1395 .await
1396 .map_err(VectorError::Other)?;
1397 if !session.is_valid() {
1398 return Err(VectorError::Other("account changed during join".into()));
1399 }
1400 self.register_v2_chats(&community, &session).await;
1401 if let Some(client) = state::nostr_client() {
1402 crate::community::v2::realtime::refresh_subscription(&client).await;
1403 }
1404 crate::community::v2::realtime::enqueue_follow(community.id());
1405 let _ = crate::db::community::delete_pending_invite(community_id);
1406 return Ok(Self::v2_summary(&community));
1407 }
1408
1409 use crate::community::invite::{accept_invite, CommunityInvite};
1411 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1412 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1413 let now = std::time::SystemTime::now()
1417 .duration_since(std::time::UNIX_EPOCH)
1418 .map(|d| d.as_secs())
1419 .unwrap_or(0);
1420 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1421 .await
1422 .map_err(VectorError::Other)?;
1423 let summary = self.finalize_member_join(community, &transport, None).await?;
1425 let _ = crate::db::community::delete_pending_invite(community_id);
1426 Ok(summary)
1427 }
1428
1429 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1434 &self,
1435 community: crate::community::Community,
1436 transport: &T,
1437 attribution: Option<(String, Option<String>)>,
1438 ) -> Result<serde_json::Value> {
1439 use crate::community::service;
1440 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1447 return Ok(serde_json::json!({
1448 "community_id": v2,
1449 "version": 2,
1450 "migrated": true,
1451 }));
1452 }
1453 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1457 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1460 if c.removed {
1461 let _ = crate::db::community::delete_community(&community.id.to_hex());
1462 return Err(VectorError::Other("you have been removed from this community".into()));
1463 }
1464 }
1465 let community = crate::db::community::load_community(&community.id)
1466 .map_err(VectorError::Other)?
1467 .unwrap_or(community);
1468 let _ = service::fetch_and_apply_control(transport, &community).await;
1472 if service::am_i_banned(&community) {
1473 let _ = crate::db::community::delete_community(&community.id.to_hex());
1474 return Err(VectorError::Other("you are banned from this community".into()));
1475 }
1476 let community = crate::db::community::load_community(&community.id)
1478 .map_err(VectorError::Other)?
1479 .unwrap_or(community);
1480 let owner_npub = community
1481 .owner_attestation
1482 .as_ref()
1483 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1484 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1485 {
1486 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1487 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1488 let mut st = state::STATE.lock().await;
1489 for ch in &community.channels {
1490 st.upsert_community_chat(
1491 &ch.id.to_hex(),
1492 &community.name,
1493 community.description.as_deref().unwrap_or(""),
1494 &community.id.to_hex(),
1495 crate::community::service::is_proven_owner(&community),
1496 community.icon.is_some(),
1497 owner_npub.as_deref(),
1498 created_at_ms,
1499 community.dissolved,
1500 crate::community::ConcordProtocol::V1,
1501 &ch.name,
1502 &primary_hex,
1503 );
1504 }
1505 }
1506 if let Some(primary) = community.channels.first() {
1509 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1510 }
1511 Ok(serde_json::json!({
1512 "community_id": community.id.to_hex(),
1513 "version": 1,
1514 "name": community.name,
1515 "channels": community.channels.iter()
1516 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1517 .collect::<Vec<_>>(),
1518 }))
1519 }
1520
1521
1522 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1526 use crate::community::CommunityId;
1527 if community_id.len() != 64 {
1528 return Err(VectorError::Other("malformed community id".into()));
1529 }
1530 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1531 match crate::db::community::community_protocol(&cid).ok().flatten() {
1532 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1533 .map_err(VectorError::Other)?
1534 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1535 Some(_) => Err(VectorError::Other(
1536 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1537 )),
1538 None => Err(VectorError::Other("community not found".into())),
1539 }
1540 }
1541
1542 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1543 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1544 .map(crate::community::ChannelId)
1545 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1546 }
1547
1548 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1553 use crate::community::{v2::service, transport::LiveTransport};
1554 let community = Self::v2_community(community_id)?;
1555 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1556 let id = if private {
1557 service::create_private_channel(&transport, &community, name).await
1558 } else {
1559 service::create_public_channel(&transport, &community, name).await
1560 }
1561 .map_err(VectorError::Other)?;
1562 if let Some(client) = state::nostr_client() {
1565 crate::community::v2::realtime::refresh_subscription(&client).await;
1566 }
1567 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1568 }
1569
1570 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &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 mut meta = community
1577 .channel(&id)
1578 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1579 .metadata();
1580 meta.name = name.to_string();
1581 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1582 service::edit_channel_metadata(&transport, &community, &id, &meta)
1583 .await
1584 .map_err(VectorError::Other)
1585 }
1586
1587 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1590 use crate::community::{v2::service, transport::LiveTransport};
1591 let community = Self::v2_community(community_id)?;
1592 let id = Self::channel_id_of(channel_id)?;
1593 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1594 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1595 service::delete_channel(&transport, &community, &id, &name)
1596 .await
1597 .map_err(VectorError::Other)
1598 }
1599
1600 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1603 use crate::community::{v2::service, transport::LiveTransport};
1604 let community = Self::v2_community(community_id)?;
1605 let id = Self::channel_id_of(channel_id)?;
1606 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1607 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1608 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1609 service::grant_channel_access(&transport, &community, &id, &member)
1610 .await
1611 .map_err(VectorError::Other)
1612 }
1613
1614 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1618 use crate::community::{v2::service, transport::LiveTransport};
1619 let community = Self::v2_community(community_id)?;
1620 let id = Self::channel_id_of(channel_id)?;
1621 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1622 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1623 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1624 service::revoke_channel_access(&transport, &community, &id, &member)
1625 .await
1626 .map_err(VectorError::Other)
1627 }
1628
1629 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1637 use nostr_sdk::prelude::{PublicKey, ToBech32};
1638 let community = Self::v2_community(community_id)?;
1639 let id = Self::channel_id_of(channel_id)?;
1640 let ch = community
1641 .channel(&id)
1642 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1643 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1646 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1647 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1648 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1649 let access_ids = roster.channel_role_ids(&chan_hex);
1650 let roles: Vec<serde_json::Value> = roster
1651 .channel_roles(&chan_hex)
1652 .into_iter()
1653 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1654 .collect();
1655 let members: Vec<String> = roster
1656 .grants
1657 .iter()
1658 .filter(|g| !banned.contains(&g.member))
1659 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1660 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1661 .collect();
1662 Ok(serde_json::json!({
1663 "channel_id": chan_hex,
1664 "private": ch.private,
1665 "readable": !(ch.private && ch.key.is_none()),
1666 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1667 "roles": roles,
1668 "members": members,
1669 }))
1670 }
1671
1672 pub async fn create_public_invite(
1677 &self,
1678 community_id: &str,
1679 expires_at_ms: Option<u64>,
1680 label: Option<String>,
1681 ) -> Result<String> {
1682 use crate::community::{service, transport::LiveTransport, CommunityId};
1683 if community_id.len() != 64 {
1684 return Err(VectorError::Other("malformed community id".into()));
1685 }
1686 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1687 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1689 crate::db::community::community_protocol(&cid).ok()
1690 {
1691 let community = crate::db::community::load_community_v2(&cid)
1692 .map_err(VectorError::Other)?
1693 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1694 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1695 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1698 let minted =
1699 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1700 .await
1701 .map_err(VectorError::Other)?;
1702 return Ok(minted.url);
1703 }
1704 let community = crate::db::community::load_community(&CommunityId(
1705 crate::simd::hex::hex_to_bytes_32(community_id),
1706 ))
1707 .map_err(VectorError::Other)?
1708 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1709 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1710 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1711 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1712 .await
1713 .map_err(VectorError::Other)?;
1714 Ok(url)
1715 }
1716
1717 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1721 use crate::community::{service, CommunityId};
1722 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1723
1724 let session = crate::state::SessionGuard::capture();
1725 let my_pk = crate::state::my_public_key()
1726 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1727
1728 if community_id.len() != 64 {
1729 return Err(VectorError::Other("malformed community id".into()));
1730 }
1731 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1732 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1738 crate::db::community::community_protocol(&cid).ok()
1739 {
1740 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1741 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1742 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1743 let bundle = {
1751 let lock = crate::community::v2::realtime::follow_lock(&cid);
1752 let _rotation = lock.lock().await;
1753 let community = crate::db::community::load_community_v2(&cid)
1754 .map_err(VectorError::Other)?
1755 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1756 crate::community::v2::service::bundle_of(
1757 &community,
1758 crate::community::v2::service::BundleAudience::Member(recipient),
1759 Some(my_pk),
1760 None,
1761 None,
1762 )
1763 };
1764 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1765 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1768 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1769 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1770 let rumor = nostr_sdk::prelude::EventBuilder::new(
1771 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1772 bundle_json,
1773 )
1774 .tag(expiry_tag.clone())
1775 .finalize_unsigned_with_id(my_pk);
1776 let k_tag = nostr_sdk::prelude::Tag::custom(
1777 "k",
1778 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1779 );
1780 if !session.is_valid() {
1781 return Err(VectorError::Other("account changed".into()));
1782 }
1783 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1784 .await
1785 .map_err(VectorError::Other)?;
1786 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1787 }
1788 let community = crate::db::community::load_community(&CommunityId(
1789 crate::simd::hex::hex_to_bytes_32(community_id),
1790 ))
1791 .map_err(VectorError::Other)?
1792 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1793
1794 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1795 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1796 }
1797 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1798 .map_err(|_| VectorError::Other("invalid npub".into()))?
1799 .to_hex();
1800 if crate::db::community::get_community_banlist(community_id)
1801 .map_err(VectorError::Other)?
1802 .iter()
1803 .any(|b| b == &invitee_hex)
1804 {
1805 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1806 }
1807
1808 if !session.is_valid() {
1810 return Err(VectorError::Other("account changed during invite".into()));
1811 }
1812
1813 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1814 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1815 .map_err(VectorError::Other)?;
1816 let pending_id = format!("community-invite-{}", community_id);
1817 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1819 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1820
1821 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1822 .await
1823 .map_err(VectorError::Other)?;
1824
1825 Ok(serde_json::json!({
1826 "community_id": community_id,
1827 "invitee": invitee_npub,
1828 "wrap_event_id": result.event_id,
1829 }))
1830 }
1831
1832 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1837 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1838 }
1839
1840 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1845 use crate::community::{service, transport::LiveTransport, CommunityId};
1846 if community_id.len() != 64 {
1847 return Err(VectorError::Other("malformed community id".into()));
1848 }
1849 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1850 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1851 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1854 let community = crate::db::community::load_community_v2(&cid)
1855 .map_err(VectorError::Other)?
1856 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1857 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1858 .await
1859 .map_err(VectorError::Other);
1860 }
1861 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1862 let community = crate::db::community::load_community(&cid)
1863 .map_err(VectorError::Other)?
1864 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1865 service::revoke_public_invite(&transport, &community, &token_bytes)
1866 .await
1867 .map_err(VectorError::Other)
1868 }
1869
1870 pub async fn send_community_message(
1872 &self,
1873 channel_id: &str,
1874 content: &str,
1875 replied_to: Option<&str>,
1876 ) -> Result<String> {
1877 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1878 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1880 let community = crate::db::community::load_community_v2(&id)
1881 .map_err(VectorError::Other)?
1882 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1883 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1884 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1885 let reply = match replied_to.filter(|r| !r.is_empty()) {
1888 Some(parent_id) => {
1889 let author_hex = {
1890 let st = state::STATE.lock().await;
1891 st.find_message(parent_id)
1892 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1893 .map(|pk| pk.to_hex())
1894 .unwrap_or_default()
1895 };
1896 Some((parent_id.to_string(), author_hex))
1897 }
1898 None => None,
1899 };
1900 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1901 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1904 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1905 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1906 .await
1907 .map_err(VectorError::Other);
1908 }
1909 let (community, channel) = self.resolve_channel(channel_id)?;
1910 Self::ensure_v1_writable(&community)?;
1911 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1912 let reply = replied_to.filter(|r| !r.is_empty());
1913 let ms = std::time::SystemTime::now()
1914 .duration_since(std::time::UNIX_EPOCH)
1915 .map(|d| d.as_millis() as u64)
1916 .unwrap_or(0);
1917 let unsigned = envelope::build_inner_typed(
1918 author_pk,
1919 &channel.id,
1920 channel.epoch,
1921 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1922 content,
1923 ms,
1924 reply,
1925 &[],
1926 );
1927 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1928 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1929 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1930 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1931 let session = state::SessionGuard::capture();
1932 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1933 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1934 .await
1935 .map_err(VectorError::Other)?;
1936 if !session.is_valid() {
1939 return Ok(message_id);
1940 }
1941 let echoed = {
1942 let mut st = state::STATE.lock().await;
1943 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1944 };
1945 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1946 let _ = crate::db::events::save_message(channel_id, &msg).await;
1947 }
1948 Ok(message_id)
1949 }
1950
1951 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1955 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1956 let path = std::path::Path::new(file_path);
1957 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1958 if bytes.is_empty() {
1959 return Err(VectorError::Other("Empty file".into()));
1960 }
1961 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1962 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1963
1964 let session = state::SessionGuard::capture();
1967 let v2_target = match self.v2_community_for_channel(channel_id)? {
1970 Some(id) => Some(
1971 crate::db::community::load_community_v2(&id)
1972 .map_err(VectorError::Other)?
1973 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
1974 ),
1975 None => None,
1976 };
1977 let v1_target = match v2_target {
1978 Some(_) => None,
1979 None => Some(self.resolve_channel(channel_id)?),
1980 };
1981 match (&v2_target, &v1_target) {
1986 (Some(c), _) => {
1987 let cid = crate::simd::hex::bytes_to_hex_32(&c.id().0);
1988 if crate::db::community::get_community_dissolved(&cid).unwrap_or(false) {
1989 return Err(VectorError::Other("this community has been dissolved".into()));
1990 }
1991 }
1992 (None, Some((c, _))) => Self::ensure_v1_writable(c)?,
1993 _ => {}
1994 }
1995 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1996
1997 let file_hash = crate::crypto::sha256_hex(&bytes);
1998 let mime = crate::crypto::mime_from_extension(&extension);
1999 let img_meta = crate::crypto::generate_image_metadata(&bytes);
2000
2001 let download_dir = crate::db::get_download_dir();
2003 let _ = std::fs::create_dir_all(&download_dir);
2004 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
2005 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
2006 let _ = std::fs::write(&local_path, &bytes);
2007
2008 let params = crate::crypto::generate_encryption_params();
2010 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
2011 let encrypted_size = encrypted.len() as u64;
2012
2013 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2014 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2015 let servers = crate::blossom_servers::compute_enabled_servers();
2016 if servers.is_empty() {
2017 return Err(VectorError::Other("No Blossom servers configured".into()));
2018 }
2019 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
2020 let url = crate::blossom::upload_blob_with_progress_and_failover(
2021 signer.clone(),
2022 servers,
2023 std::sync::Arc::new(encrypted),
2024 Some(mime),
2025 true,
2026 noop_progress,
2027 Some(3),
2028 Some(std::time::Duration::from_secs(2)),
2029 None,
2030 ).await.map_err(VectorError::Other)?;
2031
2032 let attachment = crate::types::Attachment {
2033 id: file_hash.clone(),
2034 key: params.key.clone(),
2035 nonce: params.nonce.clone(),
2036 extension: extension.clone(),
2037 name: filename.clone(),
2038 url,
2039 path: local_path.to_string_lossy().to_string(),
2040 size: encrypted_size,
2041 img_meta,
2042 downloading: false,
2043 downloaded: true,
2044 ..Default::default()
2045 };
2046 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
2047
2048 if !session.is_valid() {
2050 return Err(VectorError::Other("account changed during upload".into()));
2051 }
2052 if let Some(community) = v2_target {
2054 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2055 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2056 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2057 .await
2058 .map_err(VectorError::Other);
2059 }
2060 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2061 let ms = std::time::SystemTime::now()
2062 .duration_since(std::time::UNIX_EPOCH)
2063 .map(|d| d.as_millis() as u64)
2064 .unwrap_or(0);
2065 let unsigned = envelope::build_inner_full(
2066 author_pk, &channel.id, channel.epoch,
2067 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2068 );
2069 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2070 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2071 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2072 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2073 .await.map_err(VectorError::Other)?;
2074 let echoed = {
2076 let mut st = state::STATE.lock().await;
2077 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2078 };
2079 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2080 let _ = crate::db::events::save_message(channel_id, &m).await;
2081 }
2082 Ok(message_id)
2083 }
2084
2085 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2087 use crate::community::{service, transport::LiveTransport};
2088 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2089 let community = crate::db::community::load_community_v2(&id)
2090 .map_err(VectorError::Other)?
2091 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2092 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2093 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2094 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2095 .await
2096 .map_err(VectorError::Other);
2097 }
2098 let (community, channel) = self.resolve_channel(channel_id)?;
2099 Self::ensure_v1_writable(&community)?;
2100 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2101 service::publish_typing_signal(&transport, &community, &channel)
2102 .await
2103 .map_err(VectorError::Other)
2104 }
2105
2106 pub async fn send_community_reaction(
2109 &self,
2110 channel_id: &str,
2111 message_id: &str,
2112 emoji: &str,
2113 emoji_url: Option<&str>,
2114 ) -> Result<()> {
2115 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2116 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2117 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2118 }
2119 _ => Vec::new(),
2120 };
2121 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2122 let session = state::SessionGuard::capture();
2123 let community = crate::db::community::load_community_v2(&id)
2124 .map_err(VectorError::Other)?
2125 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2126 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2127 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2128 let held = {
2133 let st = state::STATE.lock().await;
2134 st.find_message(message_id)
2135 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2136 };
2137 let held = held.or_else(|| {
2138 crate::db::events::event_author(message_id)
2139 .ok()
2140 .flatten()
2141 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2142 });
2143 let target_author = match held {
2144 Some(pk) => pk,
2145 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2146 .await
2147 .map_err(VectorError::Other)?
2148 .iter()
2149 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2150 .map(|f| f.event.opened().author)
2151 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2152 };
2153 if !session.is_valid() {
2155 return Err(VectorError::Other("account changed before send".into()));
2156 }
2157 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2158 return crate::community::v2::service::send_reaction(
2163 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2164 )
2165 .await
2166 .map(|_| ())
2167 .map_err(VectorError::Other);
2168 }
2169 self.publish_community_control(
2170 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2171 ).await
2172 }
2173
2174 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2176 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2177 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2178 let community = crate::db::community::load_community_v2(&id)
2179 .map_err(VectorError::Other)?
2180 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2181 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2182 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2183 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2184 .await
2185 .map(|_| ())
2186 .map_err(VectorError::Other);
2187 }
2188 self.publish_community_control(
2189 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2190 ).await
2191 }
2192
2193 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2197 let channel_id = {
2198 let st = state::STATE.lock().await;
2199 match st.find_message(message_id) {
2200 Some((chat, _)) => chat.id.clone(),
2201 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2202 }
2203 };
2204 self.delete_community_message_in(&channel_id, message_id).await
2205 }
2206
2207 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2211 use crate::community::{service, transport::LiveTransport};
2212 let session = state::SessionGuard::capture();
2213 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2214
2215 let attachment_urls: Vec<String> = {
2218 let st = state::STATE.lock().await;
2219 st.find_message(message_id)
2220 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2221 .unwrap_or_default()
2222 };
2223
2224 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2225 let community = crate::db::community::load_community_v2(&id)
2228 .map_err(VectorError::Other)?
2229 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2230 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2231 crate::community::v2::service::send_delete(
2232 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2233 )
2234 .await
2235 .map_err(VectorError::Other)?;
2236 } else {
2237 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2239 let _ = service::delete_message(&transport, message_id).await;
2240 }
2241 self.publish_community_control(
2243 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2244 ).await?;
2245 }
2246 if !attachment_urls.is_empty() {
2248 if let Some(_client) = state::nostr_client() {
2249 if let Ok(signer) = crate::signer::active_signer() {
2250 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2251 }
2252 }
2253 }
2254 if !session.is_valid() {
2257 return Ok(());
2258 }
2259 let removed_chat = {
2260 let mut st = state::STATE.lock().await;
2261 st.remove_message(message_id).map(|(cid, _)| cid)
2262 };
2263 let _ = crate::db::events::delete_event(message_id).await;
2264 traits::emit_event_json("message_removed", serde_json::json!({
2265 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2266 }));
2267 Ok(())
2268 }
2269
2270 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2276 use crate::community::transport::LiveTransport;
2277 let session = state::SessionGuard::capture();
2278 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2279
2280 let author_npub = {
2283 let st = state::STATE.lock().await;
2284 st.find_message(message_id).and_then(|(_, m)| m.npub)
2285 };
2286 let author_npub = match author_npub {
2287 Some(n) => n,
2288 None => crate::db::events::event_author(message_id)
2289 .ok()
2290 .flatten()
2291 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2292 };
2293 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2294 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2295
2296 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2297 let community = crate::db::community::load_community_v2(&id)
2298 .map_err(VectorError::Other)?
2299 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2300 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2301 crate::community::v2::service::moderation_delete(
2302 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2303 )
2304 .await
2305 .map_err(VectorError::Other)?;
2306 } else {
2307 let cid = crate::db::community::community_id_for_channel(channel_id)
2308 .map_err(VectorError::Other)?
2309 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2310 let community = crate::db::community::load_community(&crate::community::CommunityId(
2311 crate::simd::hex::hex_to_bytes_32(&cid),
2312 ))
2313 .map_err(VectorError::Other)?
2314 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2315 let channel = community
2316 .channels
2317 .iter()
2318 .find(|c| c.id.to_hex() == channel_id)
2319 .cloned()
2320 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2321 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2322 .await
2323 .map_err(VectorError::Other)?;
2324 }
2325
2326 if !session.is_valid() {
2329 return Ok(());
2330 }
2331 let removed_chat = {
2332 let mut st = state::STATE.lock().await;
2333 st.remove_message(message_id).map(|(cid, _)| cid)
2334 };
2335 let _ = crate::db::events::delete_event(message_id).await;
2336 traits::emit_event_json("message_removed", serde_json::json!({
2337 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2338 }));
2339 Ok(())
2340 }
2341
2342 async fn publish_community_control(
2345 &self,
2346 channel_id: &str,
2347 kind: u16,
2348 content: &str,
2349 target: &str,
2350 emoji_tags: &[crate::types::EmojiTag],
2351 ) -> Result<()> {
2352 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2353 let (community, channel) = self.resolve_channel(channel_id)?;
2354 Self::ensure_v1_writable(&community)?;
2355 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2356 let ms = std::time::SystemTime::now()
2357 .duration_since(std::time::UNIX_EPOCH)
2358 .map(|d| d.as_millis() as u64)
2359 .unwrap_or(0);
2360 let unsigned = envelope::build_inner_typed(
2361 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2362 );
2363 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2364 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2365 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2366 let session = state::SessionGuard::capture();
2367 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2368 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2369 .await.map_err(VectorError::Other)?;
2370 if !session.is_valid() {
2373 return Ok(());
2374 }
2375 let outcome = {
2376 let mut st = state::STATE.lock().await;
2377 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2378 };
2379 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2380 if let Some(ev) = edit_event {
2381 let mut ev = (*ev).clone();
2382 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2383 let _ = crate::db::events::save_event(&ev).await;
2384 } else {
2385 let _ = crate::db::events::save_message(channel_id, &message).await;
2386 }
2387 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2388 }
2389 Ok(())
2390 }
2391
2392 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2400 use crate::community::{send, service, transport::LiveTransport};
2401 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2402 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2407 let warnings = if community::v2::realtime::follow_worker_running() {
2408 community::v2::realtime::enqueue_follow(&id);
2409 Vec::new()
2410 } else {
2411 Self::v2_inline_follow(&id).await
2412 };
2413 let new = Self::v2_backfill_channel(
2418 &id, channel_id, limit, 8, None, None,
2419 crate::community::transport::Evidence::Fast, 12,
2420 ).await;
2421 return Ok((new, warnings));
2422 }
2423 let (community, _) = self.resolve_channel(channel_id)?;
2424 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2425 let mut warnings: Vec<String> = Vec::new();
2426
2427 match service::catch_up_server_root(&transport, &community).await {
2435 Ok(c) if c.removed => {
2436 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2438 return Ok((0, warnings));
2439 }
2440 Ok(_) => {}
2441 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2442 }
2443 let (community, _) = self.resolve_channel(channel_id)?;
2444
2445 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2451 warnings.push(format!("control fold failed: {e}"));
2452 }
2453 if service::am_i_banned(&community) {
2454 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2456 return Ok((0, warnings));
2457 }
2458 let (community, channel) = self.resolve_channel(channel_id)?;
2461 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2462 warnings.push(format!("channel catch-up failed: {e}"));
2463 }
2464 let (community, _) = self.resolve_channel(channel_id)?;
2468 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2469 warnings.push(format!("read-cut resume failed: {e}"));
2470 }
2471 let (community, channel) = self.resolve_channel(channel_id)?;
2472
2473 let session = state::SessionGuard::capture();
2475 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2476 .await
2477 .map_err(VectorError::Other)?;
2478 let new = Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await;
2479 Ok((new, warnings))
2480 }
2481
2482 async fn v1_ingest_channel_page(
2487 channel_id: &str,
2488 events: &[nostr_sdk::prelude::Event],
2489 channel: &crate::community::Channel,
2490 my_pk: nostr_sdk::prelude::PublicKey,
2491 session: &state::SessionGuard,
2492 ) -> usize {
2493 use crate::community::inbound;
2494 let outcomes = {
2495 let mut st = state::STATE.lock().await;
2496 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2497 };
2498 let mut new = 0usize;
2499 let mut pending: Vec<&crate::types::Message> = Vec::new();
2503 for o in &outcomes {
2504 if !session.is_valid() {
2506 pending.clear();
2507 break;
2508 }
2509 match o {
2510 inbound::IncomingEvent::NewMessage(m) => {
2511 pending.push(m);
2512 new += 1;
2513 }
2514 inbound::IncomingEvent::Updated { message, .. } => {
2515 pending.push(message);
2516 }
2517 inbound::IncomingEvent::Removed { target_id } => {
2518 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2519 let _ = crate::db::events::delete_event(target_id).await;
2520 }
2521 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2522 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2525 let _ = crate::db::events::delete_event(reaction_id).await;
2526 }
2527 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2528 let et = if *joined {
2529 crate::stored_event::SystemEventType::MemberJoined
2530 } else {
2531 crate::stored_event::SystemEventType::MemberLeft
2532 };
2533 let note = invited_by.as_ref().map(|by| match invited_label {
2535 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2536 _ => by.clone(),
2537 });
2538 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;
2539 }
2540 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2541 community::service::persist_webxdc_signal(
2544 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2545 ).await;
2546 }
2547 inbound::IncomingEvent::Kicked { community_id }
2548 | inbound::IncomingEvent::SelfLeft { community_id } => {
2549 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2554 let _ = crate::db::community::delete_community_retain_keys(community_id);
2555 break;
2556 }
2557 inbound::IncomingEvent::Typing { .. } => {
2558 }
2560 }
2561 }
2562 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2563 new
2564 }
2565
2566 pub async fn sync_channel_events(
2581 &self,
2582 channel_id: &str,
2583 max_events: usize,
2584 until_s: Option<u64>,
2585 since_s: Option<u64>,
2586 ) -> Result<usize> {
2587 use crate::community::{send, transport::LiveTransport};
2588 let max = max_events.clamp(1, 500);
2589 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2590 let community = crate::db::community::load_community_v2(&id)
2591 .map_err(VectorError::Other)?
2592 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2593 if community.dissolved {
2594 return Err(VectorError::Other("this community has been dissolved".into()));
2595 }
2596 let ch_id = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2597 let ch = community
2598 .channel(&ch_id)
2599 .ok_or_else(|| VectorError::Other("no such channel in this community".into()))?;
2600 if ch.private && ch.key.is_none() {
2603 return Err(VectorError::Other(
2604 "this private channel has no key yet (awaiting rekey delivery)".into(),
2605 ));
2606 }
2607 let new = Self::v2_backfill_channel(
2608 &id, channel_id, max, 1, since_s, until_s,
2609 crate::community::transport::Evidence::Fast, 12,
2610 )
2611 .await;
2612 return Ok(new);
2613 }
2614 let (community, channel) = self.resolve_channel(channel_id)?;
2615 Self::ensure_v1_writable(&community)?;
2616 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2617 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2618 let session = state::SessionGuard::capture();
2619 let events = send::fetch_channel_page(&transport, &community, &channel, until_s, since_s, max)
2620 .await
2621 .map_err(VectorError::Other)?;
2622 Ok(Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await)
2623 }
2624
2625 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2637 use crate::bot_interface::{self, ChatCommandsSnapshot};
2638 use nostr_sdk::prelude::ToBech32;
2639
2640 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2641 let mut relays: Vec<String> = Vec::new();
2642 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2643 if let Some(cid_hex) = community_hex {
2644 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2645 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2646 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2647 relays = community.relays.clone();
2648 } else {
2649 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2650 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2651 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2652 };
2653 relays = community.relays.clone();
2654 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2655 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2656 members.push(pk);
2657 }
2658 }
2659 }
2660 let state = crate::state::STATE.lock().await;
2661 for pk in members {
2662 let Ok(npub) = pk.to_bech32();
2663 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2664 bots.push(pk);
2665 }
2666 }
2667 } else if chat_id.starts_with("npub1") {
2668 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2669 let is_bot = {
2670 let state = crate::state::STATE.lock().await;
2671 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2672 };
2673 if is_bot {
2674 bots.push(pk);
2675 if let Some(client) = crate::state::nostr_client() {
2678 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2679 }
2680 }
2681 }
2682 }
2683
2684 if bots.is_empty() {
2685 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2686 }
2687 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2690 relays.sort();
2691 relays.dedup();
2692 bots.sort_by_key(|p| p.to_hex());
2695 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2696 let commands = bot_interface::assemble_from_store(&bot_hexes);
2697 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2698 if !fresh {
2699 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2700 }
2701 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2702 }
2703
2704 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2709 use nostr_sdk::prelude::ToBech32;
2710 match Self::load_v2_if_v2(community_id) {
2716 Ok(Some(community)) => {
2717 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2718 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2719 if cursor == 0 {
2720 if crate::community::v2::realtime::follow_worker_running() {
2721 crate::community::v2::realtime::enqueue_follow(community.id());
2722 } else {
2723 let session = state::SessionGuard::capture();
2724 let c2 = community.clone();
2725 tokio::spawn(async move {
2726 if !session.is_valid() {
2727 return;
2728 }
2729 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2730 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2, &session).await, Ok(fresh) if !fresh.is_empty()) {
2731 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2732 }
2733 });
2734 }
2735 }
2736 return crate::community::v2::service::stored_memberlist(&community)
2737 .unwrap_or_default()
2738 .into_iter()
2739 .filter_map(|pk| pk.to_bech32().ok())
2740 .map(|npub| serde_json::json!({ "npub": npub }))
2741 .collect();
2742 }
2743 Ok(None) => {} Err(_) => return Vec::new(),
2746 }
2747 crate::db::community::community_member_activity(community_id)
2748 .unwrap_or_default()
2749 .into_iter()
2750 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2751 .collect()
2752 }
2753
2754 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2758 use crate::community::transport::LiveTransport;
2759 let session = state::SessionGuard::capture();
2760 let lock = crate::community::v2::realtime::follow_lock(id);
2765 let _guard = lock.lock().await;
2766 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2767 let mut warnings: Vec<String> = Vec::new();
2768 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2769 warnings.push("v2 community not found".to_string());
2770 return warnings;
2771 };
2772 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2773 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2774 Ok(f) if f.dissolved => return warnings,
2776 Ok(f) if f.self_removed => {
2777 if session.is_valid() {
2780 let _ = crate::db::community::delete_community(&cid_hex);
2781 }
2782 return warnings;
2783 }
2784 Ok(_) => {}
2785 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2786 }
2787 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2788 match crate::community::v2::service::follow_control(&transport, &fresh, &session).await {
2789 Ok(Some(changed)) => {
2793 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2794 warnings.push(format!("v2 rekey follow failed: {e}"));
2795 }
2796 }
2797 Ok(None) => {}
2798 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2799 }
2800 }
2801 if let Some(me) = crate::my_public_key() {
2806 if crate::db::community::is_author_banned(&cid_hex, &me) && session.is_valid() {
2807 let _ = crate::db::community::delete_community(&cid_hex);
2808 }
2809 }
2810 warnings
2811 }
2812
2813 pub(crate) async fn v2_backfill_channel(
2825 id: &crate::community::CommunityId,
2826 channel_id: &str,
2827 limit: usize,
2828 max_pages: usize,
2829 since: Option<u64>,
2830 until: Option<u64>,
2831 evidence: crate::community::transport::Evidence,
2832 transport_secs: u64,
2833 ) -> usize {
2834 let session = state::SessionGuard::capture();
2837 let Some(my_pk) = state::my_public_key() else { return 0 };
2838 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2842 return 0;
2843 }
2844 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return 0 };
2845 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2846 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
2847 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2848 &transport,
2849 &community,
2850 &ch,
2851 limit.max(50),
2852 max_pages,
2853 since,
2854 until,
2855 evidence,
2856 |page| {
2861 let mut saw_message = false;
2862 for f in page {
2863 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2864 saw_message = true;
2865 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2866 return true;
2867 }
2868 }
2869 }
2870 !saw_message
2871 },
2872 )
2873 .await
2874 else {
2875 return 0;
2876 };
2877 Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await
2878 }
2879
2880 pub(crate) async fn v2_ingest_chat_page(
2884 channel_id: &str,
2885 my_pk: nostr_sdk::prelude::PublicKey,
2886 session: crate::state::SessionGuard,
2887 page: Vec<crate::community::v2::service::FetchedEvent>,
2888 ) -> usize {
2889 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2890 let mut new = 0usize;
2891 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
2893 for f in &page {
2894 if !session.is_valid() {
2896 break;
2897 }
2898 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2903 if opened.author != my_pk {
2904 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2905 let Ok(npub) = ToBech32::to_bech32(&opened.author);
2906 crate::community::service::persist_webxdc_signal(
2907 channel_id,
2908 &npub,
2909 &topic,
2910 addr.as_deref(),
2911 &opened.rumor_id.to_hex(),
2912 opened.at_ms / 1000,
2913 )
2914 .await;
2915 }
2916 }
2917 continue;
2918 }
2919 let outcome = {
2920 let mut st = state::STATE.lock().await;
2921 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
2922 };
2923 if let Some(outcome) = outcome {
2924 if matches!(outcome, ChatPersist::New(_)) {
2925 new += 1;
2926 }
2927 outcomes.push(outcome);
2928 }
2929 }
2930 let mut pending: Vec<&crate::types::Message> = Vec::new();
2934 for outcome in &outcomes {
2935 if !session.is_valid() {
2936 pending.clear();
2937 break;
2938 }
2939 match outcome {
2940 ChatPersist::New(m) => pending.push(m),
2941 ChatPersist::Updated { message, edit_event } => match edit_event {
2942 Some(ev) => {
2943 let mut ev = (**ev).clone();
2944 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
2947 ev.chat_id = cid;
2948 }
2949 let _ = crate::db::events::save_event(&ev).await;
2950 }
2951 None => pending.push(message),
2952 },
2953 ChatPersist::Removed(target_id) => {
2954 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2955 let _ = crate::db::events::delete_event(target_id).await;
2956 }
2957 ChatPersist::ReactionRemoved { reaction_id, message } => {
2958 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2959 let _ = crate::db::events::delete_event(reaction_id).await;
2960 pending.push(message);
2961 }
2962 }
2963 }
2964 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2965 if session.is_valid() {
2971 for outcome in &outcomes {
2972 match outcome {
2973 ChatPersist::New(msg) => crate::traits::emit_event(
2974 "message_new",
2975 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
2976 ),
2977 ChatPersist::Updated { message, .. }
2978 | ChatPersist::ReactionRemoved { message, .. } => {
2979 let mut message = message.clone();
2980 let target_id = message.id.clone();
2981 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
2982 }
2983 ChatPersist::Removed(target_id) => crate::traits::emit_event(
2984 "message_removed",
2985 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
2986 ),
2987 }
2988 }
2989 }
2990 new
2991 }
2992
2993 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
2997 if community_id.len() != 64 {
2998 return Ok(None);
2999 }
3000 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3001 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
3002 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
3003 _ => Ok(None),
3004 }
3005 }
3006
3007 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
3012 use crate::community::CommunityId;
3013 if community_id.len() != 64 {
3014 return Err(VectorError::Other("malformed community id".into()));
3015 }
3016 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
3017 .map_err(VectorError::Other)?
3018 .ok_or_else(|| VectorError::Other("community not found".into()))
3019 }
3020
3021 fn admin_role_id_of(community_id: &str) -> Result<String> {
3022 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3023 roles.roles.iter()
3026 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
3027 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_FOUNDING_MASK))
3028 .map(|r| r.role_id.clone())
3029 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
3030 }
3031
3032 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
3036 use crate::community::service;
3037 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3038 use crate::community::roles::Permissions;
3039 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
3040 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
3041 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3042 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
3045 if banned.contains(&me) && me != owner_hex {
3046 return Ok(serde_json::json!({
3047 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
3048 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
3049 "pin_messages": false,
3050 }));
3051 }
3052 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
3053 return Ok(serde_json::json!({
3054 "manage_metadata": has(Permissions::MANAGE_METADATA), "manage_channels": has(Permissions::MANAGE_CHANNELS),
3055 "create_invite": has(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has(Permissions::BAN),
3056 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has(Permissions::MANAGE_ROLES),
3057 "manage_admin_role": me == owner_hex,
3059 "pin_messages": has(Permissions::PIN_MESSAGES),
3060 }));
3061 }
3062 let community = Self::load_community_hex(community_id)?;
3063 let caps = service::caller_capabilities(&community);
3064 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
3065 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
3066 .unwrap_or(false);
3067 Ok(serde_json::json!({
3068 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
3069 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
3070 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
3071 "manage_admin_role": manage_admin_role,
3072 "pin_messages": false,
3074 }))
3075 }
3076
3077 pub async fn pin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
3080 use crate::community::{transport::LiveTransport, ChannelId};
3081 let v2 = Self::load_v2_if_v2(community_id)?
3082 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3083 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3084 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3085 crate::community::v2::service::pin_message(&transport, &v2, &ch, message_id)
3086 .await
3087 .map_err(VectorError::Other)
3088 }
3089
3090 pub async fn unpin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
3092 use crate::community::{transport::LiveTransport, ChannelId};
3093 let v2 = Self::load_v2_if_v2(community_id)?
3094 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3095 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3096 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3097 crate::community::v2::service::unpin_message(&transport, &v2, &ch, message_id)
3098 .await
3099 .map_err(VectorError::Other)
3100 }
3101
3102 pub async fn fetch_pinned_attachment(
3110 &self,
3111 community_id: &str,
3112 channel_id: &str,
3113 rumor_id: &str,
3114 ) -> Result<serde_json::Value> {
3115 use crate::community::ChannelId;
3116 let session = crate::state::SessionGuard::capture();
3117 let v2 = Self::load_v2_if_v2(community_id)?
3118 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3119 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3120 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
3121 let pin = pins
3122 .pins
3123 .iter()
3124 .find(|p| p.rumor_id == rumor_id)
3125 .ok_or_else(|| VectorError::Other("that message is not pinned".into()))?;
3126 let dir = crate::db::get_download_dir();
3127 let tag = pin
3128 .tags
3129 .iter()
3130 .find(|t| t.first().map(String::as_str) == Some("imeta"))
3131 .map(|t| nostr_sdk::prelude::Tag::custom("imeta", t[1..].to_vec()))
3132 .ok_or_else(|| VectorError::Other("this pin carries no attachment".into()))?;
3133 let attachment = crate::community::attachments::attachment_from_imeta(&tag, &dir)
3134 .ok_or_else(|| VectorError::Other("this pin's attachment metadata is malformed".into()))?;
3135
3136 let respond = |path: &std::path::Path| {
3137 serde_json::json!({
3138 "path": path.to_string_lossy(),
3139 "name": attachment.name.to_string(),
3140 "extension": attachment.extension.to_string(),
3141 })
3142 };
3143
3144 let expected = attachment.original_hash.as_deref();
3148 let path = std::path::PathBuf::from(&*attachment.path);
3149 if let Ok(bytes) = std::fs::read(&path) {
3150 match expected {
3151 Some(want) if crate::crypto::sha256_hex(&bytes) == want => return Ok(respond(&path)),
3152 None => return Ok(respond(&path)),
3153 _ => {} }
3155 }
3156
3157 let author_npub = nostr_sdk::prelude::PublicKey::from_hex(&pin.author)
3158 .ok()
3159 .and_then(|pk| nostr_sdk::prelude::ToBech32::to_bech32(&pk).ok());
3160 let bytes = self.download_attachment_from(&attachment, author_npub.as_deref()).await?;
3161 if let Some(want) = expected {
3162 if crate::crypto::sha256_hex(&bytes) != want {
3163 return Err(VectorError::Other("downloaded bytes do not match the pinned content hash".into()));
3164 }
3165 }
3166 if !session.is_valid() {
3167 return Err(VectorError::Other("account changed during the download".into()));
3168 }
3169 std::fs::write(&path, &bytes).map_err(|e| VectorError::Other(format!("could not cache the attachment: {e}")))?;
3170 Ok(respond(&path))
3171 }
3172
3173 pub fn get_channel_pins(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
3177 use crate::community::ChannelId;
3178 let Some(v2) = Self::load_v2_if_v2(community_id)? else {
3179 return Ok(serde_json::json!({ "pins": [], "sealed": false, "version": 0 }));
3182 };
3183 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3184 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
3185 serde_json::to_value(&pins).map_err(|e| VectorError::Other(e.to_string()))
3186 }
3187
3188 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
3191 use nostr_sdk::prelude::{PublicKey, ToBech32};
3192 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3193 let owner = v2.owner().map_err(VectorError::Other)?;
3194 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3195 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
3197 let admins: Vec<String> = roster.grants.iter()
3198 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
3199 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3200 .collect();
3201 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
3202 }
3203 let community = Self::load_community_hex(community_id)?;
3204 let owner = community.owner_attestation.as_ref()
3205 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
3206 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
3207 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3208 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
3209 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3210 .collect();
3211 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
3212 }
3213
3214 async fn converge_v2_authority(
3223 transport: &crate::community::transport::LiveTransport,
3224 community_id: &str,
3225 session: &crate::state::SessionGuard,
3226 ) {
3227 if !session.is_valid() {
3228 return;
3229 }
3230 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
3233 let _ = crate::community::v2::service::follow_control(transport, &fresh, session).await;
3234 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh, session).await {
3239 if !added.is_empty() && session.is_valid() {
3240 traits::emit_event_json(
3241 "community_refreshed",
3242 serde_json::json!({ "community_id": community_id }),
3243 );
3244 }
3245 }
3246 }
3247 }
3248
3249 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3251 use crate::community::{service, transport::LiveTransport};
3252 let session = crate::state::SessionGuard::capture();
3253 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3254 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3255 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3256 crate::community::v2::service::grant_admin(&transport, &v2, &member)
3257 .await
3258 .map_err(VectorError::Other)?;
3259 Self::converge_v2_authority(&transport, community_id, &session).await;
3260 return Ok(());
3261 }
3262 let community = Self::load_community_hex(community_id)?;
3263 let role_id = Self::admin_role_id_of(community_id)?;
3264 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3265 }
3266
3267 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3269 use crate::community::{service, transport::LiveTransport};
3270 let session = crate::state::SessionGuard::capture();
3271 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3272 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3273 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3274 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
3275 .await
3276 .map_err(VectorError::Other)?;
3277 Self::converge_v2_authority(&transport, community_id, &session).await;
3278 return Ok(());
3279 }
3280 let community = Self::load_community_hex(community_id)?;
3281 let role_id = Self::admin_role_id_of(community_id)?;
3282 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3283 }
3284
3285 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
3287 use crate::community::{service, transport::LiveTransport};
3288 let session = crate::state::SessionGuard::capture();
3289 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3290 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3291 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3292 crate::community::v2::service::kick_member(&transport, &v2, &pk)
3293 .await
3294 .map_err(VectorError::Other)?;
3295 if session.is_valid() {
3300 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2, &session).await {
3301 if !fresh.is_empty() {
3302 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
3303 }
3304 }
3305 }
3306 Self::converge_v2_authority(&transport, community_id, &session).await;
3307 return Ok(());
3308 }
3309 let community = Self::load_community_hex(community_id)?;
3310 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
3311 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
3312 }
3313
3314 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
3320 use crate::community::{service, transport::LiveTransport, CommunityId};
3321 let session = crate::state::SessionGuard::capture();
3322 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3323 let hex = pk.to_hex();
3324 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3325 if community_id.len() == 64 {
3329 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3330 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3331 Self::converge_v2_authority(&transport, community_id, &session).await;
3338 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
3342 list.retain(|h| h != &hex);
3343 if banned {
3344 list.push(hex.clone());
3345 }
3346 let (community, stripped_roles) = {
3353 let lock = crate::community::v2::realtime::follow_lock(&cid);
3354 let _rotation = lock.lock().await;
3355 let community = crate::db::community::load_community_v2(&cid)
3356 .map_err(VectorError::Other)?
3357 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3358 crate::community::v2::service::set_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
3359 let mut stripped_roles: Vec<String> = Vec::new();
3360 if banned {
3361 let roster = crate::db::community::get_community_roles(community_id).unwrap_or_default();
3365 stripped_roles = roster.roles_of(&hex).map(|r| r.role_id.clone()).collect();
3366 let owner = community.owner().map_err(VectorError::Other)?;
3372 let my_pk = crate::state::my_public_key()
3373 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
3374 let can_strip = my_pk == owner
3375 || roster.can_act_on_member(
3376 &my_pk.to_hex(),
3377 Some(&owner.to_hex()),
3378 &hex,
3379 crate::community::roles::Permissions::MANAGE_ROLES,
3380 );
3381 if can_strip {
3382 crate::community::v2::service::grant_roles(&transport, &community, &pk, vec![]).await.map_err(VectorError::Other)?;
3383 } else {
3384 crate::log_warn!("[Ban] grant strip skipped (no MANAGE_ROLES over the target); the banlist still silences");
3385 }
3386 }
3387 (community, stripped_roles)
3388 };
3389 if banned {
3390 if crate::community::v2::service::community_is_public(&transport, &community).await {
3398 crate::log_info!("[Ban] public community — banlist + grant strip, no refound (CORD-05 §5)");
3399 match crate::community::v2::service::sever_banned_private_reads(&transport, &community, &pk, &stripped_roles).await {
3403 Ok(0) => {}
3404 Ok(n) => crate::log_info!("[Ban] rotated {n} private channel(s) the banned member could read"),
3405 Err(e) => crate::log_warn!("[Ban] private-channel read severance incomplete: {e}"),
3406 }
3407 } else if let Err(e) = crate::community::v2::service::refound_community(&transport, &community, &[pk]).await {
3408 crate::log_warn!("[Ban] refound deferred (banlist + grant strip landed): {e}");
3418 }
3419 }
3420 Self::converge_v2_authority(&transport, community_id, &session).await;
3421 return Ok(());
3422 }
3423 }
3424 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
3426 list.retain(|h| h != &hex);
3427 if banned {
3428 list.push(hex);
3429 }
3430 let community = Self::load_community_hex(community_id)?;
3431 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
3432 }
3433
3434 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
3438 use crate::community::{service, transport::LiveTransport, CommunityId};
3439 if community_id.len() != 64 {
3440 return Err(VectorError::Other("malformed community id".into()));
3441 }
3442 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3443 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3444 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3447 let community = crate::db::community::load_community_v2(&cid)
3448 .map_err(VectorError::Other)?
3449 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3450 return crate::community::v2::service::dissolve_community(&transport, &community)
3451 .await
3452 .map_err(VectorError::Other);
3453 }
3454 let community = Self::load_community_hex(community_id)?;
3455 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
3456 }
3457
3458 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
3461 use crate::community::{service, transport::LiveTransport, CommunityId};
3462 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3463 if community_id.len() == 64 {
3468 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3469 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3470 let community = crate::db::community::load_community_v2(&cid)
3471 .map_err(VectorError::Other)?
3472 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3473 let mut meta = community.metadata();
3474 if let Some(n) = name {
3475 meta.name = n.to_string();
3476 }
3477 if let Some(d) = description {
3478 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
3479 }
3480 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
3481 .await
3482 .map_err(VectorError::Other);
3483 }
3484 }
3485 let mut community = Self::load_community_hex(community_id)?;
3486 if let Some(n) = name { community.name = n.to_string(); }
3487 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
3488 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
3489 }
3490
3491
3492
3493 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
3496 use crate::community::{transport::LiveTransport, CommunityId};
3497 if community_id.len() != 64 {
3498 return Err(VectorError::Other("malformed community id".into()));
3499 }
3500 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3501 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3503 let session = state::SessionGuard::capture();
3504 let channel_ids: Vec<String> =
3505 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
3506 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3507 crate::community::v2::service::leave_community(&transport, &v2)
3508 .await
3509 .map_err(VectorError::Other)?;
3510 if !session.is_valid() {
3511 return Err(VectorError::Other("account changed during leave".into()));
3512 }
3513 let mut st = state::STATE.lock().await;
3514 st.chats.retain(|c| !channel_ids.contains(&c.id));
3515 return Ok(());
3516 }
3517 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
3518 let channel_ids: Vec<String> = community
3519 .as_ref()
3520 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
3521 .unwrap_or_default();
3522 if let Some(ref c) = community {
3524 if let Some(primary) = c.channels.first() {
3525 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3526 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
3527 }
3528 }
3529 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
3531 {
3532 let mut st = state::STATE.lock().await;
3533 st.chats.retain(|c| !channel_ids.contains(&c.id));
3534 }
3535 Ok(())
3536 }
3537
3538 fn ensure_v1_writable(community: &crate::community::Community) -> Result<()> {
3546 if crate::db::community::get_community_dissolved(&community.id.to_hex()).unwrap_or(false) {
3547 return Err(VectorError::Other("this community has been dissolved".into()));
3548 }
3549 Ok(())
3550 }
3551
3552 fn resolve_channel(
3553 &self,
3554 channel_id: &str,
3555 ) -> Result<(crate::community::Community, crate::community::Channel)> {
3556 use crate::community::CommunityId;
3557 let community_id = crate::db::community::community_id_for_channel(channel_id)
3558 .map_err(VectorError::Other)?
3559 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
3560 if community_id.len() != 64 {
3561 return Err(VectorError::Other("malformed community id".into()));
3562 }
3563 let community = crate::db::community::load_community(&CommunityId(
3564 crate::simd::hex::hex_to_bytes_32(&community_id),
3565 ))
3566 .map_err(VectorError::Other)?
3567 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
3568 let channel = community
3569 .channels
3570 .iter()
3571 .find(|c| c.id.to_hex() == channel_id)
3572 .cloned()
3573 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
3574 Ok((community, channel))
3575 }
3576
3577
3578 pub async fn sync_dms(
3595 &self,
3596 since_days: Option<u64>,
3597 handler: &dyn InboundEventHandler,
3598 ) -> Result<(u32, u32)> {
3599 use futures_util::StreamExt;
3600 use nostr_sdk::prelude::*;
3601
3602 let client = state::nostr_client()
3603 .ok_or(VectorError::Other("Not connected".into()))?;
3604 let my_pk = state::my_public_key()
3605 .ok_or(VectorError::Other("Not logged in".into()))?;
3606
3607 let all_items = db::wrappers::load_negentropy_items().unwrap_or_default();
3609
3610 let (items, filter) = if let Some(days) = since_days {
3612 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
3613 let items: Vec<(EventId, Timestamp)> = all_items.iter()
3614 .filter(|(_, ts)| ts.as_secs() >= since_ts)
3615 .cloned()
3616 .collect();
3617 let filter = Filter::new()
3618 .pubkey(my_pk)
3619 .kind(Kind::GiftWrap)
3620 .since(Timestamp::from_secs(since_ts));
3621 (items, filter)
3622 } else {
3623 let filter = Filter::new()
3624 .pubkey(my_pk)
3625 .kind(Kind::GiftWrap);
3626 (all_items, filter)
3627 };
3628
3629 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
3630
3631 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
3633 .direction(nostr_sdk::prelude::SyncDirection::Down)
3634 .initial_timeout(std::time::Duration::from_secs(10))
3635 .dry_run();
3636
3637 let relay_map = client.relays().await;
3641 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
3642 relay_map.iter()
3643 .map(|(url, relay)| (url.clone(), relay.clone()))
3644 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
3645 drop(relay_map);
3646 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
3647 if !skipped_no_neg.is_empty() {
3648 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
3649 }
3650
3651 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
3655 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
3656 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
3657 .min(neg_outer);
3658 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
3659 for (url, relay) in &all_relays {
3660 let url = url.clone();
3661 let relay = relay.clone();
3662 let f = filter.clone();
3663 let i = items.clone();
3664 let o = sync_opts.clone();
3665 relay_futs.push(async move {
3666 if !negentropy::wait_connected(&relay, connect_allowance).await {
3667 return (url, None, false);
3668 }
3669 let result = tokio::time::timeout(
3672 neg_outer,
3673 relay.sync(f).items(i).opts(o),
3674 ).await;
3675 let connected = relay.status() == RelayStatus::Connected;
3676 (url, Some(result), connected)
3677 });
3678 }
3679
3680 let cap_session = state::SessionGuard::capture();
3682 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
3683 while let Some((url, result, connected)) = relay_futs.next().await {
3684 let Some(result) = result else {
3685 log_warn!("[SyncDMs] {} skipped: not connected", url);
3686 continue;
3687 };
3688 match result {
3689 Ok(Ok(recon)) => {
3690 let count = recon.remote.len();
3691 all_missing.extend(recon.remote);
3692 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
3693 if cap_session.is_valid() {
3694 negentropy::record_neg_support(url.as_str(), true);
3695 }
3696 }
3697 Ok(Err(e)) => {
3698 log_warn!("[SyncDMs] {} failed: {}", url, e);
3699 if cap_session.is_valid()
3700 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
3701 {
3702 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
3703 negentropy::record_neg_support(url.as_str(), false);
3704 }
3705 }
3706 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
3707 }
3708 }
3709
3710 let mut total_events = 0u32;
3711 let mut new_messages = 0u32;
3712
3713 if !skipped_no_neg.is_empty() {
3718 let req_filter = filter.clone().limit(500);
3719 match client
3720 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3721 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
3722 ))
3723 .timeout(std::time::Duration::from_secs(20))
3724 .await
3725 {
3726 Ok(stream) => {
3727 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
3728 tokio::pin!(stream);
3729 while let Some((_relay, res)) = stream.next().await {
3730 let Ok(event) = res else { continue };
3731 if !cap_session.is_valid() { break; }
3735 if !seen.insert(event.id.to_bytes()) { continue; }
3736 total_events += 1;
3737 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
3738 if event_handler::commit_prepared_event(prepared, false, handler).await {
3739 new_messages += 1;
3740 }
3741 }
3742 }
3743 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
3744 }
3745 }
3746
3747 if all_missing.is_empty() {
3748 log_info!("[SyncDMs] No missing events");
3749 return Ok((total_events, new_messages));
3750 }
3751
3752 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
3754 let ids: Vec<EventId> = all_missing.into_iter().collect();
3755 let relay_strs: Vec<String> = client.relays().await.keys()
3756 .map(|u| u.to_string()).collect();
3757
3758 const BATCH_SIZE: usize = 500;
3759
3760 for batch in ids.chunks(BATCH_SIZE) {
3761 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
3764 match client
3765 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3766 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
3767 ))
3768 .timeout(std::time::Duration::from_secs(30))
3769 .await
3770 {
3771 Ok(stream) => {
3772 let client_clone = client.clone();
3773 let prepared_stream = stream
3774 .filter_map(|(_relay, res)| async move { res.ok() })
3775 .map(move |event| {
3776 let c = client_clone.clone();
3777 tokio::spawn(async move {
3778 event_handler::prepare_event(event, &c, my_pk).await
3779 })
3780 })
3781 .buffer_unordered(8);
3782 tokio::pin!(prepared_stream);
3783
3784 while let Some(result) = prepared_stream.next().await {
3785 total_events += 1;
3786 if let Ok(prepared) = result {
3787 if event_handler::commit_prepared_event(prepared, false, handler).await {
3788 new_messages += 1;
3789 }
3790 }
3791 }
3792 }
3793 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
3794 }
3795 }
3796
3797 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
3798 Ok((total_events, new_messages))
3799 }
3800
3801 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
3810 use nostr_sdk::prelude::*;
3811 let client = state::nostr_client()
3812 .ok_or(VectorError::Other("Not connected".into()))?;
3813 let my_pk = state::my_public_key()
3814 .ok_or(VectorError::Other("Not logged in".into()))?;
3815
3816 let filter = Filter::new()
3817 .pubkey(my_pk)
3818 .kind(Kind::GiftWrap)
3819 .limit(0);
3820
3821 let output = client.subscribe(filter).await
3822 .map_err(|e| VectorError::Nostr(e.to_string()))?;
3823 Ok(output.value)
3824 }
3825
3826 pub async fn sync_communities(&self) -> Result<()> {
3837 {
3841 use crate::community::{transport::LiveTransport, v2::service as v2};
3842 let bootstrap: Vec<String> = match crate::state::nostr_client() {
3843 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
3844 None => Vec::new(),
3845 };
3846 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3847 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
3848 let joined = outcome.joined;
3851 for c in &joined {
3852 if community::v2::realtime::follow_worker_running() {
3853 community::v2::realtime::enqueue_follow(c.id());
3854 } else {
3855 let _ = Self::v2_inline_follow(c.id()).await;
3856 }
3857 }
3858 if !joined.is_empty() {
3859 if let Some(client) = crate::state::nostr_client() {
3860 community::v2::realtime::refresh_subscription(&client).await;
3861 }
3862 }
3863 }
3864 }
3865
3866 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
3867 for id in ids {
3868 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
3869 if community::v2::realtime::follow_worker_running() {
3872 community::v2::realtime::enqueue_follow(&id);
3873 } else {
3874 let _ = Self::v2_inline_follow(&id).await;
3875 }
3876 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
3883 for ch in &c.channels {
3884 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
3885 let _ = Self::v2_backfill_channel(
3886 &id, &hex, 50, 2, None, None,
3887 crate::community::transport::Evidence::Fast, 12,
3888 ).await;
3889 }
3890 }
3891 continue;
3892 }
3893 if let Ok(Some(community)) = db::community::load_community(&id) {
3894 for ch in &community.channels {
3895 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
3896 }
3897 }
3898 }
3899 Ok(())
3900 }
3901
3902
3903 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
3935 use nostr_sdk::prelude::*;
3936
3937 let client = state::nostr_client()
3938 .ok_or(VectorError::Other("Not connected".into()))?;
3939 let my_pk = state::my_public_key()
3940 .ok_or(VectorError::Other("Not logged in".into()))?;
3941
3942 community::v2::streamauth::ensure_responder(&client);
3949
3950 community::v2::realtime::spawn_follow_worker(handler.clone());
3959 let _ = self.sync_communities().await;
3960 let _ = self.sync_dms(None, &NoOpEventHandler).await;
3961
3962 let dm_sub_id = self.subscribe_dms().await?;
3965 community::realtime::refresh_subscription(&client).await;
3966 community::v2::realtime::refresh_subscription(&client).await;
3967
3968 handler.on_subscription_ready(db::community::list_community_ids().map(|v| v.len()).unwrap_or(0));
3974
3975 if let Some(monitor) = client.monitor() {
3982 let mut rx = monitor.subscribe();
3983 let session = state::SessionGuard::capture();
3984 tokio::spawn(async move {
3985 let mut last_resync: Option<std::time::Instant> = None;
3988 while let Ok(notification) = rx.recv().await {
3989 if !session.is_valid() {
3990 return;
3991 }
3992 let MonitorNotification::StatusChanged { status, .. } = notification;
3993 if status == RelayStatus::Connected {
3994 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
3995 continue;
3996 }
3997 let _ = VectorCore.sync_communities().await;
3998 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
3999 if let Some(c) = state::nostr_client() {
4000 community::realtime::refresh_subscription(&c).await;
4001 community::v2::realtime::refresh_subscription(&c).await;
4002 }
4003 last_resync = Some(std::time::Instant::now());
4004 }
4005 }
4006 });
4007 }
4008
4009 {
4013 let client_health = client.clone();
4014 let session = state::SessionGuard::capture();
4015 tokio::spawn(async move {
4016 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
4018 if !session.is_valid() {
4019 return;
4020 }
4021 for (url, relay) in client_health.relays().await {
4022 match relay.status() {
4023 RelayStatus::Connected => {
4024 let probe = tokio::time::timeout(
4025 std::time::Duration::from_secs(10),
4026 client_health
4027 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
4028 url.to_string(),
4029 [Filter::new().kind(Kind::Metadata).limit(1)],
4030 ))
4031 .timeout(std::time::Duration::from_secs(8)),
4032 )
4033 .await;
4034 if !matches!(probe, Ok(Ok(_))) {
4035 let _ = relay.disconnect();
4036 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
4037 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
4038 }
4039 }
4040 RelayStatus::Terminated | RelayStatus::Disconnected => {
4041 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
4042 }
4043 _ => {}
4044 }
4045 }
4046 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
4047 }
4048 });
4049 }
4050
4051 let client_for_closure = client.clone();
4052
4053 let mut notifications = client.notifications();
4056 while let Some(notification) = notifications.next().await {
4057 let handler = handler.clone();
4058 let c = client_for_closure.clone();
4059 let dm_sid = dm_sub_id.clone();
4060 {
4061 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
4065 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
4066 sending::note_relay_ok(event_id, *status);
4067 }
4068 }
4069 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
4070 if subscription_id == dm_sid {
4071 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
4073 event_handler::commit_prepared_event(prepared, true, &*handler).await;
4074 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
4075 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
4076 {
4077 let session = state::SessionGuard::capture();
4081 community::realtime::dispatch_event(&session, *event, handler.clone()).await;
4082 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
4083 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
4084 {
4085 let session = state::SessionGuard::capture();
4087 community::v2::realtime::dispatch_event(&session, *event, handler.clone()).await;
4088 }
4089 }
4090 }
4091 }
4092
4093 Ok(())
4094 }
4095
4096 pub async fn logout(&self) {
4098 if let Some(client) = state::nostr_client() {
4099 let _ = client.disconnect().await;
4100 }
4101 db::close_database();
4102 }
4103
4104 pub async fn swap_session(&self) {
4112 state::bump_session_generation();
4114
4115 if let Some(client) = state::take_nostr_client() {
4118 let _ = client.shutdown().await;
4119 }
4120 db::close_database();
4121
4122 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
4124 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
4125 {
4126 use zeroize::Zeroize;
4127 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
4128 if let Some(s) = g.as_mut() { s.zeroize(); }
4129 *g = None;
4130 }
4131 if let Ok(mut g) = state::PENDING_NSEC.lock() {
4132 if let Some(s) = g.as_mut() { s.zeroize(); }
4133 *g = None;
4134 }
4135 }
4136
4137 {
4139 let mut st = state::STATE.lock().await;
4140 st.profiles.clear();
4141 st.chats.clear();
4142 st.db_loaded = false;
4143 st.is_syncing = false;
4144 }
4145 state::WRAPPER_ID_CACHE.lock().await.clear();
4146 state::PENDING_EVENTS.lock().await.clear();
4147 state::set_active_chat(None);
4148 crate::profile::sync::clear_profile_sync_queue();
4149 crate::inbox_relays::clear_inbox_relay_cache();
4150 crate::sending::clear_wrap_confirms();
4153 crate::emoji_packs::clear_nip65_cache();
4154 crate::db::clear_id_caches();
4158 crate::community::cache::clear();
4162 crate::community::realtime::clear().await;
4165 crate::community::v2::realtime::clear().await;
4166 crate::community::transport::clear_plane_pool();
4168 crate::emoji_packs::set_theme_emoji_tags(Vec::new());
4172 }
4173}
4174
4175#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
4176mod transport_policy_tests {
4177 use std::time::Duration;
4178
4179 #[test]
4182 fn tor_transport_policy() {
4183 let short = Duration::from_secs(5);
4184 let long = Duration::from_secs(300);
4185
4186 crate::tor::set_tor_enabled_pref(false);
4189 assert_eq!(super::tor_proxy_target(), None);
4190 assert_eq!(super::relay_connect_timeout(short), short);
4191 assert_eq!(super::relay_request_timeout(short), short);
4192
4193 crate::tor::set_tor_enabled_pref(true);
4197 assert!(matches!(
4198 crate::tor::transport_state(),
4199 crate::tor::TorTransportState::RequiredButInactive
4200 ));
4201 assert_eq!(
4207 super::tor_proxy_target(),
4208 Some(crate::tor::blackhole_proxy_addr()),
4209 "Tor enabled but inactive must blackhole, never connect direct"
4210 );
4211 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
4212 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
4213
4214 for tor in [true, false] {
4217 crate::tor::set_tor_enabled_pref(tor);
4218 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
4219 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
4220 }
4221 }
4222}
4223
4224#[cfg(test)]
4225mod facade_tests {
4226 use super::*;
4227
4228 #[tokio::test]
4231 async fn download_attachment_rejects_private_url() {
4232 let att = crate::types::Attachment {
4233 url: "http://169.254.169.254/latest/meta-data/".to_string(),
4234 ..Default::default()
4235 };
4236 match VectorCore.download_attachment(&att).await {
4237 Err(VectorError::Other(msg)) => {
4238 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
4239 }
4240 other => panic!("expected SSRF rejection, got {other:?}"),
4241 }
4242 }
4243
4244 #[tokio::test]
4245 async fn download_attachment_rejects_empty_url() {
4246 let att = crate::types::Attachment::default();
4247 assert!(VectorCore.download_attachment(&att).await.is_err());
4248 }
4249
4250 #[tokio::test]
4254 async fn list_communities_and_channel_routing_are_protocol_aware() {
4255 use crate::community::transport::memory::MemoryRelay;
4256 use nostr_sdk::prelude::Keys;
4257
4258 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
4259 crate::db::close_database();
4260 crate::db::clear_id_caches();
4261 let tmp = tempfile::tempdir().unwrap();
4262 let acct = {
4264 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
4265 let mut s = String::from("npub1");
4266 for i in 0..58 {
4267 s.push(B[(i * 7 + 3) % 32] as char);
4268 }
4269 s
4270 };
4271 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
4272 crate::db::set_app_data_dir(tmp.path().to_path_buf());
4273 crate::db::set_current_account(acct.clone()).unwrap();
4274 crate::db::init_database(&acct).unwrap();
4275 let _ = crate::state::take_nostr_client();
4276 let me = Keys::generate();
4277 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
4278 crate::state::set_my_public_key(me.public_key());
4279
4280 let relay = MemoryRelay::new();
4282 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
4283 .await
4284 .unwrap();
4285 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
4286
4287 let listed = VectorCore.list_communities().await;
4289 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
4290 assert_eq!(v2["name"], "V2 Guild");
4291 assert_eq!(v2["is_owner"], true);
4292 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
4293
4294 assert_eq!(
4296 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
4297 Some(community.identity.community_id),
4298 "a v2 channel is routed to v2"
4299 );
4300 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
4302 }
4303
4304 #[test]
4309 fn v2_invite_url_base_derivation_round_trips() {
4310 use crate::community::v2::derive::TOKEN_LEN;
4311 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
4312 use nostr_sdk::prelude::Keys;
4313 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
4314 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
4315 let signer = Keys::generate();
4316 let token = [0x07u8; TOKEN_LEN];
4317 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
4318 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
4319 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
4320 let parsed = parse_invite_link(&url).unwrap();
4321 assert_eq!(parsed.link_signer, signer.public_key());
4322 assert_eq!(parsed.token, token);
4323 }
4324}
4325
4326#[cfg(test)]
4327mod history_paging_tests {
4328 use super::*;
4329
4330 fn msg(at: u64, id_byte: u8, content: &str) -> Message {
4331 Message {
4332 id: format!("{:02x}", id_byte).repeat(32),
4333 content: content.to_string(),
4334 at,
4335 ..Default::default()
4336 }
4337 }
4338
4339 #[tokio::test]
4343 async fn history_pages_through_a_same_ms_wall_and_a_deleted_cursor() {
4344 let chat_id = "test-history-paging-wall";
4345 {
4346 let mut st = state::STATE.lock().await;
4347 st.ensure_community_chat(chat_id);
4348 for m in [msg(500, 0x01, "old"), msg(900, 0xaa, "wall-a"), msg(900, 0xbb, "wall-b"), msg(900, 0xcc, "wall-c")] {
4350 st.add_message_to_chat(chat_id, &m);
4351 }
4352 }
4353 let core = VectorCore;
4354
4355 let newest = core.get_messages_before(chat_id, None, 2).await;
4357 assert_eq!(newest.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["wall-b", "wall-c"]);
4358
4359 let cursor = (newest[0].at, newest[0].id.as_str().to_string());
4361 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
4362 assert_eq!(page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["old", "wall-a"]);
4363
4364 {
4367 let mut st = state::STATE.lock().await;
4368 let chat = st.get_chat_mut(chat_id).unwrap();
4369 chat.messages.remove_by_hex_id(&cursor.1);
4370 }
4371 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
4372 assert_eq!(
4373 page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(),
4374 ["old", "wall-a"],
4375 "a deleted cursor pages identically"
4376 );
4377
4378 assert!(core.get_messages_before("test-history-paging-nochat", None, 5).await.is_empty());
4380 }
4381}