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;
66pub mod spawn_audit;
69
70pub mod net;
72pub mod negentropy;
73pub mod blossom;
74pub mod blossom_servers;
75pub mod blossom_capabilities;
76pub mod inbox_relays;
77pub mod emoji_packs;
78pub mod emoji_usage;
79pub mod badges;
80pub mod bot_interface;
81pub mod webxdc;
82#[cfg(feature = "tor")]
83pub mod tor;
84
85#[derive(Debug)]
97pub struct VectorAuthenticator;
98
99impl nostr_sdk::prelude::Authenticator for VectorAuthenticator {
100 fn make_auth_event<'a>(
101 &'a self,
102 relay_url: &'a nostr_sdk::prelude::RelayUrl,
103 challenge: &'a str,
104 ) -> signer::BoxedFuture<'a, std::result::Result<nostr_sdk::prelude::Event, nostr_sdk::prelude::Error>>
105 {
106 Box::pin(async move {
107 let signer =
108 signer::active_signer().map_err(nostr_sdk::prelude::Error::other)?;
109 Ok(
110 nostr_sdk::prelude::ClientAuthentication::new(challenge, relay_url.clone())
111 .finalize_async(&signer)
112 .await?,
113 )
114 })
115 }
116}
117
118pub fn nostr_client_builder() -> nostr_sdk::prelude::ClientBuilder {
129 apply_tor_proxy(
130 nostr_sdk::prelude::ClientBuilder::new()
131 .authenticator(VectorAuthenticator)
132 .connect_timeout(relay_connect_timeout(std::time::Duration::from_secs(15))),
135 )
136}
137
138pub trait ClientRelayExt {
150 fn add_managed_relay<'client, 'url, U>(
152 &'client self,
153 url: U,
154 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
155 where
156 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>;
157}
158
159impl ClientRelayExt for nostr_sdk::prelude::Client {
160 fn add_managed_relay<'client, 'url, U>(
161 &'client self,
162 url: U,
163 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
164 where
165 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>,
166 {
167 self.add_relay(url).reconnect(false)
168 }
169}
170
171pub async fn resubscribe_relay_after_reconnect(
186 client: &nostr_sdk::prelude::Client,
187 relay: &nostr_sdk::prelude::RelayUrl,
188) {
189 for (id, per_relay) in client.subscriptions().await {
190 let Some(filters) = per_relay.get(relay) else { continue };
191 if filters.is_empty() {
192 continue;
193 }
194 let _ = client
195 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), filters.clone()))
196 .with_id(id)
197 .await;
198 }
199}
200
201#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
206const TOR_RELAY_CONNECT_FLOOR: std::time::Duration = std::time::Duration::from_secs(60);
207
208pub fn relay_connect_timeout(clearnet: std::time::Duration) -> std::time::Duration {
217 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
218 {
219 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
220 return clearnet.max(TOR_RELAY_CONNECT_FLOOR);
221 }
222 }
223 clearnet
224}
225
226#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
228const TOR_RELAY_REQUEST_FLOOR: std::time::Duration = std::time::Duration::from_secs(30);
229
230pub fn relay_request_timeout(clearnet: std::time::Duration) -> std::time::Duration {
236 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
237 {
238 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
239 return clearnet.max(TOR_RELAY_REQUEST_FLOOR);
240 }
241 }
242 clearnet
243}
244
245pub fn apply_tor_proxy(
252 builder: nostr_sdk::prelude::ClientBuilder,
253) -> nostr_sdk::prelude::ClientBuilder {
254 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
255 let builder = builder.proxy(nostr_sdk::prelude::Proxy::custom(|_url| tor_proxy_target()));
256 builder
257}
258
259#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
265fn tor_proxy_target() -> Option<std::net::SocketAddr> {
266 match tor::transport_state() {
267 tor::TorTransportState::Active(addr) => Some(addr),
268 tor::TorTransportState::RequiredButInactive => Some(tor::blackhole_proxy_addr()),
271 tor::TorTransportState::Disabled => None,
272 }
273}
274
275pub async fn sign_builder(
280 builder: nostr_sdk::prelude::EventBuilder,
281) -> std::result::Result<nostr_sdk::prelude::Event, String> {
282 let signer = signer::active_signer()?;
283 builder
284 .finalize_async(&signer)
285 .await
286 .map_err(|e| e.to_string())
287}
288
289pub async fn sign_and_send(
293 client: &nostr_sdk::prelude::Client,
294 builder: nostr_sdk::prelude::EventBuilder,
295) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String> {
296 let event = sign_builder(builder).await?;
297 client
298 .send_event(&event)
299 .await
300 .map_err(|e| e.to_string())
301}
302
303pub async fn send_gift_wrap<'u, I, U, T>(
309 client: &nostr_sdk::prelude::Client,
310 relays: I,
311 receiver: &nostr_sdk::prelude::PublicKey,
312 rumor: nostr_sdk::prelude::UnsignedEvent,
313 extra_tags: T,
314) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String>
315where
316 I: IntoIterator<Item = U>,
317 U: Into<nostr_sdk::prelude::RelayUrlArg<'u>>,
318 T: IntoIterator<Item = nostr_sdk::prelude::Tag>,
319{
320 let signer = signer::active_signer()?;
321 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(*receiver, rumor)
322 .extra_tags(extra_tags)
323 .finalize_async(&signer)
324 .await
325 .map_err(|e| e.to_string())?;
326 let targets: Vec<nostr_sdk::prelude::RelayUrlArg<'u>> =
327 relays.into_iter().map(Into::into).collect();
328 if targets.is_empty() {
329 client.send_event(&wrap).await.map_err(|e| e.to_string())
330 } else {
331 client
332 .send_event(&wrap)
333 .to(targets)
334 .await
335 .map_err(|e| e.to_string())
336 }
337}
338
339pub fn community_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
352 nostr_sdk::prelude::RelayCapabilities::GOSSIP
353}
354
355pub fn discovery_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
361 community_relay_capabilities()
362}
363
364pub mod stored_event;
366
367pub mod rumor;
369
370pub mod sending;
372
373pub mod pinned_chats;
375pub mod synced_prefs;
376pub mod wallpaper;
377
378pub mod deletion;
380pub mod self_destruct;
381
382pub mod simd;
384
385pub mod community;
387
388pub mod event_handler;
390
391pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
393pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
394pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
395pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
396pub use state::{
397 ChatState, MY_SECRET_KEY, STATE, ENCRYPTION_KEY,
398 nostr_client, my_public_key, has_active_session,
399 set_nostr_client, set_my_public_key,
400 take_nostr_client, clear_my_public_key,
401 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
402 set_pending_nip55_setup, pending_nip55_setup, clear_pending_nip55_setup,
403};
404pub use crypto::{GuardedKey, GuardedSigner};
405pub use signer::{
406 SignerKind, signer_kind, set_signer_kind, is_bunker, is_keyless,
407 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
408 build_bunker_signer, prewarm_bunker, drain_bunker_state,
409 parse_bunker_remote_pubkey, parse_bunker_relays,
410 BunkerConnectionState, bunker_state, set_bunker_state,
411 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
412 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
413 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
414};
415pub use nip55::{
416 Nip55Backend, Nip55Error, Nip55ResolverOutcome, Nip55Signer, Nip55State,
417 set_nip55_backend, nip55_backend, nip55_state, set_nip55_state, drain_nip55_state,
418 nip55_is_installed, nip55_pair, nip55_perms_json,
419 VECTOR_NIP55_SIGN_KINDS, VECTOR_NIP55_ENCRYPT_TYPES,
420};
421pub use error::{VectorError, Result};
422pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
423pub use db::{set_app_data_dir, get_app_data_dir};
424pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
425pub use deletion::{delete_own_dm, DeleteOutcome};
426pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
427pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
428pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
429pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
430
431use std::path::PathBuf;
432use std::sync::Arc;
433
434pub struct CoreConfig {
440 pub data_dir: PathBuf,
442 pub event_emitter: Option<Box<dyn EventEmitter>>,
444}
445
446#[derive(Clone, Copy)]
468pub struct VectorCore;
469
470#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
477pub struct BackfillCount {
478 pub fetched: usize,
479 pub new_messages: usize,
480}
481
482impl VectorCore {
483 pub fn init(config: CoreConfig) -> Result<Self> {
485 db::set_app_data_dir(config.data_dir);
487
488 if let Some(emitter) = config.event_emitter {
490 traits::set_event_emitter(emitter);
491 }
492
493 let _ = rustls::crypto::ring::default_provider().install_default();
495
496 Ok(VectorCore)
497 }
498
499 pub fn accounts(&self) -> Result<Vec<String>> {
501 db::get_accounts().map_err(VectorError::from)
502 }
503
504 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
506 use nostr_sdk::prelude::*;
507
508 let keys = if key.starts_with("nsec1") {
510 let secret = SecretKey::from_bech32(key)
511 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
512 Keys::new(secret)
513 } else {
514 Keys::from_mnemonic(key, None)
516 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
517 };
518
519 let public_key = keys.public_key();
520 let npub = public_key.to_bech32()
521 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
522
523 let secret_bytes = keys.secret_key().to_secret_bytes();
525 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
526 state::set_my_public_key(public_key);
527
528 db::set_current_account(npub.clone())?;
530 db::init_database(&npub)?;
531
532 {
534 let nsec = keys.secret_key().to_bech32()
535 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
536 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
537
538 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
545 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
546 db::set_pkey(&nsec)?;
547 }
548 }
549
550 let has_encryption = state::resolve_encryption_enabled_from_db();
553
554 if has_encryption {
555 if let Some(pwd) = password {
556 let key = crate::crypto::hash_pass(pwd).await;
557 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
558 }
559 }
560 state::init_encryption_enabled();
563
564 let client = crate::nostr_client_builder()
567 .monitor(Monitor::new(1024))
569 .build();
570
571 for relay in state::TRUSTED_RELAYS {
573 client.add_managed_relay(*relay).await.ok();
574 }
575
576 client.connect().await;
578
579 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
580
581 Ok(LoginResult { npub, has_encryption })
582 }
583
584 pub fn generate_nsec(&self) -> Result<String> {
587 use nostr_sdk::prelude::*;
588 Keys::generate().secret_key().to_bech32()
589 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
590 }
591
592 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
597 let config = SendConfig { self_send: false, ..SendConfig::headless() };
598 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
599 .map_err(|e| VectorError::Other(e))
600 }
601
602 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
604 let config = SendConfig { self_send: false, ..SendConfig::headless() };
605 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
606 .map_err(|e| VectorError::Other(e))
607 }
608
609 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
616 self.download_attachment_from(attachment, None).await
617 }
618
619 pub async fn download_attachment_from(
624 &self,
625 attachment: &Attachment,
626 author_npub: Option<&str>,
627 ) -> Result<Vec<u8>> {
628 use futures_util::StreamExt;
629 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
630 if attachment.url.is_empty() {
631 return Err(VectorError::Other("attachment has no URL".into()));
632 }
633 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
634 let mut last_err = String::from("download failed");
635 let mut candidates: Vec<String> = vec![attachment.url.clone()];
636 candidates.extend(attachment.fallback_urls.iter().cloned());
637 let mut hash_swap_tried = false;
638 let mut i = 0;
639 'sources: while i < candidates.len() {
640 let url = candidates[i].clone();
641 i += 1;
642 let extend_with_swap = |candidates: &mut Vec<String>, servers: &[String]| {
645 let extra = crate::blossom::hash_swap_candidates(&attachment.url, servers);
646 for c in extra {
647 if !candidates.contains(&c) {
648 candidates.push(c);
649 }
650 }
651 };
652 macro_rules! next_source {
653 () => {{
654 log_net_fail!("[Download] source failed ({}): {}", url, last_err);
655 if i == candidates.len() && !hash_swap_tried {
656 hash_swap_tried = true;
657 let servers = crate::blossom_servers::author_swap_servers(author_npub, false).await;
658 extend_with_swap(&mut candidates, &servers);
659 }
660 continue 'sources;
661 }};
662 }
663 if let Err(e) = crate::net::validate_url_not_private(&url) {
667 last_err = e.to_string();
668 next_source!();
669 }
670 let resp = match client.get(&url).send().await {
671 Ok(r) => r,
672 Err(e) => {
673 last_err = format!("download: {e}");
674 next_source!();
675 }
676 };
677 if !resp.status().is_success() {
678 last_err = format!("download failed: HTTP {}", resp.status());
679 next_source!();
680 }
681 let mut encrypted: Vec<u8> = Vec::with_capacity(
684 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
685 );
686 let mut stream = resp.bytes_stream();
687 while let Some(chunk) = stream.next().await {
688 let chunk = match chunk {
689 Ok(c) => c,
690 Err(e) => {
691 last_err = format!("read body: {e}");
692 next_source!();
693 }
694 };
695 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
696 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
697 }
698 encrypted.extend_from_slice(&chunk);
699 }
700 match crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce) {
701 Ok(plain) => {
702 if i > 1 {
703 log_net_info!("[Download] fallback source {}/{} served {}", i, candidates.len(), url);
704 }
705 return Ok(plain);
706 }
707 Err(e) => {
708 last_err = format!("decrypt: {e}");
711 next_source!();
712 }
713 }
714 }
715 log_net_fail!("[Download] all {} source(s) failed for {}: {}", candidates.len(), attachment.url, last_err);
716 Err(VectorError::Other(last_err))
717 }
718
719 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
721 let path = std::path::Path::new(file_path);
722 let bytes = std::fs::read(path)
723 .map_err(|e| VectorError::Io(e))?;
724 let filename = path.file_name()
725 .and_then(|n| n.to_str())
726 .unwrap_or("file");
727 let extension = path.extension()
728 .and_then(|e| e.to_str())
729 .unwrap_or("bin");
730
731 sending::send_file_dm(
732 to_npub,
733 std::sync::Arc::new(bytes),
734 filename,
735 extension,
736 None,
737 &SendConfig::default(),
738 Arc::new(NoOpSendCallback),
739 ).await.map_err(|e| VectorError::Other(e))
740 }
741
742 async fn own_reaction_id(message_id: &str, emoji: &str) -> Option<String> {
747 use nostr_sdk::prelude::ToBech32;
748 let me = state::my_public_key()?.to_bech32().ok()?;
749 let st = state::STATE.lock().await;
750 let (_, message) = st.find_message(message_id)?;
751 message
752 .reactions
753 .iter()
754 .find(|r| r.author_id == me && r.emoji == emoji)
755 .map(|r| r.id.clone())
756 }
757
758 pub async fn send_reaction(
763 &self,
764 to_npub: &str,
765 reference_id: &str,
766 emoji: &str,
767 emoji_url: Option<&str>,
768 ) -> Result<String> {
769 use nostr_sdk::prelude::*;
770
771 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
772 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
773
774 if let Some(existing) = Self::own_reaction_id(reference_id, emoji).await {
776 return Ok(existing);
777 }
778
779 badges::check_new_reaction_allowance(reference_id, emoji)
781 .await
782 .map_err(VectorError::Other)?;
783
784 let reference_event = EventId::from_hex(reference_id)
785 .map_err(|e| VectorError::Nostr(e.to_string()))?;
786 let receiver_pubkey = PublicKey::from_bech32(to_npub)
787 .map_err(|e| VectorError::Nostr(e.to_string()))?;
788
789 let custom_emoji_tag = emoji_url.and_then(|url| {
791 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
792 return None;
793 }
794 let shortcode = &emoji[1..emoji.len() - 1];
795 if shortcode.is_empty() { return None; }
796 Some(Tag::custom("emoji", [shortcode.to_string(), url.to_string()]))
797 });
798
799 let reaction_target = nostr_sdk::prelude::nip25::ReactionTarget {
800 event_id: reference_event,
801 public_key: receiver_pubkey,
802 coordinate: None,
803 kind: Some(Kind::PrivateDirectMessage),
804 relay_hint: None,
805 };
806 let mut builder =
807 nostr_sdk::prelude::nip25::ReactionBuilder::new(reaction_target, emoji)
808 .into_event_builder();
809 if let Some(tag) = custom_emoji_tag {
810 builder = builder.tag(tag);
811 }
812 let rumor = builder.finalize_unsigned_with_id(my_public_key);
813 let inner_rumor_id = rumor.id;
814 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
815
816 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
820 .await.map_err(VectorError::Other)?;
821 if !outcome.output.success.is_empty() {
822 if let Some(rid) = inner_rumor_id {
823 if let Err(e) = db::nip17_keys::store_wrap_key(
824 &outcome.wrap_event_id, &rid, &receiver_pubkey,
825 db::nip17_keys::WrapRole::Recipient,
826 &outcome.wrap_secret, &outcome.targeted_relays,
827 ) {
828 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
829 }
830 }
831 }
832
833 let self_wrap_client = client.clone();
836 db::spawn_bound(async move {
837 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
838 &self_wrap_client, &my_public_key, rumor, [],
839 ).await {
840 if !self_outcome.output.success.is_empty() {
841 if let Some(rid) = inner_rumor_id {
842 let _ = db::nip17_keys::store_wrap_key(
843 &self_outcome.wrap_event_id, &rid, &my_public_key,
844 db::nip17_keys::WrapRole::SelfSend,
845 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
846 );
847 }
848 }
849 }
850 });
851
852 let reaction = Reaction {
854 id: rumor_id.clone(),
855 reference_id: reference_id.to_string(),
856 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
857 emoji: emoji.to_string(),
858 emoji_url: emoji_url.map(|s| s.to_string()),
859 };
860 let msg_for_save = {
861 let mut st = state::STATE.lock().await;
862 match st.add_reaction_to_message(reference_id, reaction) {
863 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
864 _ => None,
865 }
866 };
867 if let Some((cid, mut msg)) = msg_for_save {
868 let _ = db::events::save_message(&cid, &msg).await;
869 traits::emit_message_update(&cid, reference_id, &mut msg).await;
870 }
871
872 Ok(rumor_id)
873 }
874
875 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
878 use nostr_sdk::prelude::*;
879
880 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
881 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
882 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
883
884 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
885 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
886 .tag(Tag::public_key(pubkey))
887 .tag(Tag::custom("d", vec!["vector"]))
888 .tag(Tag::expiration(expiry))
889 .finalize_unsigned_with_id(my_public_key);
890
891 let signer = signer::active_signer().map_err(VectorError::Other)?;
893 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(pubkey, rumor.clone())
894 .extra_tags([Tag::expiration(expiry)])
895 .finalize_async(&signer)
896 .await
897 .map_err(|e| VectorError::Nostr(e.to_string()))?;
898 client
899 .send_event(&wrap)
900 .to(state::active_trusted_relays().await)
901 .await
902 .map_err(|e| VectorError::Nostr(e.to_string()))?;
903 Ok(())
904 }
905
906 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
910 crate::db::scoped(async move {
911 use nostr_sdk::prelude::*;
912
913 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
914 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
915 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
916 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
917 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
918
919 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
921
922 let mut builder = EventBuilder::new(
923 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
924 new_content,
925 ).tag(Tag::event(reference_event));
926 for et in &emoji_tags {
927 builder = builder.tag(Tag::custom(
928 "emoji",
929 [et.shortcode.clone(), et.url.clone()],
930 ));
931 }
932 let rumor = builder.finalize_unsigned_with_id(my_public_key);
933 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
934 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
935
936 let msg_for_emit = {
938 let mut st = state::STATE.lock().await;
939 st.update_message_in_chat(to_npub, message_id, |msg| {
940 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
941 msg.preview_metadata = None;
942 })
943 };
944 if let Some(mut msg) = msg_for_emit {
945 traits::emit_message_update(to_npub, message_id, &mut msg).await;
946 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
947 let _ = db::events::save_edit_event(
948 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
949 ).await;
950 }
951 }
952
953 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
954 .await.map_err(VectorError::Other)?;
955
956 let self_wrap_client = client.clone();
957 let self_wrap_session = crate::db::current_session();
958 db::spawn_bound(async move {
959 if !self_wrap_session.is_live() { return; }
960 let Ok(signer) = signer::active_signer() else { return };
961 if let Ok(wrap) = nostr_sdk::prelude::GiftWrapBuilder::new(my_public_key, rumor)
962 .finalize_async(&signer)
963 .await
964 {
965 let _ = self_wrap_client.send_event(&wrap).await;
966 }
967 });
968
969 Ok(edit_id)
970 })
971 .await
972 }
973
974 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
976 use nostr_sdk::prelude::*;
977 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
978 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
979 }
980
981 pub async fn get_chats(&self) -> Vec<SerializableChat> {
983 let state = state::STATE.lock().await;
984 state.chats.iter()
985 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
986 .collect()
987 }
988
989 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
991 let state = state::STATE.lock().await;
992 if let Some(chat) = state.get_chat(chat_id) {
993 let msgs = chat.get_all_messages(&state.interner);
994 let start = offset.min(msgs.len());
995 let end = (offset + limit).min(msgs.len());
996 msgs[start..end].to_vec()
997 } else {
998 Vec::new()
999 }
1000 }
1001
1002 pub async fn get_messages_before(
1012 &self,
1013 chat_id: &str,
1014 before: Option<(u64, &str)>,
1015 limit: usize,
1016 ) -> Vec<Message> {
1017 let state = state::STATE.lock().await;
1018 let Some(chat) = state.get_chat(chat_id) else {
1019 return Vec::new();
1020 };
1021 let mut msgs = chat.get_all_messages(&state.interner);
1022 if let Some((at, id)) = before {
1023 msgs.retain(|m| (m.at, m.id.as_str()) < (at, id));
1024 }
1025 msgs.sort_by(|a, b| (a.at, a.id.as_str()).cmp(&(b.at, b.id.as_str())));
1026 if msgs.len() > limit {
1027 msgs.drain(..msgs.len() - limit);
1028 }
1029 msgs
1030 }
1031
1032 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
1034 let state = state::STATE.lock().await;
1035 state.get_profile(npub)
1036 .map(|p| SlimProfile::from_profile(p, &state.interner))
1037 }
1038
1039 pub async fn load_profile(&self, npub: &str) -> bool {
1041 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
1042 }
1043
1044 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1046 profile::sync::update_profile(
1047 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1048 &NoOpProfileSyncHandler,
1049 ).await
1050 }
1051
1052 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1055 profile::sync::update_bot_profile(
1056 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1057 &NoOpProfileSyncHandler,
1058 ).await
1059 }
1060
1061 pub async fn update_status(&self, status: &str) -> bool {
1063 profile::sync::update_status(status.to_string()).await
1064 }
1065
1066 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
1072 let path = std::path::Path::new(file_path);
1073 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1074 if bytes.is_empty() {
1075 return Err(VectorError::Other("Empty image file".into()));
1076 }
1077 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1078 let mime = crate::crypto::mime_from_extension(&extension);
1079 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1080 let signer = crate::signer::active_signer()
1081 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1082 let servers = crate::blossom_servers::compute_enabled_servers();
1083 if servers.is_empty() {
1084 return Err(VectorError::Other("No Blossom servers configured".into()));
1085 }
1086 crate::blossom::upload_blob_with_failover(
1089 signer,
1090 servers,
1091 std::sync::Arc::new(bytes),
1092 Some(mime),
1093 Some(std::time::Duration::from_secs(20)),
1094 )
1095 .await
1096 .map_err(VectorError::Other)
1097 }
1098
1099 pub async fn block_user(&self, npub: &str) -> bool {
1101 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1102 }
1103
1104 pub async fn unblock_user(&self, npub: &str) -> bool {
1106 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1107 }
1108
1109 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1111 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1112 }
1113
1114 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1116 profile::sync::get_blocked_users().await
1117 }
1118
1119 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1121 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1122 }
1123
1124 pub fn my_npub(&self) -> Option<String> {
1126 state::my_public_key()
1127 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1128 }
1129
1130 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1137 use crate::community::ConcordProtocol;
1138 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1139 let mut out = Vec::new();
1140 for id in ids {
1141 match crate::db::community::community_protocol(&id).ok().flatten() {
1143 Some(ConcordProtocol::V2) => {
1144 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1145 let me = state::my_public_key();
1146 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1147 out.push(serde_json::json!({
1148 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1149 "version": 2,
1150 "name": c.name,
1151 "description": c.description,
1152 "is_owner": is_owner,
1153 "dissolved": c.dissolved,
1158 "channels": c.channels.iter()
1164 .map(|ch| serde_json::json!({
1165 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1166 "name": ch.name,
1167 "private": ch.private,
1168 "readable": !(ch.private && ch.key.is_none()),
1169 "epoch": ch.epoch.0,
1170 }))
1171 .collect::<Vec<_>>(),
1172 }));
1173 }
1174 }
1175 _ => {
1176 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1177 out.push(serde_json::json!({
1178 "community_id": c.id.to_hex(),
1179 "version": 1,
1180 "name": c.name,
1181 "description": c.description,
1182 "is_owner": crate::community::service::is_proven_owner(&c),
1183 "dissolved": c.dissolved,
1184 "channels": c.channels.iter()
1185 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1186 .collect::<Vec<_>>(),
1187 }));
1188 }
1189 }
1190 }
1191 }
1192 out
1193 }
1194
1195 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1200 use crate::community::{v2::service as v2, transport::LiveTransport};
1201 let relays: Vec<String> = crate::state::active_trusted_relays()
1202 .await
1203 .iter()
1204 .map(|s| s.to_string())
1205 .collect();
1206 if relays.is_empty() {
1207 return Err(VectorError::Other("no relays available to host the Community".into()));
1208 }
1209 let session = crate::db::current_session();
1210 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1211 let community = v2::create_community(&transport, name, relays, None)
1212 .await
1213 .map_err(VectorError::Other)?;
1214 self.register_v2_chats(&community, &session).await;
1215 if let Some(client) = state::nostr_client() {
1217 crate::community::v2::realtime::refresh_subscription(&client).await;
1218 }
1219 Ok(Self::v2_summary(&community))
1220 }
1221
1222 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1228 use crate::community::ConcordProtocol;
1229 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1230 return Ok(None);
1231 };
1232 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1233 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1234 Some(ConcordProtocol::V2) => Some(cid),
1235 _ => None,
1236 })
1237 }
1238
1239 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1241 let me = state::my_public_key();
1242 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1243 serde_json::json!({
1244 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1245 "version": 2,
1246 "name": community.name,
1247 "description": community.description,
1248 "is_owner": is_owner,
1249 "channels": community.channels.iter()
1250 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1251 .collect::<Vec<_>>(),
1252 })
1253 }
1254
1255 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, __session: &std::sync::Arc<crate::db::Session>) {
1261 register_v2_chats_inner(community).await
1262 }
1263}
1264
1265pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2) {
1268 crate::db::scoped(async move {
1269 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1270 let me = state::my_public_key();
1271 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1272 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1273 let Some(primary) = community.primary_channel() else { return };
1276 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1277 let slims = {
1282 let mut st = state::STATE.lock().await;
1283 let mut slims = Vec::new();
1284 for ch in &community.channels {
1285 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1286 st.upsert_community_chat(
1287 &ch_hex,
1288 &community.name,
1289 community.description.as_deref().unwrap_or(""),
1290 &id_hex,
1291 is_owner,
1292 community.icon.is_some(),
1293 owner_npub.as_deref(),
1294 Some(community.created_at_ms),
1295 community.dissolved,
1296 crate::community::ConcordProtocol::V2,
1297 &ch.name,
1298 &primary_hex,
1299 );
1300 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1301 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1302 }
1303 }
1304 slims
1305 };
1306 for slim in &slims {
1308 let _ = crate::db::chats::save_slim_chat(slim);
1309 }
1310 })
1311 .await
1312}
1313
1314impl VectorCore {
1315 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1319 use crate::community::{public_invite, service, transport::LiveTransport};
1320 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1324 let session = crate::db::current_session();
1325 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1326 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1327 .await
1328 .map_err(VectorError::Other)?;
1329 self.register_v2_chats(&community, &session).await;
1330 if let Some(client) = state::nostr_client() {
1331 crate::community::v2::realtime::refresh_subscription(&client).await;
1332 }
1333 if crate::community::v2::realtime::follow_worker_running() {
1338 crate::community::v2::realtime::enqueue_follow(community.id());
1339 } else {
1340 let seed_community = community.clone();
1341 db::spawn_bound(async move {
1342 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1343 if matches!(
1344 crate::community::v2::service::sync_guestbook(&transport, &seed_community).await,
1345 Ok(fresh) if !fresh.is_empty()
1346 ) {
1347 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1348 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1349 }
1350 });
1351 }
1352 return Ok(Self::v2_summary(&community));
1353 }
1354 let (relays, token) = public_invite::parse_invite_url(invite_url)
1355 .map_err(|e| VectorError::Other(e.to_string()))?;
1356 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1357 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1358 .await
1359 .map_err(VectorError::Other)?;
1360 let now = std::time::SystemTime::now()
1361 .duration_since(std::time::UNIX_EPOCH)
1362 .map(|d| d.as_secs())
1363 .unwrap_or(0);
1364 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1367 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1368 .await
1369 .map_err(VectorError::Other)?;
1370 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1371 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1374 self.finalize_member_join(community, &transport, attribution).await
1375 }
1376
1377 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1380 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1381 Ok(rows.iter().map(|p| {
1382 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1385 serde_json::json!({
1386 "community_id": p.community_id,
1387 "name": v2.name,
1388 "inviter_npub": p.inviter_npub,
1389 "version": 2,
1390 })
1391 } else {
1392 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1393 .ok().map(|i| i.name).unwrap_or_default();
1394 serde_json::json!({
1395 "community_id": p.community_id,
1396 "name": name,
1397 "inviter_npub": p.inviter_npub,
1398 "version": 1,
1399 })
1400 }
1401 }).collect())
1402 }
1403
1404 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1408 crate::db::scoped(async move {
1409 use crate::community::transport::LiveTransport;
1410 let bundle_json = crate::db::community::get_pending_invite(community_id)
1411 .map_err(VectorError::Other)?
1412 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1413 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1414
1415 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1417 let session = crate::db::current_session();
1418 let inviter = crate::db::community::list_pending_invites()
1420 .ok()
1421 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1422 let community = match crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref()).await {
1434 Ok(c) => c,
1435 Err(e) if e == crate::community::v2::service::ERR_DISSOLVED => {
1436 let relays = crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json)
1437 .map(|i| i.relays)
1438 .unwrap_or_default();
1439 crate::community::v2::service::retire_dead_invite(&transport, community_id, &relays).await;
1440 return Err(VectorError::Other("this community has been dissolved — the invite was removed".into()));
1441 }
1442 Err(e) => return Err(VectorError::Other(e)),
1443 };
1444 self.register_v2_chats(&community, &session).await;
1445 if let Some(client) = state::nostr_client() {
1446 crate::community::v2::realtime::refresh_subscription(&client).await;
1447 }
1448 crate::community::v2::realtime::enqueue_follow(community.id());
1449 let _ = crate::db::community::delete_pending_invite(community_id);
1450 return Ok(Self::v2_summary(&community));
1451 }
1452
1453 use crate::community::invite::{accept_invite, CommunityInvite};
1455 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1456 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1457 let now = std::time::SystemTime::now()
1461 .duration_since(std::time::UNIX_EPOCH)
1462 .map(|d| d.as_secs())
1463 .unwrap_or(0);
1464 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1465 .await
1466 .map_err(VectorError::Other)?;
1467 let summary = self.finalize_member_join(community, &transport, None).await?;
1469 let _ = crate::db::community::delete_pending_invite(community_id);
1470 Ok(summary)
1471 })
1472 .await
1473 }
1474
1475 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1480 &self,
1481 community: crate::community::Community,
1482 transport: &T,
1483 attribution: Option<(String, Option<String>)>,
1484 ) -> Result<serde_json::Value> {
1485 use crate::community::service;
1486 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1493 return Ok(serde_json::json!({
1494 "community_id": v2,
1495 "version": 2,
1496 "migrated": true,
1497 }));
1498 }
1499 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1503 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1506 if c.removed {
1507 let _ = crate::db::community::delete_community(&community.id.to_hex());
1508 return Err(VectorError::Other("you have been removed from this community".into()));
1509 }
1510 }
1511 let community = crate::db::community::load_community(&community.id)
1512 .map_err(VectorError::Other)?
1513 .unwrap_or(community);
1514 let _ = service::fetch_and_apply_control(transport, &community).await;
1518 if service::am_i_banned(&community) {
1519 let _ = crate::db::community::delete_community(&community.id.to_hex());
1520 return Err(VectorError::Other("you are banned from this community".into()));
1521 }
1522 let community = crate::db::community::load_community(&community.id)
1524 .map_err(VectorError::Other)?
1525 .unwrap_or(community);
1526 let owner_npub = community
1527 .owner_attestation
1528 .as_ref()
1529 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1530 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1531 {
1532 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1533 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1534 let mut st = state::STATE.lock().await;
1535 for ch in &community.channels {
1536 st.upsert_community_chat(
1537 &ch.id.to_hex(),
1538 &community.name,
1539 community.description.as_deref().unwrap_or(""),
1540 &community.id.to_hex(),
1541 crate::community::service::is_proven_owner(&community),
1542 community.icon.is_some(),
1543 owner_npub.as_deref(),
1544 created_at_ms,
1545 community.dissolved,
1546 crate::community::ConcordProtocol::V1,
1547 &ch.name,
1548 &primary_hex,
1549 );
1550 }
1551 }
1552 if let Some(primary) = community.channels.first() {
1555 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1556 }
1557 Ok(serde_json::json!({
1558 "community_id": community.id.to_hex(),
1559 "version": 1,
1560 "name": community.name,
1561 "channels": community.channels.iter()
1562 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1563 .collect::<Vec<_>>(),
1564 }))
1565 }
1566
1567
1568 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1572 use crate::community::CommunityId;
1573 if community_id.len() != 64 {
1574 return Err(VectorError::Other("malformed community id".into()));
1575 }
1576 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1577 match crate::db::community::community_protocol(&cid).ok().flatten() {
1578 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1579 .map_err(VectorError::Other)?
1580 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1581 Some(_) => Err(VectorError::Other(
1582 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1583 )),
1584 None => Err(VectorError::Other("community not found".into())),
1585 }
1586 }
1587
1588 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1589 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1590 .map(crate::community::ChannelId)
1591 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1592 }
1593
1594 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1599 use crate::community::{v2::service, transport::LiveTransport};
1600 let community = Self::v2_community(community_id)?;
1601 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1602 let id = if private {
1603 service::create_private_channel(&transport, &community, name).await
1604 } else {
1605 service::create_public_channel(&transport, &community, name).await
1606 }
1607 .map_err(VectorError::Other)?;
1608 if let Some(client) = state::nostr_client() {
1611 crate::community::v2::realtime::refresh_subscription(&client).await;
1612 }
1613 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1614 }
1615
1616 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &str) -> Result<()> {
1619 use crate::community::{v2::service, transport::LiveTransport};
1620 let community = Self::v2_community(community_id)?;
1621 let id = Self::channel_id_of(channel_id)?;
1622 let mut meta = community
1623 .channel(&id)
1624 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1625 .metadata();
1626 meta.name = name.to_string();
1627 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1628 service::edit_channel_metadata(&transport, &community, &id, &meta)
1629 .await
1630 .map_err(VectorError::Other)
1631 }
1632
1633 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1636 use crate::community::{v2::service, transport::LiveTransport};
1637 let community = Self::v2_community(community_id)?;
1638 let id = Self::channel_id_of(channel_id)?;
1639 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1640 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1641 service::delete_channel(&transport, &community, &id, &name)
1642 .await
1643 .map_err(VectorError::Other)
1644 }
1645
1646 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1649 use crate::community::{v2::service, transport::LiveTransport};
1650 let community = Self::v2_community(community_id)?;
1651 let id = Self::channel_id_of(channel_id)?;
1652 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1653 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1654 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1655 service::grant_channel_access(&transport, &community, &id, &member)
1656 .await
1657 .map_err(VectorError::Other)
1658 }
1659
1660 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1664 use crate::community::{v2::service, transport::LiveTransport};
1665 let community = Self::v2_community(community_id)?;
1666 let id = Self::channel_id_of(channel_id)?;
1667 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1668 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1669 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1670 service::revoke_channel_access(&transport, &community, &id, &member)
1671 .await
1672 .map_err(VectorError::Other)
1673 }
1674
1675 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1683 use nostr_sdk::prelude::{PublicKey, ToBech32};
1684 let community = Self::v2_community(community_id)?;
1685 let id = Self::channel_id_of(channel_id)?;
1686 let ch = community
1687 .channel(&id)
1688 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1689 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1692 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1693 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1694 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1695 let access_ids = roster.channel_role_ids(&chan_hex);
1696 let roles: Vec<serde_json::Value> = roster
1697 .channel_roles(&chan_hex)
1698 .into_iter()
1699 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1700 .collect();
1701 let members: Vec<String> = roster
1702 .grants
1703 .iter()
1704 .filter(|g| !banned.contains(&g.member))
1705 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1706 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1707 .collect();
1708 Ok(serde_json::json!({
1709 "channel_id": chan_hex,
1710 "private": ch.private,
1711 "readable": !(ch.private && ch.key.is_none()),
1712 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1713 "roles": roles,
1714 "members": members,
1715 }))
1716 }
1717
1718 pub async fn create_public_invite(
1723 &self,
1724 community_id: &str,
1725 expires_at_ms: Option<u64>,
1726 label: Option<String>,
1727 ) -> Result<String> {
1728 use crate::community::{service, transport::LiveTransport, CommunityId};
1729 if community_id.len() != 64 {
1730 return Err(VectorError::Other("malformed community id".into()));
1731 }
1732 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1733 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1735 crate::db::community::community_protocol(&cid).ok()
1736 {
1737 let community = crate::db::community::load_community_v2(&cid)
1738 .map_err(VectorError::Other)?
1739 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1740 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1741 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1744 let minted =
1745 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1746 .await
1747 .map_err(VectorError::Other)?;
1748 return Ok(minted.url);
1749 }
1750 let community = crate::db::community::load_community(&CommunityId(
1751 crate::simd::hex::hex_to_bytes_32(community_id),
1752 ))
1753 .map_err(VectorError::Other)?
1754 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1755 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1756 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1757 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1758 .await
1759 .map_err(VectorError::Other)?;
1760 Ok(url)
1761 }
1762
1763 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1767 crate::db::scoped(async move {
1768 use crate::community::{service, CommunityId};
1769 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1770
1771 let my_pk = crate::state::my_public_key()
1772 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1773
1774 if community_id.len() != 64 {
1775 return Err(VectorError::Other("malformed community id".into()));
1776 }
1777 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1778 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1784 crate::db::community::community_protocol(&cid).ok()
1785 {
1786 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1787 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1788 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1789 let bundle = {
1797 let lock = crate::community::v2::realtime::follow_lock(&cid);
1798 let _rotation = lock.lock().await;
1799 let community = crate::db::community::load_community_v2(&cid)
1800 .map_err(VectorError::Other)?
1801 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1802 crate::community::v2::service::bundle_of(
1803 &community,
1804 crate::community::v2::service::BundleAudience::Member(recipient),
1805 Some(my_pk),
1806 None,
1807 None,
1808 )
1809 };
1810 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1811 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1814 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1815 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1816 let rumor = nostr_sdk::prelude::EventBuilder::new(
1817 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1818 bundle_json,
1819 )
1820 .tag(expiry_tag.clone())
1821 .finalize_unsigned_with_id(my_pk);
1822 let k_tag = nostr_sdk::prelude::Tag::custom(
1823 "k",
1824 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1825 );
1826 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1827 .await
1828 .map_err(VectorError::Other)?;
1829 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1830 }
1831 let community = crate::db::community::load_community(&CommunityId(
1832 crate::simd::hex::hex_to_bytes_32(community_id),
1833 ))
1834 .map_err(VectorError::Other)?
1835 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1836
1837 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1838 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1839 }
1840 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1841 .map_err(|_| VectorError::Other("invalid npub".into()))?
1842 .to_hex();
1843 if crate::db::community::get_community_banlist(community_id)
1844 .map_err(VectorError::Other)?
1845 .iter()
1846 .any(|b| b == &invitee_hex)
1847 {
1848 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1849 }
1850
1851
1852 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1853 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1854 .map_err(VectorError::Other)?;
1855 let pending_id = format!("community-invite-{}", community_id);
1856 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1858 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1859
1860 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1861 .await
1862 .map_err(VectorError::Other)?;
1863
1864 Ok(serde_json::json!({
1865 "community_id": community_id,
1866 "invitee": invitee_npub,
1867 "wrap_event_id": result.event_id,
1868 }))
1869 })
1870 .await
1871 }
1872
1873 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1878 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1879 }
1880
1881 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1886 use crate::community::{service, transport::LiveTransport, CommunityId};
1887 if community_id.len() != 64 {
1888 return Err(VectorError::Other("malformed community id".into()));
1889 }
1890 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1891 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1892 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
1895 let community = crate::db::community::load_community_v2(&cid)
1896 .map_err(VectorError::Other)?
1897 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1898 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
1899 .await
1900 .map_err(VectorError::Other);
1901 }
1902 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
1903 let community = crate::db::community::load_community(&cid)
1904 .map_err(VectorError::Other)?
1905 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1906 service::revoke_public_invite(&transport, &community, &token_bytes)
1907 .await
1908 .map_err(VectorError::Other)
1909 }
1910
1911 pub async fn send_community_message(
1913 &self,
1914 channel_id: &str,
1915 content: &str,
1916 replied_to: Option<&str>,
1917 ) -> Result<String> {
1918 use crate::community::{envelope, inbound, service, transport::LiveTransport};
1919 if let Some(id) = self.v2_community_for_channel(channel_id)? {
1921 let community = crate::db::community::load_community_v2(&id)
1922 .map_err(VectorError::Other)?
1923 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1924 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
1925 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1926 let reply = match replied_to.filter(|r| !r.is_empty()) {
1929 Some(parent_id) => {
1930 let author_hex = {
1931 let st = state::STATE.lock().await;
1932 st.find_message(parent_id)
1933 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
1934 .map(|pk| pk.to_hex())
1935 .unwrap_or_default()
1936 };
1937 Some((parent_id.to_string(), author_hex))
1938 }
1939 None => None,
1940 };
1941 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
1942 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
1945 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
1946 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, vec![])
1947 .await
1948 .map_err(VectorError::Other);
1949 }
1950 let (community, channel) = self.resolve_channel(channel_id)?;
1951 Self::ensure_v1_writable(&community)?;
1952 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1953 let reply = replied_to.filter(|r| !r.is_empty());
1954 let ms = std::time::SystemTime::now()
1955 .duration_since(std::time::UNIX_EPOCH)
1956 .map(|d| d.as_millis() as u64)
1957 .unwrap_or(0);
1958 let unsigned = envelope::build_inner_typed(
1959 author_pk,
1960 &channel.id,
1961 channel.epoch,
1962 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
1963 content,
1964 ms,
1965 reply,
1966 &[],
1967 );
1968 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
1969 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1970 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1971 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
1972 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1973 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
1974 .await
1975 .map_err(VectorError::Other)?;
1976 let echoed = {
1978 let mut st = state::STATE.lock().await;
1979 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
1980 };
1981 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
1982 let _ = crate::db::events::save_message(channel_id, &msg).await;
1983 }
1984 Ok(message_id)
1985 }
1986
1987 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
1991 crate::db::scoped(async move {
1992 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
1993 let path = std::path::Path::new(file_path);
1994 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1995 if bytes.is_empty() {
1996 return Err(VectorError::Other("Empty file".into()));
1997 }
1998 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
1999 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
2000
2001 let v2_target = match self.v2_community_for_channel(channel_id)? {
2006 Some(id) => Some(
2007 crate::db::community::load_community_v2(&id)
2008 .map_err(VectorError::Other)?
2009 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
2010 ),
2011 None => None,
2012 };
2013 let v1_target = match v2_target {
2014 Some(_) => None,
2015 None => Some(self.resolve_channel(channel_id)?),
2016 };
2017 match (&v2_target, &v1_target) {
2022 (Some(c), _) => {
2023 let cid = crate::simd::hex::bytes_to_hex_32(&c.id().0);
2024 if crate::db::community::get_community_dissolved(&cid).unwrap_or(false) {
2025 return Err(VectorError::Other("this community has been dissolved".into()));
2026 }
2027 }
2028 (None, Some((c, _))) => Self::ensure_v1_writable(c)?,
2029 _ => {}
2030 }
2031 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2032
2033 let file_hash = crate::crypto::sha256_hex(&bytes);
2034 let mime = crate::crypto::mime_from_extension(&extension);
2035 let img_meta = crate::crypto::generate_image_metadata(&bytes);
2036
2037 let download_dir = crate::db::get_download_dir();
2039 let _ = std::fs::create_dir_all(&download_dir);
2040 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
2041 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
2042 let _ = std::fs::write(&local_path, &bytes);
2043
2044 let params = crate::crypto::generate_encryption_params();
2046 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
2047 let encrypted_size = encrypted.len() as u64;
2048
2049 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2050 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2051 let servers = crate::blossom_servers::compute_enabled_servers();
2052 if servers.is_empty() {
2053 return Err(VectorError::Other("No Blossom servers configured".into()));
2054 }
2055 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
2056 let url = crate::blossom::upload_blob_with_progress_and_failover(
2057 signer.clone(),
2058 servers,
2059 std::sync::Arc::new(encrypted),
2060 Some(mime),
2061 true,
2062 noop_progress,
2063 Some(3),
2064 Some(std::time::Duration::from_secs(2)),
2065 None,
2066 ).await.map_err(VectorError::Other)?;
2067
2068 let attachment = crate::types::Attachment {
2069 id: file_hash.clone(),
2070 key: params.key.clone(),
2071 nonce: params.nonce.clone(),
2072 extension: extension.clone(),
2073 name: filename.clone(),
2074 url,
2075 path: local_path.to_string_lossy().to_string(),
2076 size: encrypted_size,
2077 img_meta,
2078 downloading: false,
2079 downloaded: true,
2080 ..Default::default()
2081 };
2082 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
2083
2084 if let Some(community) = v2_target {
2086 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2087 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2088 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2089 .await
2090 .map_err(VectorError::Other);
2091 }
2092 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2093 let ms = std::time::SystemTime::now()
2094 .duration_since(std::time::UNIX_EPOCH)
2095 .map(|d| d.as_millis() as u64)
2096 .unwrap_or(0);
2097 let unsigned = envelope::build_inner_full(
2098 author_pk, &channel.id, channel.epoch,
2099 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2100 );
2101 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2102 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2103 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2104 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2105 .await.map_err(VectorError::Other)?;
2106 let echoed = {
2108 let mut st = state::STATE.lock().await;
2109 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2110 };
2111 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2112 let _ = crate::db::events::save_message(channel_id, &m).await;
2113 }
2114 Ok(message_id)
2115 })
2116 .await
2117 }
2118
2119 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2121 use crate::community::{service, transport::LiveTransport};
2122 if let Some(id) = self.v2_community_for_channel(channel_id)? {
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 = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2128 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2129 .await
2130 .map_err(VectorError::Other);
2131 }
2132 let (community, channel) = self.resolve_channel(channel_id)?;
2133 Self::ensure_v1_writable(&community)?;
2134 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2135 service::publish_typing_signal(&transport, &community, &channel)
2136 .await
2137 .map_err(VectorError::Other)
2138 }
2139
2140 pub async fn send_community_reaction(
2143 &self,
2144 channel_id: &str,
2145 message_id: &str,
2146 emoji: &str,
2147 emoji_url: Option<&str>,
2148 ) -> Result<()> {
2149 crate::db::scoped(async move {
2150 if Self::own_reaction_id(message_id, emoji).await.is_some() {
2152 return Ok(());
2153 }
2154 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2155 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2156 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2157 }
2158 _ => Vec::new(),
2159 };
2160 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2161 let community = crate::db::community::load_community_v2(&id)
2162 .map_err(VectorError::Other)?
2163 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2164 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2165 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2166 let held = {
2171 let st = state::STATE.lock().await;
2172 st.find_message(message_id)
2173 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2174 };
2175 let held = held.or_else(|| {
2176 crate::db::events::event_author(message_id)
2177 .ok()
2178 .flatten()
2179 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2180 });
2181 let target_author = match held {
2182 Some(pk) => pk,
2183 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2184 .await
2185 .map_err(VectorError::Other)?
2186 .iter()
2187 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2188 .map(|f| f.event.opened().author)
2189 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2190 };
2191 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2193 return crate::community::v2::service::send_reaction(
2198 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2199 )
2200 .await
2201 .map(|_| ())
2202 .map_err(VectorError::Other);
2203 }
2204 self.publish_community_control(
2205 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2206 ).await
2207 })
2208 .await
2209 }
2210
2211 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2213 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2214 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2215 let community = crate::db::community::load_community_v2(&id)
2216 .map_err(VectorError::Other)?
2217 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2218 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2219 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2220 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2221 .await
2222 .map(|_| ())
2223 .map_err(VectorError::Other);
2224 }
2225 self.publish_community_control(
2226 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2227 ).await
2228 }
2229
2230 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2234 let channel_id = {
2235 let st = state::STATE.lock().await;
2236 match st.find_message(message_id) {
2237 Some((chat, _)) => chat.id.clone(),
2238 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2239 }
2240 };
2241 self.delete_community_message_in(&channel_id, message_id).await
2242 }
2243
2244 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2248 use crate::community::{service, transport::LiveTransport};
2249 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2250
2251 let attachment_urls: Vec<String> = {
2254 let st = state::STATE.lock().await;
2255 st.find_message(message_id)
2256 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2257 .unwrap_or_default()
2258 };
2259
2260 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2261 let community = crate::db::community::load_community_v2(&id)
2264 .map_err(VectorError::Other)?
2265 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2266 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2267 crate::community::v2::service::send_delete(
2268 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2269 )
2270 .await
2271 .map_err(VectorError::Other)?;
2272 } else {
2273 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2275 let _ = service::delete_message(&transport, message_id).await;
2276 }
2277 self.publish_community_control(
2279 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2280 ).await?;
2281 }
2282 if !attachment_urls.is_empty() {
2284 if let Some(_client) = state::nostr_client() {
2285 if let Ok(signer) = crate::signer::active_signer() {
2286 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2287 }
2288 }
2289 }
2290 let removed_chat = {
2291 let mut st = state::STATE.lock().await;
2292 st.remove_message(message_id).map(|(cid, _)| cid)
2293 };
2294 let _ = crate::db::events::delete_event(message_id).await;
2295 traits::emit_event_json("message_removed", serde_json::json!({
2296 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2297 }));
2298 Ok(())
2299 }
2300
2301 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2307 use crate::community::transport::LiveTransport;
2308 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2309
2310 let author_npub = {
2313 let st = state::STATE.lock().await;
2314 st.find_message(message_id).and_then(|(_, m)| m.npub)
2315 };
2316 let author_npub = match author_npub {
2317 Some(n) => n,
2318 None => crate::db::events::event_author(message_id)
2319 .ok()
2320 .flatten()
2321 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2322 };
2323 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2324 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2325
2326 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2327 let community = crate::db::community::load_community_v2(&id)
2328 .map_err(VectorError::Other)?
2329 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2330 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2331 crate::community::v2::service::moderation_delete(
2332 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2333 )
2334 .await
2335 .map_err(VectorError::Other)?;
2336 } else {
2337 let cid = crate::db::community::community_id_for_channel(channel_id)
2338 .map_err(VectorError::Other)?
2339 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2340 let community = crate::db::community::load_community(&crate::community::CommunityId(
2341 crate::simd::hex::hex_to_bytes_32(&cid),
2342 ))
2343 .map_err(VectorError::Other)?
2344 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2345 let channel = community
2346 .channels
2347 .iter()
2348 .find(|c| c.id.to_hex() == channel_id)
2349 .cloned()
2350 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2351 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2352 .await
2353 .map_err(VectorError::Other)?;
2354 }
2355
2356 let removed_chat = {
2357 let mut st = state::STATE.lock().await;
2358 st.remove_message(message_id).map(|(cid, _)| cid)
2359 };
2360 let _ = crate::db::events::delete_event(message_id).await;
2361 traits::emit_event_json("message_removed", serde_json::json!({
2362 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2363 }));
2364 Ok(())
2365 }
2366
2367 async fn publish_community_control(
2370 &self,
2371 channel_id: &str,
2372 kind: u16,
2373 content: &str,
2374 target: &str,
2375 emoji_tags: &[crate::types::EmojiTag],
2376 ) -> Result<()> {
2377 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2378 let (community, channel) = self.resolve_channel(channel_id)?;
2379 Self::ensure_v1_writable(&community)?;
2380 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2381 let ms = std::time::SystemTime::now()
2382 .duration_since(std::time::UNIX_EPOCH)
2383 .map(|d| d.as_millis() as u64)
2384 .unwrap_or(0);
2385 let unsigned = envelope::build_inner_typed(
2386 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2387 );
2388 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2389 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2390 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2391 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2392 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2393 .await.map_err(VectorError::Other)?;
2394 let outcome = {
2397 let mut st = state::STATE.lock().await;
2398 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2399 };
2400 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2401 if let Some(ev) = edit_event {
2402 let mut ev = (*ev).clone();
2403 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2404 let _ = crate::db::events::save_event(&ev).await;
2405 } else {
2406 let _ = crate::db::events::save_message(channel_id, &message).await;
2407 }
2408 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2409 }
2410 Ok(())
2411 }
2412
2413 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2421 self.sync_community_channel_before(channel_id, limit, None).await
2422 }
2423
2424 pub async fn sync_community_channel_page(
2427 &self,
2428 channel_id: &str,
2429 limit: usize,
2430 before_secs: Option<u64>,
2431 since_secs: Option<u64>,
2432 ) -> Result<(BackfillCount, Vec<String>)> {
2433 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2434 let warnings = if community::v2::realtime::follow_worker_running() {
2435 community::v2::realtime::enqueue_follow(&id);
2436 Vec::new()
2437 } else {
2438 Self::v2_inline_follow(&id).await
2439 };
2440 let count = Self::v2_backfill_channel_counted(
2441 &id, channel_id, limit, 8, since_secs, before_secs,
2442 crate::community::transport::Evidence::Fast, 12,
2443 ).await;
2444 return Ok((count, warnings));
2445 }
2446 let (new_messages, warnings) = self.sync_community_channel(channel_id, limit).await?;
2449 Ok((BackfillCount { fetched: new_messages, new_messages }, warnings))
2450 }
2451
2452 pub async fn sync_community_channel_before(
2461 &self,
2462 channel_id: &str,
2463 limit: usize,
2464 before_secs: Option<u64>,
2465 ) -> Result<(usize, Vec<String>)> {
2466 use crate::community::{send, service, transport::LiveTransport};
2467 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2468 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2473 let warnings = if community::v2::realtime::follow_worker_running() {
2474 community::v2::realtime::enqueue_follow(&id);
2475 Vec::new()
2476 } else {
2477 Self::v2_inline_follow(&id).await
2478 };
2479 let new = Self::v2_backfill_channel(
2484 &id, channel_id, limit, 8, None, before_secs,
2485 crate::community::transport::Evidence::Fast, 12,
2486 ).await;
2487 return Ok((new, warnings));
2488 }
2489 let (community, _) = self.resolve_channel(channel_id)?;
2490 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2491 let mut warnings: Vec<String> = Vec::new();
2492
2493 match service::catch_up_server_root(&transport, &community).await {
2501 Ok(c) if c.removed => {
2502 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2504 return Ok((0, warnings));
2505 }
2506 Ok(_) => {}
2507 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2508 }
2509 let (community, _) = self.resolve_channel(channel_id)?;
2510
2511 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2517 warnings.push(format!("control fold failed: {e}"));
2518 }
2519 if service::am_i_banned(&community) {
2520 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2522 return Ok((0, warnings));
2523 }
2524 let (community, channel) = self.resolve_channel(channel_id)?;
2527 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2528 warnings.push(format!("channel catch-up failed: {e}"));
2529 }
2530 let (community, _) = self.resolve_channel(channel_id)?;
2534 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2535 warnings.push(format!("read-cut resume failed: {e}"));
2536 }
2537 let (community, channel) = self.resolve_channel(channel_id)?;
2538
2539 let session = crate::db::current_session();
2541 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2542 .await
2543 .map_err(VectorError::Other)?;
2544 let new = Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await;
2545 Ok((new, warnings))
2546 }
2547
2548 async fn v1_ingest_channel_page(
2553 channel_id: &str,
2554 events: &[nostr_sdk::prelude::Event],
2555 channel: &crate::community::Channel,
2556 my_pk: nostr_sdk::prelude::PublicKey,
2557 session: &std::sync::Arc<crate::db::Session>,
2558 ) -> usize {
2559 crate::db::scoped(async move {
2560 use crate::community::inbound;
2561 let outcomes = {
2562 let mut st = state::STATE.lock().await;
2563 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2564 };
2565 let mut new = 0usize;
2566 let mut pending: Vec<&crate::types::Message> = Vec::new();
2570 for o in &outcomes {
2571 if !session.is_live() {
2573 pending.clear();
2574 break;
2575 }
2576 match o {
2577 inbound::IncomingEvent::NewMessage(m) => {
2578 pending.push(m);
2579 new += 1;
2580 }
2581 inbound::IncomingEvent::Updated { message, .. } => {
2582 pending.push(message);
2583 }
2584 inbound::IncomingEvent::Removed { target_id } => {
2585 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2586 let _ = crate::db::events::delete_event(target_id).await;
2587 }
2588 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2589 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2592 let _ = crate::db::events::delete_event(reaction_id).await;
2593 }
2594 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2595 let et = if *joined {
2596 crate::stored_event::SystemEventType::MemberJoined
2597 } else {
2598 crate::stored_event::SystemEventType::MemberLeft
2599 };
2600 let note = invited_by.as_ref().map(|by| match invited_label {
2602 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2603 _ => by.clone(),
2604 });
2605 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;
2606 }
2607 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2608 community::service::persist_webxdc_signal(
2611 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2612 ).await;
2613 }
2614 inbound::IncomingEvent::Kicked { community_id }
2615 | inbound::IncomingEvent::SelfLeft { community_id } => {
2616 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2621 let _ = crate::db::community::delete_community_retain_keys(community_id);
2622 break;
2623 }
2624 inbound::IncomingEvent::Typing { .. } => {
2625 }
2627 }
2628 }
2629 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2630 new
2631 })
2632 .await
2633 }
2634
2635 pub async fn sync_channel_events(
2650 &self,
2651 channel_id: &str,
2652 max_events: usize,
2653 until_s: Option<u64>,
2654 since_s: Option<u64>,
2655 ) -> Result<usize> {
2656 use crate::community::{send, transport::LiveTransport};
2657 let max = max_events.clamp(1, 500);
2658 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2659 let community = crate::db::community::load_community_v2(&id)
2660 .map_err(VectorError::Other)?
2661 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2662 if community.dissolved {
2663 return Err(VectorError::Other("this community has been dissolved".into()));
2664 }
2665 let ch_id = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2666 let ch = community
2667 .channel(&ch_id)
2668 .ok_or_else(|| VectorError::Other("no such channel in this community".into()))?;
2669 if ch.private && ch.key.is_none() {
2672 return Err(VectorError::Other(
2673 "this private channel has no key yet (awaiting rekey delivery)".into(),
2674 ));
2675 }
2676 let new = Self::v2_backfill_channel(
2677 &id, channel_id, max, 1, since_s, until_s,
2678 crate::community::transport::Evidence::Fast, 12,
2679 )
2680 .await;
2681 return Ok(new);
2682 }
2683 let (community, channel) = self.resolve_channel(channel_id)?;
2684 Self::ensure_v1_writable(&community)?;
2685 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2686 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2687 let session = crate::db::current_session();
2688 let events = send::fetch_channel_page(&transport, &community, &channel, until_s, since_s, max)
2689 .await
2690 .map_err(VectorError::Other)?;
2691 Ok(Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await)
2692 }
2693
2694 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2706 use crate::bot_interface::{self, ChatCommandsSnapshot};
2707 use nostr_sdk::prelude::ToBech32;
2708
2709 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2710 let mut relays: Vec<String> = Vec::new();
2711 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2712 if let Some(cid_hex) = community_hex {
2713 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2714 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2715 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2716 relays = community.relays.clone();
2717 } else {
2718 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2719 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2720 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2721 };
2722 relays = community.relays.clone();
2723 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2724 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2725 members.push(pk);
2726 }
2727 }
2728 }
2729 let state = crate::state::STATE.lock().await;
2730 for pk in members {
2731 let Ok(npub) = pk.to_bech32();
2732 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2733 bots.push(pk);
2734 }
2735 }
2736 } else if chat_id.starts_with("npub1") {
2737 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2738 let is_bot = {
2739 let state = crate::state::STATE.lock().await;
2740 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2741 };
2742 if is_bot {
2743 bots.push(pk);
2744 if let Some(client) = crate::state::nostr_client() {
2747 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2748 }
2749 }
2750 }
2751 }
2752
2753 if bots.is_empty() {
2754 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2755 }
2756 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2759 relays.sort();
2760 relays.dedup();
2761 bots.sort_by_key(|p| p.to_hex());
2764 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2765 let commands = bot_interface::assemble_from_store(&bot_hexes);
2766 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2767 if !fresh {
2768 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2769 }
2770 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2771 }
2772
2773 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2778 use nostr_sdk::prelude::ToBech32;
2779 match Self::load_v2_if_v2(community_id) {
2785 Ok(Some(community)) => {
2786 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2787 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2788 if cursor == 0 {
2789 if crate::community::v2::realtime::follow_worker_running() {
2790 crate::community::v2::realtime::enqueue_follow(community.id());
2791 } else {
2792 let c2 = community.clone();
2793 db::spawn_bound(async move {
2794 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2795 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2).await, Ok(fresh) if !fresh.is_empty()) {
2796 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2797 }
2798 });
2799 }
2800 }
2801 return crate::community::v2::service::stored_memberlist(&community)
2802 .unwrap_or_default()
2803 .into_iter()
2804 .filter_map(|pk| pk.to_bech32().ok())
2805 .map(|npub| serde_json::json!({ "npub": npub }))
2806 .collect();
2807 }
2808 Ok(None) => {} Err(_) => return Vec::new(),
2811 }
2812 crate::db::community::community_member_activity(community_id)
2813 .unwrap_or_default()
2814 .into_iter()
2815 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2816 .collect()
2817 }
2818
2819 async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
2823 crate::db::scoped(async move {
2824 use crate::community::transport::LiveTransport;
2825 let session = crate::db::current_session();
2826 let lock = crate::community::v2::realtime::follow_lock(id);
2831 let _guard = lock.lock().await;
2832 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2833 let mut warnings: Vec<String> = Vec::new();
2834 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
2835 warnings.push("v2 community not found".to_string());
2836 return warnings;
2837 };
2838 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
2839 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
2840 Ok(f) if f.dissolved => return warnings,
2842 Ok(f) if f.self_removed => {
2843 let _ = crate::db::community::delete_community(&cid_hex);
2845 return warnings;
2846 }
2847 Ok(_) => {}
2848 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
2849 }
2850 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
2851 match crate::community::v2::service::follow_control(&transport, &fresh).await {
2852 Ok(Some(changed)) => {
2856 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &changed, &session).await {
2857 warnings.push(format!("v2 rekey follow failed: {e}"));
2858 }
2859 }
2860 Ok(None) => {}
2861 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
2862 }
2863 }
2864 if let Some(me) = crate::my_public_key() {
2869 if crate::db::community::is_author_banned(&cid_hex, &me) {
2870 let _ = crate::db::community::delete_community(&cid_hex);
2871 }
2872 }
2873 warnings
2874 })
2875 .await
2876 }
2877
2878 pub(crate) async fn v2_backfill_channel(
2890 id: &crate::community::CommunityId,
2891 channel_id: &str,
2892 limit: usize,
2893 max_pages: usize,
2894 since: Option<u64>,
2895 until: Option<u64>,
2896 evidence: crate::community::transport::Evidence,
2897 transport_secs: u64,
2898 ) -> usize {
2899 Self::v2_backfill_channel_counted(id, channel_id, limit, max_pages, since, until, evidence, transport_secs)
2900 .await
2901 .new_messages
2902 }
2903
2904 pub(crate) async fn v2_backfill_channel_counted(
2909 id: &crate::community::CommunityId,
2910 channel_id: &str,
2911 limit: usize,
2912 max_pages: usize,
2913 since: Option<u64>,
2914 until: Option<u64>,
2915 evidence: crate::community::transport::Evidence,
2916 transport_secs: u64,
2917 ) -> BackfillCount {
2918 let session = crate::db::current_session();
2921 let Some(my_pk) = state::my_public_key() else { return BackfillCount::default() };
2922 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
2926 return BackfillCount::default();
2927 }
2928 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return BackfillCount::default() };
2929 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2930 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
2931 let Ok(page) = crate::community::v2::service::fetch_channel_history(
2932 &transport,
2933 &community,
2934 &ch,
2935 limit.max(50),
2936 max_pages,
2937 since,
2938 until,
2939 evidence,
2940 |page| {
2945 let mut saw_message = false;
2946 for f in page {
2947 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
2948 saw_message = true;
2949 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
2950 return true;
2951 }
2952 }
2953 }
2954 !saw_message
2955 },
2956 )
2957 .await
2958 else {
2959 return BackfillCount::default();
2960 };
2961 let fetched = page.len();
2962 let new_messages = Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await;
2963 BackfillCount { fetched, new_messages }
2964 }
2965
2966 pub(crate) async fn v2_ingest_chat_page(
2970 channel_id: &str,
2971 my_pk: nostr_sdk::prelude::PublicKey,
2972 session: std::sync::Arc<crate::db::Session>,
2973 page: Vec<crate::community::v2::service::FetchedEvent>,
2974 ) -> usize {
2975 crate::db::scoped(async move {
2976 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
2977 let mut new = 0usize;
2978 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
2980 for f in &page {
2981 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
2986 if opened.author != my_pk {
2987 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
2988 let Ok(npub) = ToBech32::to_bech32(&opened.author);
2989 crate::community::service::persist_webxdc_signal(
2990 channel_id,
2991 &npub,
2992 &topic,
2993 addr.as_deref(),
2994 &opened.rumor_id.to_hex(),
2995 opened.at_ms / 1000,
2996 )
2997 .await;
2998 }
2999 }
3000 continue;
3001 }
3002 let outcome = {
3003 let mut st = state::STATE.lock().await;
3004 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
3005 };
3006 if let Some(outcome) = outcome {
3007 if matches!(outcome, ChatPersist::New(_)) {
3008 new += 1;
3009 }
3010 outcomes.push(outcome);
3011 }
3012 }
3013 let mut pending: Vec<&crate::types::Message> = Vec::new();
3017 for outcome in &outcomes {
3018 if !session.is_live() {
3019 pending.clear();
3020 break;
3021 }
3022 match outcome {
3023 ChatPersist::New(m) => pending.push(m),
3024 ChatPersist::Updated { message, edit_event } => match edit_event {
3025 Some(ev) => {
3026 let mut ev = (**ev).clone();
3027 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
3030 ev.chat_id = cid;
3031 }
3032 let _ = crate::db::events::save_event(&ev).await;
3033 }
3034 None => pending.push(message),
3035 },
3036 ChatPersist::Removed(target_id) => {
3037 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
3038 let _ = crate::db::events::delete_event(target_id).await;
3039 }
3040 ChatPersist::ReactionRemoved { reaction_id, message } => {
3041 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
3042 let _ = crate::db::events::delete_event(reaction_id).await;
3043 pending.push(message);
3044 }
3045 }
3046 }
3047 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
3048 drop(pending);
3049 {
3053 let quoted: Vec<&mut crate::types::Message> = outcomes
3054 .iter_mut()
3055 .filter_map(|o| match o {
3056 ChatPersist::New(m)
3057 | ChatPersist::Updated { message: m, .. }
3058 | ChatPersist::ReactionRemoved { message: m, .. } => Some(m),
3059 ChatPersist::Removed(_) => None,
3060 })
3061 .collect();
3062 let _ = crate::db::events::populate_reply_contexts(quoted).await;
3063 }
3064 for outcome in &outcomes {
3070 match outcome {
3071 ChatPersist::New(msg) => crate::traits::emit_event(
3072 "message_new",
3073 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
3074 ),
3075 ChatPersist::Updated { message, .. }
3076 | ChatPersist::ReactionRemoved { message, .. } => {
3077 let mut message = message.clone();
3078 let target_id = message.id.clone();
3079 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
3080 }
3081 ChatPersist::Removed(target_id) => crate::traits::emit_event(
3082 "message_removed",
3083 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
3084 ),
3085 }
3086 }
3087 new
3088 })
3089 .await
3090 }
3091
3092 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
3096 if community_id.len() != 64 {
3097 return Ok(None);
3098 }
3099 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3100 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
3101 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
3102 _ => Ok(None),
3103 }
3104 }
3105
3106 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
3111 use crate::community::CommunityId;
3112 if community_id.len() != 64 {
3113 return Err(VectorError::Other("malformed community id".into()));
3114 }
3115 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
3116 .map_err(VectorError::Other)?
3117 .ok_or_else(|| VectorError::Other("community not found".into()))
3118 }
3119
3120 fn admin_role_id_of(community_id: &str) -> Result<String> {
3121 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3122 roles.roles.iter()
3125 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
3126 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_FOUNDING_MASK))
3127 .map(|r| r.role_id.clone())
3128 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
3129 }
3130
3131 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
3135 use crate::community::service;
3136 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3137 use crate::community::roles::Permissions;
3138 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
3139 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
3140 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3141 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
3144 if banned.contains(&me) && me != owner_hex {
3145 return Ok(serde_json::json!({
3146 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
3147 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
3148 "pin_messages": false, "control_write": false,
3149 }));
3150 }
3151 let control_write = crate::community::v2::control::ControlPlane::of(&v2).can_write();
3157 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
3158 let has_ctl = |p: u64| control_write && has(p);
3159 return Ok(serde_json::json!({
3160 "manage_metadata": has_ctl(Permissions::MANAGE_METADATA), "manage_channels": has_ctl(Permissions::MANAGE_CHANNELS),
3161 "create_invite": has_ctl(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has_ctl(Permissions::BAN),
3162 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has_ctl(Permissions::MANAGE_ROLES),
3163 "manage_admin_role": me == owner_hex && control_write,
3165 "pin_messages": has_ctl(Permissions::PIN_MESSAGES),
3166 "control_write": control_write,
3167 }));
3168 }
3169 let community = Self::load_community_hex(community_id)?;
3170 let caps = service::caller_capabilities(&community);
3171 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
3172 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
3173 .unwrap_or(false);
3174 Ok(serde_json::json!({
3175 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
3176 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
3177 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
3178 "manage_admin_role": manage_admin_role,
3179 "pin_messages": false,
3181 }))
3182 }
3183
3184 pub async fn pin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
3187 use crate::community::{transport::LiveTransport, ChannelId};
3188 let v2 = Self::load_v2_if_v2(community_id)?
3189 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3190 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3191 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3192 crate::community::v2::service::pin_message(&transport, &v2, &ch, message_id)
3193 .await
3194 .map_err(VectorError::Other)
3195 }
3196
3197 pub async fn unpin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
3199 use crate::community::{transport::LiveTransport, ChannelId};
3200 let v2 = Self::load_v2_if_v2(community_id)?
3201 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3202 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3203 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3204 crate::community::v2::service::unpin_message(&transport, &v2, &ch, message_id)
3205 .await
3206 .map_err(VectorError::Other)
3207 }
3208
3209 pub async fn fetch_pinned_attachment(
3217 &self,
3218 community_id: &str,
3219 channel_id: &str,
3220 rumor_id: &str,
3221 ) -> Result<serde_json::Value> {
3222 crate::db::scoped(async move {
3223 use crate::community::ChannelId;
3224 let v2 = Self::load_v2_if_v2(community_id)?
3225 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
3226 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3227 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
3228 let pin = pins
3229 .pins
3230 .iter()
3231 .find(|p| p.rumor_id == rumor_id)
3232 .ok_or_else(|| VectorError::Other("that message is not pinned".into()))?;
3233 let dir = crate::db::get_download_dir();
3234 let tag = pin
3235 .tags
3236 .iter()
3237 .find(|t| t.first().map(String::as_str) == Some("imeta"))
3238 .map(|t| nostr_sdk::prelude::Tag::custom("imeta", t[1..].to_vec()))
3239 .ok_or_else(|| VectorError::Other("this pin carries no attachment".into()))?;
3240 let attachment = crate::community::attachments::attachment_from_imeta(&tag, &dir)
3241 .ok_or_else(|| VectorError::Other("this pin's attachment metadata is malformed".into()))?;
3242
3243 let respond = |path: &std::path::Path| {
3244 serde_json::json!({
3245 "path": path.to_string_lossy(),
3246 "name": attachment.name.to_string(),
3247 "extension": attachment.extension.to_string(),
3248 })
3249 };
3250
3251 let expected = attachment.original_hash.as_deref();
3255 let path = std::path::PathBuf::from(&*attachment.path);
3256 if let Ok(bytes) = std::fs::read(&path) {
3257 match expected {
3258 Some(want) if crate::crypto::sha256_hex(&bytes) == want => return Ok(respond(&path)),
3259 None => return Ok(respond(&path)),
3260 _ => {} }
3262 }
3263
3264 let author_npub = nostr_sdk::prelude::PublicKey::from_hex(&pin.author)
3265 .ok()
3266 .and_then(|pk| nostr_sdk::prelude::ToBech32::to_bech32(&pk).ok());
3267 let bytes = self.download_attachment_from(&attachment, author_npub.as_deref()).await?;
3268 if let Some(want) = expected {
3269 if crate::crypto::sha256_hex(&bytes) != want {
3270 return Err(VectorError::Other("downloaded bytes do not match the pinned content hash".into()));
3271 }
3272 }
3273 std::fs::write(&path, &bytes).map_err(|e| VectorError::Other(format!("could not cache the attachment: {e}")))?;
3274 Ok(respond(&path))
3275 })
3276 .await
3277 }
3278
3279 pub fn get_channel_pins(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
3283 use crate::community::ChannelId;
3284 let Some(v2) = Self::load_v2_if_v2(community_id)? else {
3285 return Ok(serde_json::json!({ "pins": [], "sealed": false, "version": 0 }));
3288 };
3289 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3290 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
3291 serde_json::to_value(&pins).map_err(|e| VectorError::Other(e.to_string()))
3292 }
3293
3294 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
3297 use nostr_sdk::prelude::{PublicKey, ToBech32};
3298 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3299 let owner = v2.owner().map_err(VectorError::Other)?;
3300 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3301 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
3303 let admins: Vec<String> = roster.grants.iter()
3304 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
3305 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3306 .collect();
3307 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
3308 }
3309 let community = Self::load_community_hex(community_id)?;
3310 let owner = community.owner_attestation.as_ref()
3311 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
3312 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
3313 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
3314 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
3315 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3316 .collect();
3317 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
3318 }
3319
3320 async fn converge_v2_authority(
3329 transport: &crate::community::transport::LiveTransport,
3330 community_id: &str,
3331 ) {
3332 crate::db::scoped(async move {
3333 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
3336 let _ = crate::community::v2::service::follow_control(transport, &fresh).await;
3337 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh).await {
3342 if !added.is_empty() {
3343 traits::emit_event_json(
3344 "community_refreshed",
3345 serde_json::json!({ "community_id": community_id }),
3346 );
3347 }
3348 }
3349 }
3350 })
3351 .await
3352 }
3353
3354 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3356 use crate::community::{service, transport::LiveTransport};
3357 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3358 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3359 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3360 crate::community::v2::service::grant_admin(&transport, &v2, &member)
3361 .await
3362 .map_err(VectorError::Other)?;
3363 Self::converge_v2_authority(&transport, community_id).await;
3364 return Ok(());
3365 }
3366 let community = Self::load_community_hex(community_id)?;
3367 let role_id = Self::admin_role_id_of(community_id)?;
3368 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3369 }
3370
3371 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
3373 use crate::community::{service, transport::LiveTransport};
3374 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3375 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3376 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3377 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
3378 .await
3379 .map_err(VectorError::Other)?;
3380 Self::converge_v2_authority(&transport, community_id).await;
3381 return Ok(());
3382 }
3383 let community = Self::load_community_hex(community_id)?;
3384 let role_id = Self::admin_role_id_of(community_id)?;
3385 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
3386 }
3387
3388 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
3390 crate::db::scoped(async move {
3391 use crate::community::{service, transport::LiveTransport};
3392 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
3393 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3394 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3395 crate::community::v2::service::kick_member(&transport, &v2, &pk)
3396 .await
3397 .map_err(VectorError::Other)?;
3398 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2).await {
3403 if !fresh.is_empty() {
3404 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
3405 }
3406 }
3407 Self::converge_v2_authority(&transport, community_id).await;
3408 return Ok(());
3409 }
3410 let community = Self::load_community_hex(community_id)?;
3411 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
3412 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
3413 })
3414 .await
3415 }
3416
3417 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
3423 self.set_members_banned(community_id, &[npub], banned).await
3424 }
3425
3426 pub async fn set_members_banned(&self, community_id: &str, npubs: &[&str], banned: bool) -> Result<()> {
3436 use crate::community::{service, transport::LiveTransport, CommunityId};
3437 if npubs.is_empty() {
3438 return Ok(());
3439 }
3440 let mut pks: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(npubs.len());
3442 for n in npubs {
3443 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
3444 if !pks.contains(&pk) {
3445 pks.push(pk);
3446 }
3447 }
3448 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3449 if community_id.len() == 64 {
3453 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3454 if let Some(crate::community::ConcordProtocol::V2) = crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
3458 return crate::community::v2::service::set_members_banned(&transport, &cid, &pks, banned)
3459 .await
3460 .map_err(VectorError::Other);
3461 }
3462 }
3463 let hexes: Vec<String> = pks.iter().map(|p| p.to_hex()).collect();
3465 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
3466 list.retain(|h| !hexes.contains(h));
3467 if banned {
3468 list.extend(hexes);
3469 }
3470 let community = Self::load_community_hex(community_id)?;
3471 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)
3472 }
3473
3474 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
3478 use crate::community::{service, transport::LiveTransport, CommunityId};
3479 if community_id.len() != 64 {
3480 return Err(VectorError::Other("malformed community id".into()));
3481 }
3482 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3483 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3484 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3487 let community = crate::db::community::load_community_v2(&cid)
3488 .map_err(VectorError::Other)?
3489 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3490 return crate::community::v2::service::dissolve_community(&transport, &community)
3491 .await
3492 .map_err(VectorError::Other);
3493 }
3494 let community = Self::load_community_hex(community_id)?;
3495 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
3496 }
3497
3498 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
3501 use crate::community::{service, transport::LiveTransport, CommunityId};
3502 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3503 if community_id.len() == 64 {
3508 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3509 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
3510 let community = crate::db::community::load_community_v2(&cid)
3511 .map_err(VectorError::Other)?
3512 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3513 let mut meta = community.metadata();
3514 if let Some(n) = name {
3515 meta.name = n.to_string();
3516 }
3517 if let Some(d) = description {
3518 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
3519 }
3520 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
3521 .await
3522 .map_err(VectorError::Other);
3523 }
3524 }
3525 let mut community = Self::load_community_hex(community_id)?;
3526 if let Some(n) = name { community.name = n.to_string(); }
3527 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
3528 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
3529 }
3530
3531
3532
3533 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
3536 crate::db::scoped(async move {
3537 use crate::community::{transport::LiveTransport, CommunityId};
3538 if community_id.len() != 64 {
3539 return Err(VectorError::Other("malformed community id".into()));
3540 }
3541 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
3542 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
3544 let channel_ids: Vec<String> =
3545 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
3546 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3547 crate::community::v2::service::leave_community(&transport, &v2)
3548 .await
3549 .map_err(VectorError::Other)?;
3550 let mut st = state::STATE.lock().await;
3551 st.chats.retain(|c| !channel_ids.contains(&c.id));
3552 return Ok(());
3553 }
3554 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
3555 let channel_ids: Vec<String> = community
3556 .as_ref()
3557 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
3558 .unwrap_or_default();
3559 if let Some(ref c) = community {
3561 if let Some(primary) = c.channels.first() {
3562 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3563 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
3564 }
3565 }
3566 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
3568 {
3569 let mut st = state::STATE.lock().await;
3570 st.chats.retain(|c| !channel_ids.contains(&c.id));
3571 }
3572 Ok(())
3573 })
3574 .await
3575 }
3576
3577 fn ensure_v1_writable(community: &crate::community::Community) -> Result<()> {
3585 if crate::db::community::get_community_dissolved(&community.id.to_hex()).unwrap_or(false) {
3586 return Err(VectorError::Other("this community has been dissolved".into()));
3587 }
3588 Ok(())
3589 }
3590
3591 fn resolve_channel(
3592 &self,
3593 channel_id: &str,
3594 ) -> Result<(crate::community::Community, crate::community::Channel)> {
3595 use crate::community::CommunityId;
3596 let community_id = crate::db::community::community_id_for_channel(channel_id)
3597 .map_err(VectorError::Other)?
3598 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
3599 if community_id.len() != 64 {
3600 return Err(VectorError::Other("malformed community id".into()));
3601 }
3602 let community = crate::db::community::load_community(&CommunityId(
3603 crate::simd::hex::hex_to_bytes_32(&community_id),
3604 ))
3605 .map_err(VectorError::Other)?
3606 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
3607 let channel = community
3608 .channels
3609 .iter()
3610 .find(|c| c.id.to_hex() == channel_id)
3611 .cloned()
3612 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
3613 Ok((community, channel))
3614 }
3615
3616
3617 pub async fn sync_dms(
3634 &self,
3635 since_days: Option<u64>,
3636 handler: &dyn InboundEventHandler,
3637 ) -> Result<(u32, u32)> {
3638 crate::db::scoped(async move {
3639 use futures_util::StreamExt;
3640 use nostr_sdk::prelude::*;
3641
3642 let client = state::nostr_client()
3643 .ok_or(VectorError::Other("Not connected".into()))?;
3644 let my_pk = state::my_public_key()
3645 .ok_or(VectorError::Other("Not logged in".into()))?;
3646
3647 let (items, filter) = if let Some(days) = since_days {
3652 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
3653 let items = db::wrappers::load_negentropy_items_since(since_ts)
3654 .unwrap_or_default();
3655 let filter = Filter::new()
3656 .pubkey(my_pk)
3657 .kind(Kind::GiftWrap)
3658 .since(Timestamp::from_secs(since_ts));
3659 (items, filter)
3660 } else {
3661 let items = db::wrappers::load_negentropy_items().unwrap_or_default();
3662 let filter = Filter::new()
3663 .pubkey(my_pk)
3664 .kind(Kind::GiftWrap);
3665 (items, filter)
3666 };
3667
3668 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
3669
3670 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
3672 .direction(nostr_sdk::prelude::SyncDirection::Down)
3673 .initial_timeout(std::time::Duration::from_secs(10))
3674 .dry_run();
3675
3676 let relay_map = client.relays().await;
3680 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
3681 relay_map.iter()
3682 .map(|(url, relay)| (url.clone(), relay.clone()))
3683 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
3684 drop(relay_map);
3685 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
3686 if !skipped_no_neg.is_empty() {
3687 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
3688 }
3689
3690 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
3694 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
3695 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
3696 .min(neg_outer);
3697 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
3698 for (url, relay) in &all_relays {
3699 let url = url.clone();
3700 let relay = relay.clone();
3701 let f = filter.clone();
3702 let i = items.clone();
3703 let o = sync_opts.clone();
3704 relay_futs.push(async move {
3705 if !negentropy::wait_connected(&relay, connect_allowance).await {
3706 return (url, None, false);
3707 }
3708 let result = tokio::time::timeout(
3711 neg_outer,
3712 relay.sync(f).items(i).opts(o),
3713 ).await;
3714 let connected = relay.status() == RelayStatus::Connected;
3715 (url, Some(result), connected)
3716 });
3717 }
3718
3719 let cap_session = crate::db::current_session();
3721 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
3722 while let Some((url, result, connected)) = relay_futs.next().await {
3723 let Some(result) = result else {
3724 log_warn!("[SyncDMs] {} skipped: not connected", url);
3725 continue;
3726 };
3727 match result {
3728 Ok(Ok(recon)) => {
3729 let count = recon.remote.len();
3730 all_missing.extend(recon.remote);
3731 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
3732 negentropy::record_neg_support(url.as_str(), true);
3733 }
3734 Ok(Err(e)) => {
3735 log_warn!("[SyncDMs] {} failed: {}", url, e);
3736 if cap_session.is_live()
3737 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
3738 {
3739 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
3740 negentropy::record_neg_support(url.as_str(), false);
3741 }
3742 }
3743 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
3744 }
3745 }
3746
3747 let mut total_events = 0u32;
3748 let mut new_messages = 0u32;
3749
3750 if !skipped_no_neg.is_empty() {
3755 let req_filter = filter.clone().limit(500);
3756 match client
3757 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3758 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
3759 ))
3760 .timeout(std::time::Duration::from_secs(20))
3761 .await
3762 {
3763 Ok(stream) => {
3764 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
3765 tokio::pin!(stream);
3766 while let Some((_relay, res)) = stream.next().await {
3767 let Ok(event) = res else { continue };
3768 if !seen.insert(event.id.to_bytes()) { continue; }
3772 total_events += 1;
3773 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
3774 if event_handler::commit_prepared_event(prepared, false, handler).await {
3775 new_messages += 1;
3776 }
3777 }
3778 }
3779 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
3780 }
3781 }
3782
3783 if all_missing.is_empty() {
3784 log_info!("[SyncDMs] No missing events");
3785 return Ok((total_events, new_messages));
3786 }
3787
3788 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
3790 let ids: Vec<EventId> = all_missing.into_iter().collect();
3791 let relay_strs: Vec<String> = client.relays().await.keys()
3792 .map(|u| u.to_string()).collect();
3793
3794 const BATCH_SIZE: usize = 500;
3795
3796 for batch in ids.chunks(BATCH_SIZE) {
3797 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
3800 match client
3801 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
3802 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
3803 ))
3804 .timeout(std::time::Duration::from_secs(30))
3805 .await
3806 {
3807 Ok(stream) => {
3808 let client_clone = client.clone();
3809 let prepared_stream = stream
3810 .filter_map(|(_relay, res)| async move { res.ok() })
3811 .map(move |event| {
3812 let c = client_clone.clone();
3813 db::spawn_bound(async move {
3814 event_handler::prepare_event(event, &c, my_pk).await
3815 })
3816 })
3817 .buffer_unordered(8);
3818 tokio::pin!(prepared_stream);
3819
3820 while let Some(result) = prepared_stream.next().await {
3821 total_events += 1;
3822 if let Ok(prepared) = result {
3823 if event_handler::commit_prepared_event(prepared, false, handler).await {
3824 new_messages += 1;
3825 }
3826 }
3827 }
3828 }
3829 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
3830 }
3831 }
3832
3833 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
3834 Ok((total_events, new_messages))
3835 })
3836 .await
3837 }
3838
3839 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
3848 use nostr_sdk::prelude::*;
3849 let client = state::nostr_client()
3850 .ok_or(VectorError::Other("Not connected".into()))?;
3851 let my_pk = state::my_public_key()
3852 .ok_or(VectorError::Other("Not logged in".into()))?;
3853
3854 let filter = Filter::new()
3855 .pubkey(my_pk)
3856 .kind(Kind::GiftWrap)
3857 .limit(0);
3858
3859 let output = client.subscribe(filter).await
3860 .map_err(|e| VectorError::Nostr(e.to_string()))?;
3861 Ok(output.value)
3862 }
3863
3864 pub async fn sync_communities(&self) -> Result<()> {
3875 {
3879 use crate::community::{transport::LiveTransport, v2::service as v2};
3880 let bootstrap: Vec<String> = match crate::state::nostr_client() {
3881 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
3882 None => Vec::new(),
3883 };
3884 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3885 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
3886 let joined = outcome.joined;
3889 for c in &joined {
3890 if community::v2::realtime::follow_worker_running() {
3891 community::v2::realtime::enqueue_follow(c.id());
3892 } else {
3893 let _ = Self::v2_inline_follow(c.id()).await;
3894 }
3895 }
3896 if !joined.is_empty() {
3897 if let Some(client) = crate::state::nostr_client() {
3898 community::v2::realtime::refresh_subscription(&client).await;
3899 }
3900 }
3901 }
3902 }
3903
3904 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
3905 for id in ids {
3906 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
3907 if community::v2::realtime::follow_worker_running() {
3910 community::v2::realtime::enqueue_follow(&id);
3911 } else {
3912 let _ = Self::v2_inline_follow(&id).await;
3913 }
3914 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
3921 for ch in &c.channels {
3922 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
3923 let _ = Self::v2_backfill_channel(
3924 &id, &hex, 50, 2, None, None,
3925 crate::community::transport::Evidence::Fast, 12,
3926 ).await;
3927 }
3928 }
3929 continue;
3930 }
3931 if let Ok(Some(community)) = db::community::load_community(&id) {
3932 for ch in &community.channels {
3933 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
3934 }
3935 }
3936 }
3937 Ok(())
3938 }
3939
3940
3941 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
3973 use nostr_sdk::prelude::*;
3974
3975 let client = state::nostr_client()
3976 .ok_or(VectorError::Other("Not connected".into()))?;
3977 let my_pk = state::my_public_key()
3978 .ok_or(VectorError::Other("Not logged in".into()))?;
3979
3980 community::v2::streamauth::ensure_responder(&client);
3987
3988 community::v2::realtime::spawn_follow_worker(handler.clone());
3997 let _ = self.sync_communities().await;
3998 let _ = self.sync_dms(None, &NoOpEventHandler).await;
3999
4000 let dm_sub_id = self.subscribe_dms().await?;
4003 community::realtime::refresh_subscription(&client).await;
4004 community::v2::realtime::refresh_subscription(&client).await;
4005
4006 handler.on_subscription_ready(db::community::list_community_ids().map(|v| v.len()).unwrap_or(0));
4012
4013 if let Some(monitor) = client.monitor() {
4020 let mut rx = monitor.subscribe();
4021 db::spawn_bound(async move {
4022 let mut last_resync: Option<std::time::Instant> = None;
4025 while let Ok(notification) = rx.recv().await {
4026 let MonitorNotification::StatusChanged { status, .. } = notification;
4027 if status == RelayStatus::Connected {
4028 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
4029 continue;
4030 }
4031 let _ = VectorCore.sync_communities().await;
4032 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
4033 if let Some(c) = state::nostr_client() {
4034 community::realtime::refresh_subscription(&c).await;
4035 community::v2::realtime::refresh_subscription(&c).await;
4036 }
4037 last_resync = Some(std::time::Instant::now());
4038 }
4039 }
4040 });
4041 }
4042
4043 {
4047 let client_health = client.clone();
4048 db::spawn_bound(async move {
4049 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
4051 for (url, relay) in client_health.relays().await {
4052 match relay.status() {
4053 RelayStatus::Connected => {
4054 let probe = tokio::time::timeout(
4055 std::time::Duration::from_secs(10),
4056 client_health
4057 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
4058 url.to_string(),
4059 [Filter::new().kind(Kind::Metadata).limit(1)],
4060 ))
4061 .timeout(std::time::Duration::from_secs(8)),
4062 )
4063 .await;
4064 if !matches!(probe, Ok(Ok(_))) {
4065 let _ = relay.disconnect();
4066 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
4067 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
4068 }
4069 }
4070 RelayStatus::Terminated | RelayStatus::Disconnected => {
4071 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
4072 }
4073 _ => {}
4074 }
4075 }
4076 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
4077 }
4078 });
4079 }
4080
4081 let client_for_closure = client.clone();
4082
4083 let mut notifications = client.notifications();
4086 while let Some(notification) = notifications.next().await {
4087 let handler = handler.clone();
4088 let c = client_for_closure.clone();
4089 let dm_sid = dm_sub_id.clone();
4090 {
4091 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
4095 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
4096 sending::note_relay_ok(event_id, *status);
4097 }
4098 }
4099 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
4100 if subscription_id == dm_sid {
4101 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
4103 event_handler::commit_prepared_event(prepared, true, &*handler).await;
4104 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
4105 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
4106 {
4107 community::realtime::dispatch_event(*event, handler.clone()).await;
4111 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
4112 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
4113 {
4114 community::v2::realtime::dispatch_event(*event, handler.clone()).await;
4116 }
4117 }
4118 }
4119 }
4120
4121 Ok(())
4122 }
4123
4124 pub async fn logout(&self) {
4126 if let Some(client) = state::nostr_client() {
4127 let _ = client.disconnect().await;
4128 }
4129 db::close_database();
4130 }
4131
4132 pub async fn swap_session(&self) {
4140 state::clear_message_tombstones();
4142
4143 if let Some(client) = state::take_nostr_client() {
4146 let _ = client.shutdown().await;
4147 }
4148 db::close_database();
4149
4150 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
4152 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
4153 {
4154 use zeroize::Zeroize;
4155 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
4156 if let Some(s) = g.as_mut() { s.zeroize(); }
4157 *g = None;
4158 }
4159 if let Ok(mut g) = state::PENDING_NSEC.lock() {
4160 if let Some(s) = g.as_mut() { s.zeroize(); }
4161 *g = None;
4162 }
4163 }
4164
4165 crate::community::realtime::clear().await;
4173 crate::community::v2::realtime::clear().await;
4174 crate::community::transport::clear_plane_pool();
4177 }
4178}
4179
4180#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
4181mod transport_policy_tests {
4182 use std::time::Duration;
4183
4184 #[test]
4187 fn tor_transport_policy() {
4188 let short = Duration::from_secs(5);
4189 let long = Duration::from_secs(300);
4190
4191 crate::tor::set_tor_enabled_pref(false);
4194 assert_eq!(super::tor_proxy_target(), None);
4195 assert_eq!(super::relay_connect_timeout(short), short);
4196 assert_eq!(super::relay_request_timeout(short), short);
4197
4198 crate::tor::set_tor_enabled_pref(true);
4202 assert!(matches!(
4203 crate::tor::transport_state(),
4204 crate::tor::TorTransportState::RequiredButInactive
4205 ));
4206 assert_eq!(
4212 super::tor_proxy_target(),
4213 Some(crate::tor::blackhole_proxy_addr()),
4214 "Tor enabled but inactive must blackhole, never connect direct"
4215 );
4216 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
4217 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
4218
4219 for tor in [true, false] {
4222 crate::tor::set_tor_enabled_pref(tor);
4223 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
4224 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
4225 }
4226 }
4227}
4228
4229#[cfg(test)]
4230mod facade_tests {
4231 use super::*;
4232
4233 #[tokio::test]
4236 async fn download_attachment_rejects_private_url() {
4237 let att = crate::types::Attachment {
4238 url: "http://169.254.169.254/latest/meta-data/".to_string(),
4239 ..Default::default()
4240 };
4241 match VectorCore.download_attachment(&att).await {
4242 Err(VectorError::Other(msg)) => {
4243 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
4244 }
4245 other => panic!("expected SSRF rejection, got {other:?}"),
4246 }
4247 }
4248
4249 #[tokio::test]
4250 async fn download_attachment_rejects_empty_url() {
4251 let att = crate::types::Attachment::default();
4252 assert!(VectorCore.download_attachment(&att).await.is_err());
4253 }
4254
4255 #[tokio::test]
4259 async fn list_communities_and_channel_routing_are_protocol_aware() {
4260 use crate::community::transport::memory::MemoryRelay;
4261 use nostr_sdk::prelude::Keys;
4262
4263 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
4264 crate::db::close_database();
4265 crate::db::clear_id_caches();
4266 let tmp = tempfile::tempdir().unwrap();
4267 let acct = {
4269 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
4270 let mut s = String::from("npub1");
4271 for i in 0..58 {
4272 s.push(B[(i * 7 + 3) % 32] as char);
4273 }
4274 s
4275 };
4276 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
4277 crate::db::set_app_data_dir(crate::db::shared_test_data_dir().to_path_buf());
4278 crate::db::set_current_account(acct.clone()).unwrap();
4279 crate::db::init_database(&acct).unwrap();
4280 let _ = crate::state::take_nostr_client();
4281 let me = Keys::generate();
4282 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
4283 crate::state::set_my_public_key(me.public_key());
4284
4285 let relay = MemoryRelay::new();
4287 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
4288 .await
4289 .unwrap();
4290 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
4291
4292 let listed = VectorCore.list_communities().await;
4294 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
4295 assert_eq!(v2["name"], "V2 Guild");
4296 assert_eq!(v2["is_owner"], true);
4297 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
4298
4299 assert_eq!(
4301 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
4302 Some(community.identity.community_id),
4303 "a v2 channel is routed to v2"
4304 );
4305 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
4307 }
4308
4309 #[test]
4314 fn v2_invite_url_base_derivation_round_trips() {
4315 use crate::community::v2::derive::TOKEN_LEN;
4316 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
4317 use nostr_sdk::prelude::Keys;
4318 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
4319 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
4320 let signer = Keys::generate();
4321 let token = [0x07u8; TOKEN_LEN];
4322 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
4323 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
4324 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
4325 let parsed = parse_invite_link(&url).unwrap();
4326 assert_eq!(parsed.link_signer, signer.public_key());
4327 assert_eq!(parsed.token, token);
4328 }
4329}
4330
4331#[cfg(test)]
4332mod history_paging_tests {
4333 use super::*;
4334
4335 fn msg(at: u64, id_byte: u8, content: &str) -> Message {
4336 Message {
4337 id: format!("{:02x}", id_byte).repeat(32),
4338 content: content.to_string(),
4339 at,
4340 ..Default::default()
4341 }
4342 }
4343
4344 #[tokio::test]
4348 async fn history_pages_through_a_same_ms_wall_and_a_deleted_cursor() {
4349 let chat_id = "test-history-paging-wall";
4350 {
4351 let mut st = state::STATE.lock().await;
4352 st.ensure_community_chat(chat_id);
4353 for m in [msg(500, 0x01, "old"), msg(900, 0xaa, "wall-a"), msg(900, 0xbb, "wall-b"), msg(900, 0xcc, "wall-c")] {
4355 st.add_message_to_chat(chat_id, &m);
4356 }
4357 }
4358 let core = VectorCore;
4359
4360 let newest = core.get_messages_before(chat_id, None, 2).await;
4362 assert_eq!(newest.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["wall-b", "wall-c"]);
4363
4364 let cursor = (newest[0].at, newest[0].id.as_str().to_string());
4366 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
4367 assert_eq!(page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["old", "wall-a"]);
4368
4369 {
4372 let mut st = state::STATE.lock().await;
4373 let chat = st.get_chat_mut(chat_id).unwrap();
4374 chat.messages.remove_by_hex_id(&cursor.1);
4375 }
4376 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
4377 assert_eq!(
4378 page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(),
4379 ["old", "wall-a"],
4380 "a deleted cursor pages identically"
4381 );
4382
4383 assert!(core.get_messages_before("test-history-paging-nochat", None, 5).await.is_empty());
4385 }
4386}