1#[macro_use]
29mod macros;
30
31pub mod rt;
33pub mod files;
34#[cfg(target_arch = "wasm32")]
35pub mod webfiles;
36pub mod logging;
37pub mod error;
38pub mod traits;
39
40use crate::event_ext::FinalizeUnsignedWithId;
42use nostr_sdk::prelude::{FinalizeEventAsync, ToBech32};
43
44pub mod event_ext;
46pub mod tags;
47pub mod types;
48pub mod profile;
49pub mod chat;
50pub mod compact;
51
52pub mod state;
54
55#[cfg(debug_assertions)]
57pub mod stats;
58
59pub mod crypto;
61
62pub mod signer;
64
65pub mod nip55;
67
68pub mod nip07;
70
71pub mod db;
73pub mod spawn_audit;
76
77pub mod net;
79pub mod proxy;
80pub mod negentropy;
81pub mod blossom;
82pub mod blossom_servers;
83pub mod blossom_capabilities;
84pub mod blossom_error;
85pub mod blossom_info;
86pub mod blossom_stats;
87pub mod inbox_relays;
88pub mod emoji_packs;
89pub mod emoji_usage;
90pub mod badges;
91pub mod bot_interface;
92pub mod webxdc;
93pub mod webxdc_permissions;
94#[cfg(feature = "xdc")]
95pub mod xdc;
96#[cfg(feature = "tor")]
97pub mod tor;
98
99#[derive(Debug)]
111pub struct VectorAuthenticator;
112
113impl nostr_sdk::prelude::Authenticator for VectorAuthenticator {
114 fn make_auth_event<'a>(
115 &'a self,
116 relay_url: &'a nostr_sdk::prelude::RelayUrl,
117 challenge: &'a str,
118 ) -> signer::BoxedFuture<'a, std::result::Result<nostr_sdk::prelude::Event, nostr_sdk::prelude::Error>>
119 {
120 Box::pin(async move {
121 let signer =
122 signer::active_signer().map_err(nostr_sdk::prelude::Error::other)?;
123 Ok(
124 nostr_sdk::prelude::ClientAuthentication::new(challenge, relay_url.clone())
125 .finalize_async(&signer)
126 .await?,
127 )
128 })
129 }
130}
131
132pub fn nostr_client_builder() -> nostr_sdk::prelude::ClientBuilder {
143 apply_tor_proxy(
144 nostr_sdk::prelude::ClientBuilder::new()
145 .authenticator(VectorAuthenticator)
146 .connect_timeout(relay_connect_timeout(std::time::Duration::from_secs(15))),
149 )
150}
151
152pub trait ClientRelayExt {
164 fn add_managed_relay<'client, 'url, U>(
166 &'client self,
167 url: U,
168 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
169 where
170 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>;
171}
172
173impl ClientRelayExt for nostr_sdk::prelude::Client {
174 fn add_managed_relay<'client, 'url, U>(
175 &'client self,
176 url: U,
177 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
178 where
179 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>,
180 {
181 self.add_relay(url).reconnect(false)
182 }
183}
184
185pub async fn resubscribe_relay_after_reconnect(
200 client: &nostr_sdk::prelude::Client,
201 relay: &nostr_sdk::prelude::RelayUrl,
202) {
203 for (id, per_relay) in client.subscriptions().await {
204 let Some(filters) = per_relay.get(relay) else { continue };
205 if filters.is_empty() {
206 continue;
207 }
208 let _ = client
209 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), filters.clone()))
210 .with_id(id)
211 .await;
212 }
213}
214
215#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
220const TOR_RELAY_CONNECT_FLOOR: std::time::Duration = std::time::Duration::from_secs(60);
221
222pub fn relay_connect_timeout(clearnet: std::time::Duration) -> std::time::Duration {
231 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
232 {
233 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
234 return clearnet.max(TOR_RELAY_CONNECT_FLOOR);
235 }
236 }
237 clearnet
238}
239
240#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
242const TOR_RELAY_REQUEST_FLOOR: std::time::Duration = std::time::Duration::from_secs(30);
243
244pub fn relay_request_timeout(clearnet: std::time::Duration) -> std::time::Duration {
250 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
251 {
252 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
253 return clearnet.max(TOR_RELAY_REQUEST_FLOOR);
254 }
255 }
256 clearnet
257}
258
259pub fn apply_tor_proxy(
266 builder: nostr_sdk::prelude::ClientBuilder,
267) -> nostr_sdk::prelude::ClientBuilder {
268 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
269 let builder = builder.proxy(nostr_sdk::prelude::Proxy::custom(|_url| tor_proxy_target()));
270 builder
271}
272
273#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
279fn tor_proxy_target() -> Option<std::net::SocketAddr> {
280 match tor::transport_state() {
281 tor::TorTransportState::Active(addr) => Some(addr),
282 tor::TorTransportState::RequiredButInactive => Some(tor::blackhole_proxy_addr()),
285 tor::TorTransportState::Disabled => None,
286 }
287}
288
289pub async fn sign_builder(
294 builder: nostr_sdk::prelude::EventBuilder,
295) -> std::result::Result<nostr_sdk::prelude::Event, String> {
296 let signer = signer::active_signer()?;
297 builder
298 .finalize_async(&signer)
299 .await
300 .map_err(|e| e.to_string())
301}
302
303pub async fn sign_and_send(
307 client: &nostr_sdk::prelude::Client,
308 builder: nostr_sdk::prelude::EventBuilder,
309) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String> {
310 let event = sign_builder(builder).await?;
311 client
312 .send_event(&event)
313 .await
314 .map_err(|e| e.to_string())
315}
316
317pub async fn send_gift_wrap<'u, I, U, T>(
323 client: &nostr_sdk::prelude::Client,
324 relays: I,
325 receiver: &nostr_sdk::prelude::PublicKey,
326 rumor: nostr_sdk::prelude::UnsignedEvent,
327 extra_tags: T,
328) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String>
329where
330 I: IntoIterator<Item = U>,
331 U: Into<nostr_sdk::prelude::RelayUrlArg<'u>>,
332 T: IntoIterator<Item = nostr_sdk::prelude::Tag>,
333{
334 let signer = signer::active_signer()?;
335 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(*receiver, rumor)
336 .extra_tags(extra_tags)
337 .finalize_async(&signer)
338 .await
339 .map_err(|e| e.to_string())?;
340 let targets: Vec<nostr_sdk::prelude::RelayUrlArg<'u>> =
341 relays.into_iter().map(Into::into).collect();
342 if targets.is_empty() {
343 client.send_event(&wrap).await.map_err(|e| e.to_string())
344 } else {
345 client
346 .send_event(&wrap)
347 .to(targets)
348 .await
349 .map_err(|e| e.to_string())
350 }
351}
352
353pub fn community_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
366 nostr_sdk::prelude::RelayCapabilities::GOSSIP
367}
368
369pub fn discovery_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
375 community_relay_capabilities()
376}
377
378pub mod stored_event;
380
381pub mod rumor;
383
384pub mod sending;
386
387pub mod pinned_chats;
389pub mod rail_layout;
390pub mod synced_prefs;
391pub mod notify;
392pub mod wallpaper;
393
394pub mod deletion;
396pub mod self_destruct;
397
398pub mod simd;
400
401pub mod calls;
403
404pub mod community;
406
407pub mod event_handler;
409
410pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
412pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
413pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
414pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
415pub use state::{
416 ChatState, MY_SECRET_KEY, STATE, ENCRYPTION_KEY,
417 nostr_client, my_public_key, has_active_session,
418 set_nostr_client, set_my_public_key,
419 take_nostr_client, clear_my_public_key,
420 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
421 set_pending_nip55_setup, pending_nip55_setup, clear_pending_nip55_setup,
422};
423pub use crypto::{GuardedKey, GuardedSigner};
424pub use signer::{
425 SignerKind, signer_kind, set_signer_kind, is_bunker, is_keyless,
426 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
427 build_bunker_signer, prewarm_bunker, drain_bunker_state,
428 parse_bunker_remote_pubkey, parse_bunker_relays,
429 BunkerConnectionState, bunker_state, set_bunker_state,
430 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
431 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
432 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
433};
434pub use nip55::{
435 Nip55Backend, Nip55Error, Nip55ResolverOutcome, Nip55Signer, Nip55State,
436 set_nip55_backend, nip55_backend, nip55_state, set_nip55_state, drain_nip55_state,
437 nip55_is_installed, nip55_pair, nip55_perms_json,
438 VECTOR_NIP55_SIGN_KINDS, VECTOR_NIP55_ENCRYPT_TYPES,
439};
440pub use nip07::{Nip07Backend, Nip07Signer, set_nip07_backend, nip07_backend, nip07_get_public_key};
441pub use error::{DownloadError, VectorError, Result};
442pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
443pub use db::{set_app_data_dir, get_app_data_dir};
444pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
445pub use deletion::{delete_own_dm, DeleteOutcome};
446pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
447pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
448pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
449pub use event_handler::{InboundEventHandler, LiveOnly, NoOpEventHandler, PreparedEvent, process_event};
450
451use std::path::PathBuf;
452use std::sync::Arc;
453
454pub struct CoreConfig {
460 pub data_dir: PathBuf,
462 pub event_emitter: Option<Box<dyn EventEmitter>>,
464}
465
466#[derive(Clone, Copy)]
488pub struct VectorCore;
489
490enum LiveRoute {
492 Dm,
493 CommunityV1,
494 CommunityV2,
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum CatchUp {
500 Inline,
505 External,
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
517pub struct BackfillCount {
518 pub fetched: usize,
519 pub new_messages: usize,
520}
521
522impl VectorCore {
523 pub fn init(config: CoreConfig) -> Result<Self> {
525 db::set_app_data_dir(config.data_dir);
527
528 if let Some(emitter) = config.event_emitter {
530 traits::set_event_emitter(emitter);
531 }
532
533 #[cfg(not(target_arch = "wasm32"))]
535 let _ = rustls::crypto::ring::default_provider().install_default();
536
537 net::raise_fd_limit();
538
539 Ok(VectorCore)
540 }
541
542 pub fn accounts(&self) -> Result<Vec<String>> {
544 db::get_accounts().map_err(VectorError::from)
545 }
546
547 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
549 use nostr_sdk::prelude::*;
550
551 let keys = if key.starts_with("nsec1") {
553 let secret = SecretKey::from_bech32(key)
554 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
555 Keys::new(secret)
556 } else {
557 Keys::from_mnemonic(key, None)
559 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
560 };
561
562 let public_key = keys.public_key();
563 let npub = public_key.to_bech32()
564 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
565
566 let secret_bytes = keys.secret_key().to_secret_bytes();
568 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
569 state::set_my_public_key(public_key);
570
571 db::set_current_account(npub.clone())?;
573 db::init_database(&npub)?;
574
575 {
577 let nsec = keys.secret_key().to_bech32()
578 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
579 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
580
581 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
588 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
589 db::set_pkey(&nsec)?;
590 }
591 }
592
593 let has_encryption = state::resolve_encryption_enabled_from_db();
596
597 if has_encryption {
598 if let Some(pwd) = password {
599 let key = crate::crypto::hash_pass(pwd).await;
600 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
601 }
602 }
603 state::init_encryption_enabled();
606
607 let client = crate::nostr_client_builder()
610 .monitor(Monitor::new(1024))
612 .build();
613
614 for relay in state::TRUSTED_RELAYS {
616 client.add_managed_relay(*relay).await.ok();
617 }
618
619 client.connect().await;
621
622 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
623
624 Ok(LoginResult { npub, has_encryption })
625 }
626
627 pub fn generate_nsec(&self) -> Result<String> {
630 use nostr_sdk::prelude::*;
631 Keys::generate().secret_key().to_bech32()
632 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
633 }
634
635 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
640 let config = SendConfig { self_send: false, ..SendConfig::headless() };
641 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
642 .map_err(VectorError::Other)
643 }
644
645 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
647 let config = SendConfig { self_send: false, ..SendConfig::headless() };
648 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
649 .map_err(VectorError::Other)
650 }
651
652 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
659 self.download_attachment_from(attachment, None).await
660 }
661
662 pub async fn download_attachment_from(
667 &self,
668 attachment: &Attachment,
669 author_npub: Option<&str>,
670 ) -> Result<Vec<u8>> {
671 self.download_attachment_within(attachment, author_npub, 256 * 1024 * 1024)
672 .await
673 .map_err(|e| VectorError::Other(e.to_string()))
674 }
675
676 pub async fn download_attachment_within(
680 &self,
681 attachment: &Attachment,
682 author_npub: Option<&str>,
683 max_bytes: usize,
684 ) -> std::result::Result<Vec<u8>, DownloadError> {
685 use futures_util::StreamExt;
686 let max_download = max_bytes;
687 if attachment.url.is_empty() {
688 return Err(DownloadError::Unusable("attachment has no URL".into()));
689 }
690 let client = crate::net::build_http_client_with_options(
692 None,
693 Some(crate::net::TRANSFER_STALL),
694 true,
695 )
696 .map_err(DownloadError::Unreachable)?;
697 let mut last_err = String::from("download failed");
698 let mut all_answered = true;
700 let mut candidates: Vec<String> = vec![attachment.url.clone()];
701 candidates.extend(attachment.fallback_urls.iter().cloned());
702 let mut hash_swap_tried = false;
703 let mut i = 0;
704 'sources: while i < candidates.len() {
705 let url = candidates[i].clone();
706 i += 1;
707 let extend_with_swap = |candidates: &mut Vec<String>, servers: &[String]| {
710 let extra = crate::blossom::hash_swap_candidates(&attachment.url, servers);
711 for c in extra {
712 if !candidates.contains(&c) {
713 candidates.push(c);
714 }
715 }
716 };
717 macro_rules! next_source {
718 () => {{
719 log_net_fail!("[Download] source failed ({}): {}", url, last_err);
720 if i == candidates.len() && !hash_swap_tried {
721 hash_swap_tried = true;
722 let servers = crate::blossom_servers::author_swap_servers(author_npub, false).await;
723 extend_with_swap(&mut candidates, &servers);
724 }
725 continue 'sources;
726 }};
727 }
728 if let Err(e) = crate::net::validate_url_not_private(&url) {
732 last_err = e.to_string();
733 next_source!();
734 }
735 let resp = match crate::net::proxied_request(&client, reqwest::Method::GET, &url).await.send().await {
736 Ok(r) => r,
737 Err(e) => {
738 last_err = format!("download: {e}");
739 all_answered = false;
740 next_source!();
741 }
742 };
743 if !resp.status().is_success() {
744 last_err = format!("download failed: HTTP {}", resp.status());
745 all_answered = false;
746 next_source!();
747 }
748 let mut encrypted: Vec<u8> = Vec::with_capacity(
751 resp.content_length().map(|l| usize::try_from(l).unwrap_or(usize::MAX).min(max_download).min(4 * 1024 * 1024)).unwrap_or(64 * 1024),
753 );
754 let mut stream = resp.bytes_stream();
755 while let Some(chunk) = stream.next().await {
756 let chunk = match chunk {
757 Ok(c) => c,
758 Err(e) => {
759 last_err = format!("read body: {e}");
760 all_answered = false;
761 next_source!();
762 }
763 };
764 if encrypted.len() + chunk.len() > max_download {
765 return Err(DownloadError::TooLarge(max_download));
766 }
767 encrypted.extend_from_slice(&chunk);
768 }
769 if attachment.key.is_empty() {
773 let claim = attachment.original_hash.as_deref().unwrap_or(&attachment.id);
774 if crate::crypto::sha256_hex(&encrypted) == claim {
775 return Ok(encrypted);
776 }
777 last_err = "plaintext blob fails its hash claim".to_string();
778 next_source!();
779 }
780 match crate::crypto::decrypt_data_owned(encrypted, &attachment.key, &attachment.nonce) {
781 Ok(plain) => {
782 if i > 1 {
783 log_net_info!("[Download] fallback source {}/{} served {}", i, candidates.len(), url);
784 }
785 return Ok(plain);
786 }
787 Err(e) => {
788 last_err = format!("decrypt: {e}");
791 next_source!();
792 }
793 }
794 }
795 log_net_fail!("[Download] all {} source(s) failed for {}: {}", candidates.len(), attachment.url, last_err);
796 Err(if all_answered { DownloadError::Unusable(last_err) } else { DownloadError::Unreachable(last_err) })
797 }
798
799 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
801 let path = std::path::Path::new(file_path);
802 let bytes = std::fs::read(path)
803 .map_err(VectorError::Io)?;
804 let filename = path.file_name()
805 .and_then(|n| n.to_str())
806 .unwrap_or("file");
807 let extension = path.extension()
808 .and_then(|e| e.to_str())
809 .unwrap_or("bin");
810
811 sending::send_file_dm(
812 to_npub,
813 std::sync::Arc::new(bytes),
814 filename,
815 extension,
816 None,
817 None,
818 &SendConfig::default(),
819 Arc::new(NoOpSendCallback),
820 ).await.map_err(VectorError::Other)
821 }
822
823 async fn own_reaction_id(message_id: &str, emoji: &str) -> Option<String> {
828 use nostr_sdk::prelude::ToBech32;
829 let me = state::my_public_key()?.to_bech32().ok()?;
830 let st = state::STATE.lock().await;
831 let (_, message) = st.find_message(message_id)?;
832 message
833 .reactions
834 .iter()
835 .find(|r| r.author_id == me && r.emoji == emoji)
836 .map(|r| r.id.clone())
837 }
838
839 pub async fn send_reaction(
844 &self,
845 to_npub: &str,
846 reference_id: &str,
847 emoji: &str,
848 emoji_url: Option<&str>,
849 ) -> Result<String> {
850 use nostr_sdk::prelude::*;
851
852 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
853 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
854
855 if let Some(existing) = Self::own_reaction_id(reference_id, emoji).await {
857 return Ok(existing);
858 }
859
860 badges::check_new_reaction_allowance(reference_id, emoji)
862 .await
863 .map_err(VectorError::Other)?;
864
865 let reference_event = EventId::from_hex(reference_id)
866 .map_err(|e| VectorError::Nostr(e.to_string()))?;
867 let receiver_pubkey = PublicKey::from_bech32(to_npub)
868 .map_err(|e| VectorError::Nostr(e.to_string()))?;
869
870 let custom_emoji_tag = emoji_url.and_then(|url| {
872 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
873 return None;
874 }
875 let shortcode = &emoji[1..emoji.len() - 1];
876 if shortcode.is_empty() { return None; }
877 Some(Tag::custom("emoji", [shortcode.to_string(), url.to_string()]))
878 });
879
880 let reaction_target = nostr_sdk::prelude::nip25::ReactionTarget {
881 event_id: reference_event,
882 public_key: receiver_pubkey,
883 coordinate: None,
884 kind: Some(Kind::PrivateDirectMessage),
885 relay_hint: None,
886 };
887 let mut builder =
888 nostr_sdk::prelude::nip25::ReactionBuilder::new(reaction_target, emoji)
889 .into_event_builder();
890 if let Some(tag) = custom_emoji_tag {
891 builder = builder.tag(tag);
892 }
893 let rumor = builder.finalize_unsigned_with_id(my_public_key);
894 let inner_rumor_id = rumor.id;
895 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
896
897 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
901 .await.map_err(VectorError::Other)?;
902 if !outcome.output.success.is_empty() {
903 if let Some(rid) = inner_rumor_id {
904 if let Err(e) = db::nip17_keys::store_wrap_key(
905 &outcome.wrap_event_id, &rid, &receiver_pubkey,
906 db::nip17_keys::WrapRole::Recipient,
907 &outcome.wrap_secret, &outcome.targeted_relays,
908 ) {
909 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
910 }
911 }
912 }
913
914 let self_wrap_client = client.clone();
917 db::spawn_bound(async move {
918 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
919 &self_wrap_client, &my_public_key, rumor, [],
920 ).await {
921 if !self_outcome.output.success.is_empty() {
922 if let Some(rid) = inner_rumor_id {
923 let _ = db::nip17_keys::store_wrap_key(
924 &self_outcome.wrap_event_id, &rid, &my_public_key,
925 db::nip17_keys::WrapRole::SelfSend,
926 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
927 );
928 }
929 }
930 }
931 });
932
933 let reaction = Reaction {
935 id: rumor_id.clone(),
936 reference_id: reference_id.to_string(),
937 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
938 emoji: emoji.to_string(),
939 emoji_url: emoji_url.map(|s| s.to_string()),
940 };
941 let msg_for_save = {
942 let mut st = state::STATE.lock().await;
943 match st.add_reaction_to_message(reference_id, reaction) {
944 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
945 _ => None,
946 }
947 };
948 if let Some((cid, mut msg)) = msg_for_save {
949 let _ = db::events::save_message(&cid, &msg).await;
950 traits::emit_message_update(&cid, reference_id, &mut msg).await;
951 }
952
953 Ok(rumor_id)
954 }
955
956 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
959 use nostr_sdk::prelude::*;
960
961 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
962 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
963 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
964
965 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
966 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
967 .tag(Tag::public_key(pubkey))
968 .tag(Tag::custom("d", vec!["vector"]))
969 .tag(Tag::expiration(expiry))
970 .finalize_unsigned_with_id(my_public_key);
971
972 let signer = signer::active_signer().map_err(VectorError::Other)?;
974 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(pubkey, rumor.clone())
975 .extra_tags([Tag::expiration(expiry)])
976 .finalize_async(&signer)
977 .await
978 .map_err(|e| VectorError::Nostr(e.to_string()))?;
979 client
980 .send_event(&wrap)
981 .to(state::active_trusted_relays().await)
982 .await
983 .map_err(|e| VectorError::Nostr(e.to_string()))?;
984 Ok(())
985 }
986
987 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
991 crate::db::scoped(async move {
992 use nostr_sdk::prelude::*;
993
994 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
995 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
996 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
997 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
998 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
999
1000 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
1002
1003 let mut builder = EventBuilder::new(
1004 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
1005 new_content,
1006 ).tag(Tag::event(reference_event));
1007 for et in &emoji_tags {
1008 builder = builder.tag(Tag::custom(
1009 "emoji",
1010 [et.shortcode.clone(), et.url.clone()],
1011 ));
1012 }
1013 let rumor = builder.finalize_unsigned_with_id(my_public_key);
1014 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
1015 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
1016
1017 let msg_for_emit = {
1019 let mut st = state::STATE.lock().await;
1020 st.update_message_in_chat(to_npub, message_id, |msg| {
1021 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
1022 msg.preview_metadata = None;
1023 })
1024 };
1025 if let Some(mut msg) = msg_for_emit {
1026 traits::emit_message_update(to_npub, message_id, &mut msg).await;
1027 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
1028 let _ = db::events::save_edit_event(
1029 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
1030 ).await;
1031 }
1032 }
1033
1034 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
1035 .await.map_err(VectorError::Other)?;
1036
1037 let self_wrap_client = client.clone();
1038 let self_wrap_session = crate::db::current_session();
1039 db::spawn_bound(async move {
1040 if !self_wrap_session.is_live() { return; }
1041 let Ok(signer) = signer::active_signer() else { return };
1042 if let Ok(wrap) = nostr_sdk::prelude::GiftWrapBuilder::new(my_public_key, rumor)
1043 .finalize_async(&signer)
1044 .await
1045 {
1046 let _ = self_wrap_client.send_event(&wrap).await;
1047 }
1048 });
1049
1050 Ok(edit_id)
1051 })
1052 .await
1053 }
1054
1055 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
1057 use nostr_sdk::prelude::*;
1058 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
1059 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
1060 }
1061
1062 pub async fn get_chats(&self) -> Vec<SerializableChat> {
1064 let state = state::STATE.lock().await;
1065 state.chats.iter()
1066 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
1067 .collect()
1068 }
1069
1070 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
1072 let state = state::STATE.lock().await;
1073 if let Some(chat) = state.get_chat(chat_id) {
1074 let msgs = chat.get_all_messages(&state.interner);
1075 let start = offset.min(msgs.len());
1076 let end = (offset + limit).min(msgs.len());
1077 msgs[start..end].to_vec()
1078 } else {
1079 Vec::new()
1080 }
1081 }
1082
1083 pub async fn get_message(&self, message_id: &str) -> Option<(String, Message)> {
1092 {
1093 let state = state::STATE.lock().await;
1094 if let Some((chat, msg)) = state.find_message(message_id) {
1095 return Some((chat.id.clone(), msg));
1096 }
1097 }
1098 db::events::get_message_by_id(message_id).await.ok().flatten()
1099 }
1100
1101 pub async fn get_messages_with_attachment(&self, content_hash: &str) -> Vec<(String, Message)> {
1110 let Ok(ids) = db::attachments::events_with_attachment_hash(content_hash) else { return Vec::new() };
1111 let mut out = Vec::with_capacity(ids.len());
1112 for id in ids {
1113 if let Some(found) = self.get_message(&id).await {
1114 out.push(found);
1115 }
1116 }
1117 out
1118 }
1119
1120 pub async fn get_messages_before(
1130 &self,
1131 chat_id: &str,
1132 before: Option<(u64, &str)>,
1133 limit: usize,
1134 ) -> Vec<Message> {
1135 let state = state::STATE.lock().await;
1136 let Some(chat) = state.get_chat(chat_id) else {
1137 return Vec::new();
1138 };
1139 let mut msgs = chat.get_all_messages(&state.interner);
1140 if let Some((at, id)) = before {
1141 msgs.retain(|m| (m.at, m.id.as_str()) < (at, id));
1142 }
1143 msgs.sort_by(|a, b| (a.at, a.id.as_str()).cmp(&(b.at, b.id.as_str())));
1144 if msgs.len() > limit {
1145 msgs.drain(..msgs.len() - limit);
1146 }
1147 msgs
1148 }
1149
1150 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
1152 let state = state::STATE.lock().await;
1153 state.get_profile(npub)
1154 .map(|p| SlimProfile::from_profile(p, &state.interner))
1155 }
1156
1157 pub async fn load_profile(&self, npub: &str) -> bool {
1159 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
1160 }
1161
1162 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1164 profile::sync::update_profile(
1165 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1166 &NoOpProfileSyncHandler,
1167 ).await
1168 }
1169
1170 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1173 profile::sync::update_bot_profile(
1174 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1175 &NoOpProfileSyncHandler,
1176 ).await
1177 }
1178
1179 pub async fn update_status(&self, status: &str) -> bool {
1181 profile::sync::update_status(status.to_string()).await
1182 }
1183
1184 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
1190 let path = std::path::Path::new(file_path);
1191 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1192 if bytes.is_empty() {
1193 return Err(VectorError::Other("Empty image file".into()));
1194 }
1195 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1196 let mime = crate::crypto::mime_from_extension(&extension);
1197 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1198 let signer = crate::signer::active_signer()
1199 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1200 let servers = crate::blossom_servers::compute_enabled_servers();
1201 if servers.is_empty() {
1202 return Err(VectorError::Other("No Blossom servers configured".into()));
1203 }
1204 crate::blossom::upload_blob_with_failover(
1207 signer,
1208 servers,
1209 std::sync::Arc::new(bytes),
1210 Some(mime),
1211 Some(std::time::Duration::from_secs(20)),
1212 )
1213 .await
1214 .map_err(VectorError::Other)
1215 }
1216
1217 pub async fn block_user(&self, npub: &str) -> bool {
1219 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1220 }
1221
1222 pub async fn unblock_user(&self, npub: &str) -> bool {
1224 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1225 }
1226
1227 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1229 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1230 }
1231
1232 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1234 profile::sync::get_blocked_users().await
1235 }
1236
1237 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1239 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1240 }
1241
1242 pub fn my_npub(&self) -> Option<String> {
1244 state::my_public_key()
1245 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1246 }
1247
1248 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1255 use crate::community::ConcordProtocol;
1256 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1257 let mut out = Vec::new();
1258 for id in ids {
1259 match crate::db::community::community_protocol(&id).ok().flatten() {
1261 Some(ConcordProtocol::V2) => {
1262 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1263 let me = state::my_public_key();
1264 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1265 out.push(serde_json::json!({
1266 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1267 "version": 2,
1268 "name": c.name,
1269 "description": c.description,
1270 "is_owner": is_owner,
1271 "dissolved": c.dissolved,
1276 "channels": c.channels.iter()
1282 .map(|ch| serde_json::json!({
1283 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1284 "name": ch.name,
1285 "private": ch.private,
1286 "readable": !(ch.private && ch.key.is_none()),
1287 "epoch": ch.epoch.0,
1288 }))
1289 .collect::<Vec<_>>(),
1290 }));
1291 }
1292 }
1293 _ => {
1294 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1295 out.push(serde_json::json!({
1296 "community_id": c.id.to_hex(),
1297 "version": 1,
1298 "name": c.name,
1299 "description": c.description,
1300 "is_owner": crate::community::service::is_proven_owner(&c),
1301 "dissolved": c.dissolved,
1302 "channels": c.channels.iter()
1303 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1304 .collect::<Vec<_>>(),
1305 }));
1306 }
1307 }
1308 }
1309 }
1310 out
1311 }
1312
1313 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1318 use crate::community::{v2::service as v2, transport::LiveTransport};
1319 let relays: Vec<String> = crate::state::active_trusted_relays()
1320 .await
1321 .iter()
1322 .map(|s| s.to_string())
1323 .collect();
1324 if relays.is_empty() {
1325 return Err(VectorError::Other("no relays available to host the Community".into()));
1326 }
1327 let session = crate::db::current_session();
1328 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1329 let community = v2::create_community(&transport, name, relays, None)
1330 .await
1331 .map_err(VectorError::Other)?;
1332 self.register_v2_chats(&community, &session).await;
1333 if let Some(client) = state::nostr_client() {
1335 crate::community::v2::realtime::refresh_subscription(&client).await;
1336 }
1337 Ok(Self::v2_summary(&community))
1338 }
1339
1340 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1346 use crate::community::ConcordProtocol;
1347 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1348 return Ok(None);
1349 };
1350 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1351 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1352 Some(ConcordProtocol::V2) => Some(cid),
1353 _ => None,
1354 })
1355 }
1356
1357 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1359 let me = state::my_public_key();
1360 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1361 serde_json::json!({
1362 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1363 "version": 2,
1364 "name": community.name,
1365 "description": community.description,
1366 "is_owner": is_owner,
1367 "primary_channel": community.primary_channel().map(|c| crate::simd::hex::bytes_to_hex_32(&c.id.0)),
1372 "channels": community.channels.iter()
1373 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1374 .collect::<Vec<_>>(),
1375 })
1376 }
1377
1378 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, __session: &std::sync::Arc<crate::db::Session>) {
1384 register_v2_chats_inner(community).await
1385 }
1386}
1387
1388pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2) {
1391 crate::db::scoped(async move {
1392 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1393 let me = state::my_public_key();
1394 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1395 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1396 let Some(primary) = community.primary_channel() else { return };
1399 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1400 let slims = {
1405 let mut st = state::STATE.lock().await;
1406 let mut slims = Vec::new();
1407 for ch in &community.channels {
1408 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1409 st.upsert_community_chat(
1410 &ch_hex,
1411 &community.name,
1412 community.description.as_deref().unwrap_or(""),
1413 &id_hex,
1414 is_owner,
1415 community.icon.is_some(),
1416 owner_npub.as_deref(),
1417 Some(community.created_at_ms),
1418 community.dissolved,
1419 crate::community::ConcordProtocol::V2,
1420 &ch.name,
1421 &primary_hex,
1422 );
1423 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1424 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1425 }
1426 }
1427 slims
1428 };
1429 for slim in &slims {
1431 let _ = crate::db::chats::save_slim_chat(slim);
1432 }
1433 })
1434 .await
1435}
1436
1437const RAID_REPORT_TTL_SECS: u64 = 90;
1441
1442struct RaidReportCache;
1444
1445struct PolicyReportCache;
1446
1447type ReportCache<T> = std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, (u64, std::sync::Arc<T>)>>>;
1448
1449fn policy_report_cache() -> ReportCache<serde_json::Value> {
1453 crate::db::current_session().scoped::<PolicyReportCache, _>()
1454}
1455
1456fn raid_report_cache() -> ReportCache<crate::community::raid::RaidReport> {
1457 crate::db::current_session().scoped::<RaidReportCache, _>()
1458}
1459
1460impl VectorCore {
1461 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1465 use crate::community::{public_invite, service, transport::LiveTransport};
1466 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1470 let session = crate::db::current_session();
1471 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1472 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1473 .await
1474 .map_err(VectorError::Other)?;
1475 self.register_v2_chats(&community, &session).await;
1476 if let Some(client) = state::nostr_client() {
1477 crate::community::v2::realtime::refresh_subscription(&client).await;
1478 }
1479 if crate::community::v2::realtime::follow_worker_running() {
1484 crate::community::v2::realtime::enqueue_follow(community.id());
1485 } else {
1486 let seed_community = community.clone();
1487 db::spawn_bound(async move {
1488 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1489 if matches!(
1490 crate::community::v2::service::sync_guestbook(&transport, &seed_community).await,
1491 Ok(fresh) if !fresh.is_empty()
1492 ) {
1493 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1494 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1495 }
1496 });
1497 }
1498 return Ok(Self::v2_summary(&community));
1499 }
1500 let (relays, token) = public_invite::parse_invite_url(invite_url)
1501 .map_err(|e| VectorError::Other(e.to_string()))?;
1502 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1503 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1504 .await
1505 .map_err(VectorError::Other)?;
1506 let now = web_time::SystemTime::now()
1507 .duration_since(web_time::UNIX_EPOCH)
1508 .map(|d| d.as_secs())
1509 .unwrap_or(0);
1510 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1513 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1514 .await
1515 .map_err(VectorError::Other)?;
1516 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1517 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1520 self.finalize_member_join(community, &transport, attribution).await
1521 }
1522
1523 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1526 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1527 Ok(rows.iter().map(|p| {
1528 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1531 serde_json::json!({
1532 "community_id": p.community_id,
1533 "name": v2.name,
1534 "inviter_npub": p.inviter_npub,
1535 "version": 2,
1536 })
1537 } else {
1538 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1539 .ok().map(|i| i.name).unwrap_or_default();
1540 serde_json::json!({
1541 "community_id": p.community_id,
1542 "name": name,
1543 "inviter_npub": p.inviter_npub,
1544 "version": 1,
1545 })
1546 }
1547 }).collect())
1548 }
1549
1550 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1554 crate::db::scoped(async move {
1555 use crate::community::transport::LiveTransport;
1556 let bundle_json = crate::db::community::get_pending_invite(community_id)
1557 .map_err(VectorError::Other)?
1558 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1559 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1560
1561 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1563 let session = crate::db::current_session();
1564 let inviter = crate::db::community::list_pending_invites()
1566 .ok()
1567 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1568 let community = match crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref()).await {
1580 Ok(c) => c,
1581 Err(e) if e == crate::community::v2::service::ERR_DISSOLVED => {
1582 let relays = crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json)
1583 .map(|i| i.relays)
1584 .unwrap_or_default();
1585 crate::community::v2::service::retire_dead_invite(&transport, community_id, &relays).await;
1586 return Err(VectorError::Other("this community has been dissolved — the invite was removed".into()));
1587 }
1588 Err(e) => return Err(VectorError::Other(e)),
1589 };
1590 self.register_v2_chats(&community, &session).await;
1591 if let Some(client) = state::nostr_client() {
1592 crate::community::v2::realtime::refresh_subscription(&client).await;
1593 }
1594 crate::community::v2::realtime::enqueue_follow(community.id());
1595 let _ = crate::db::community::delete_pending_invite(community_id);
1596 return Ok(Self::v2_summary(&community));
1597 }
1598
1599 use crate::community::invite::{accept_invite, CommunityInvite};
1601 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1602 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1603 let now = web_time::SystemTime::now()
1607 .duration_since(web_time::UNIX_EPOCH)
1608 .map(|d| d.as_secs())
1609 .unwrap_or(0);
1610 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1611 .await
1612 .map_err(VectorError::Other)?;
1613 let summary = self.finalize_member_join(community, &transport, None).await?;
1615 let _ = crate::db::community::delete_pending_invite(community_id);
1616 Ok(summary)
1617 })
1618 .await
1619 }
1620
1621 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1626 &self,
1627 community: crate::community::Community,
1628 transport: &T,
1629 attribution: Option<(String, Option<String>)>,
1630 ) -> Result<serde_json::Value> {
1631 use crate::community::service;
1632 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1639 return Ok(serde_json::json!({
1640 "community_id": v2,
1641 "version": 2,
1642 "migrated": true,
1643 }));
1644 }
1645 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1649 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1652 if c.removed {
1653 let _ = crate::db::community::delete_community(&community.id.to_hex());
1654 return Err(VectorError::Other("you have been removed from this community".into()));
1655 }
1656 }
1657 let community = crate::db::community::load_community(&community.id)
1658 .map_err(VectorError::Other)?
1659 .unwrap_or(community);
1660 let _ = service::fetch_and_apply_control(transport, &community).await;
1664 if service::am_i_banned(&community) {
1665 let _ = crate::db::community::delete_community(&community.id.to_hex());
1666 return Err(VectorError::Other("you are banned from this community".into()));
1667 }
1668 let community = crate::db::community::load_community(&community.id)
1670 .map_err(VectorError::Other)?
1671 .unwrap_or(community);
1672 let owner_npub = community
1673 .owner_attestation
1674 .as_ref()
1675 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1676 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1677 {
1678 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1679 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1680 let mut st = state::STATE.lock().await;
1681 for ch in &community.channels {
1682 st.upsert_community_chat(
1683 &ch.id.to_hex(),
1684 &community.name,
1685 community.description.as_deref().unwrap_or(""),
1686 &community.id.to_hex(),
1687 crate::community::service::is_proven_owner(&community),
1688 community.icon.is_some(),
1689 owner_npub.as_deref(),
1690 created_at_ms,
1691 community.dissolved,
1692 crate::community::ConcordProtocol::V1,
1693 &ch.name,
1694 &primary_hex,
1695 );
1696 }
1697 }
1698 if let Some(primary) = community.channels.first() {
1701 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1702 }
1703 Ok(serde_json::json!({
1704 "community_id": community.id.to_hex(),
1705 "version": 1,
1706 "name": community.name,
1707 "channels": community.channels.iter()
1708 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1709 .collect::<Vec<_>>(),
1710 }))
1711 }
1712
1713
1714 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1718 use crate::community::CommunityId;
1719 if community_id.len() != 64 {
1720 return Err(VectorError::Other("malformed community id".into()));
1721 }
1722 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1723 match crate::db::community::community_protocol(&cid).ok().flatten() {
1724 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1725 .map_err(VectorError::Other)?
1726 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1727 Some(_) => Err(VectorError::Other(
1728 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1729 )),
1730 None => Err(VectorError::Other("community not found".into())),
1731 }
1732 }
1733
1734 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1735 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1736 .map(crate::community::ChannelId)
1737 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1738 }
1739
1740 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1745 use crate::community::{v2::service, transport::LiveTransport};
1746 let community = Self::v2_community(community_id)?;
1747 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1748 let id = if private {
1749 service::create_private_channel(&transport, &community, name).await
1750 } else {
1751 service::create_public_channel(&transport, &community, name).await
1752 }
1753 .map_err(VectorError::Other)?;
1754 if let Some(client) = state::nostr_client() {
1757 crate::community::v2::realtime::refresh_subscription(&client).await;
1758 }
1759 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1760 }
1761
1762 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &str) -> Result<()> {
1765 use crate::community::{v2::service, transport::LiveTransport};
1766 let community = Self::v2_community(community_id)?;
1767 let id = Self::channel_id_of(channel_id)?;
1768 let mut meta = community
1769 .channel(&id)
1770 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1771 .metadata();
1772 meta.name = name.to_string();
1773 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1774 service::edit_channel_metadata(&transport, &community, &id, &meta)
1775 .await
1776 .map_err(VectorError::Other)
1777 }
1778
1779 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1782 use crate::community::{v2::service, transport::LiveTransport};
1783 let community = Self::v2_community(community_id)?;
1784 let id = Self::channel_id_of(channel_id)?;
1785 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1786 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1787 service::delete_channel(&transport, &community, &id, &name)
1788 .await
1789 .map_err(VectorError::Other)
1790 }
1791
1792 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1795 use crate::community::{v2::service, transport::LiveTransport};
1796 let community = Self::v2_community(community_id)?;
1797 let id = Self::channel_id_of(channel_id)?;
1798 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1799 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1800 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1801 service::grant_channel_access(&transport, &community, &id, &member)
1802 .await
1803 .map_err(VectorError::Other)
1804 }
1805
1806 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1810 use crate::community::{v2::service, transport::LiveTransport};
1811 let community = Self::v2_community(community_id)?;
1812 let id = Self::channel_id_of(channel_id)?;
1813 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1814 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1815 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1816 service::revoke_channel_access(&transport, &community, &id, &member)
1817 .await
1818 .map_err(VectorError::Other)
1819 }
1820
1821 pub fn member_can_read_channel(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<bool> {
1844 use nostr_sdk::prelude::PublicKey;
1845 let community = Self::v2_community(community_id)?;
1846 let id = Self::channel_id_of(channel_id)?;
1847 let ch = community.channel(&id).ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1848 let who = PublicKey::parse(npub).map_err(|_| VectorError::Other(format!("invalid npub: {npub}")))?.to_hex();
1849 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1850 if crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default().contains(&who) {
1851 return Ok(false);
1852 }
1853 if !ch.private {
1854 return Ok(true);
1855 }
1856 if community.owner().map(|o| o.to_hex()) == Ok(who.clone()) {
1857 return Ok(true);
1858 }
1859 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1860 let access_ids = roster.channel_role_ids(&crate::simd::hex::bytes_to_hex_32(&id.0));
1861 Ok(roster
1862 .grants
1863 .iter()
1864 .filter(|g| g.member == who)
1865 .any(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid))))
1866 }
1867
1868 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1869 use nostr_sdk::prelude::{PublicKey, ToBech32};
1870 let community = Self::v2_community(community_id)?;
1871 let id = Self::channel_id_of(channel_id)?;
1872 let ch = community
1873 .channel(&id)
1874 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1875 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1878 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1879 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1880 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1881 let access_ids = roster.channel_role_ids(&chan_hex);
1882 let roles: Vec<serde_json::Value> = roster
1883 .channel_roles(&chan_hex)
1884 .into_iter()
1885 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1886 .collect();
1887 let members: Vec<String> = roster
1888 .grants
1889 .iter()
1890 .filter(|g| !banned.contains(&g.member))
1891 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1892 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1893 .collect();
1894 Ok(serde_json::json!({
1895 "channel_id": chan_hex,
1896 "private": ch.private,
1897 "readable": !(ch.private && ch.key.is_none()),
1898 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1899 "roles": roles,
1900 "members": members,
1901 }))
1902 }
1903
1904 pub async fn create_public_invite(
1909 &self,
1910 community_id: &str,
1911 expires_at_ms: Option<u64>,
1912 label: Option<String>,
1913 ) -> Result<String> {
1914 use crate::community::{service, transport::LiveTransport, CommunityId};
1915 if community_id.len() != 64 {
1916 return Err(VectorError::Other("malformed community id".into()));
1917 }
1918 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1919 if let Ok(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid) {
1921 let community = crate::db::community::load_community_v2(&cid)
1922 .map_err(VectorError::Other)?
1923 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1924 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1925 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1928 let minted =
1929 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1930 .await
1931 .map_err(VectorError::Other)?;
1932 return Ok(minted.url);
1933 }
1934 let community = crate::db::community::load_community(&CommunityId(
1935 crate::simd::hex::hex_to_bytes_32(community_id),
1936 ))
1937 .map_err(VectorError::Other)?
1938 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1939 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1940 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1941 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1942 .await
1943 .map_err(VectorError::Other)?;
1944 Ok(url)
1945 }
1946
1947 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1951 crate::db::scoped(async move {
1952 use crate::community::{service, CommunityId};
1953 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1954
1955 let my_pk = crate::state::my_public_key()
1956 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1957
1958 if community_id.len() != 64 {
1959 return Err(VectorError::Other("malformed community id".into()));
1960 }
1961 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1962 if let Ok(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid) {
1968 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1969 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1970 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1971 let bundle = {
1979 let lock = crate::community::v2::realtime::follow_lock(&cid);
1980 let _rotation = lock.lock().await;
1981 let community = crate::db::community::load_community_v2(&cid)
1982 .map_err(VectorError::Other)?
1983 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1984 crate::community::v2::service::bundle_of(
1985 &community,
1986 crate::community::v2::service::BundleAudience::Member(recipient),
1987 Some(my_pk),
1988 None,
1989 None,
1990 )
1991 };
1992 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1993 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1996 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1997 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1998 let rumor = nostr_sdk::prelude::EventBuilder::new(
1999 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
2000 bundle_json,
2001 )
2002 .tag(expiry_tag.clone())
2003 .finalize_unsigned_with_id(my_pk);
2004 let k_tag = nostr_sdk::prelude::Tag::custom(
2005 "k",
2006 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
2007 );
2008 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
2009 .await
2010 .map_err(VectorError::Other)?;
2011 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
2012 }
2013 let community = crate::db::community::load_community(&CommunityId(
2014 crate::simd::hex::hex_to_bytes_32(community_id),
2015 ))
2016 .map_err(VectorError::Other)?
2017 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2018
2019 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
2020 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
2021 }
2022 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
2023 .map_err(|_| VectorError::Other("invalid npub".into()))?
2024 .to_hex();
2025 if crate::db::community::get_community_banlist(community_id)
2026 .map_err(VectorError::Other)?
2027 .iter()
2028 .any(|b| b == &invitee_hex)
2029 {
2030 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
2031 }
2032
2033
2034 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
2035 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
2036 .map_err(VectorError::Other)?;
2037 let pending_id = format!("community-invite-{}", community_id);
2038 let config = SendConfig { self_send: false, ..SendConfig::gui() };
2040 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
2041
2042 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
2043 .await
2044 .map_err(VectorError::Other)?;
2045
2046 Ok(serde_json::json!({
2047 "community_id": community_id,
2048 "invitee": invitee_npub,
2049 "wrap_event_id": result.event_id,
2050 }))
2051 })
2052 .await
2053 }
2054
2055 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
2060 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
2061 }
2062
2063 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
2068 use crate::community::{service, transport::LiveTransport, CommunityId};
2069 if community_id.len() != 64 {
2070 return Err(VectorError::Other("malformed community id".into()));
2071 }
2072 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
2073 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2074 if let Ok(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid) {
2077 let community = crate::db::community::load_community_v2(&cid)
2078 .map_err(VectorError::Other)?
2079 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2080 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
2081 .await
2082 .map_err(VectorError::Other);
2083 }
2084 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
2085 let community = crate::db::community::load_community(&cid)
2086 .map_err(VectorError::Other)?
2087 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2088 service::revoke_public_invite(&transport, &community, &token_bytes)
2089 .await
2090 .map_err(VectorError::Other)
2091 }
2092
2093 pub async fn send_community_message(
2095 &self,
2096 channel_id: &str,
2097 content: &str,
2098 replied_to: Option<&str>,
2099 ) -> Result<String> {
2100 self.send_community_message_expiring(channel_id, content, replied_to, None).await
2101 }
2102
2103 pub async fn send_community_message_expiring(
2115 &self,
2116 channel_id: &str,
2117 content: &str,
2118 replied_to: Option<&str>,
2119 expires_in_secs: Option<u64>,
2120 ) -> Result<String> {
2121 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2122 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2124 let community = crate::db::community::load_community_v2(&id)
2125 .map_err(VectorError::Other)?
2126 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2127 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2128 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2129 let reply = match replied_to.filter(|r| !r.is_empty()) {
2132 Some(parent_id) => {
2133 let author_hex = {
2134 let st = state::STATE.lock().await;
2135 st.find_message(parent_id)
2136 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2137 .map(|pk| pk.to_hex())
2138 .unwrap_or_default()
2139 };
2140 Some((parent_id.to_string(), author_hex))
2141 }
2142 None => None,
2143 };
2144 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
2145 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
2148 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
2149 let mut extra_tags = Vec::new();
2150 if let Some(secs) = expires_in_secs.filter(|s| *s > 0) {
2151 let at = web_time::SystemTime::now()
2152 .duration_since(web_time::UNIX_EPOCH)
2153 .map(|d| d.as_secs())
2154 .unwrap_or(0)
2155 .saturating_add(secs);
2156 extra_tags.push(nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(at)));
2157 }
2158 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, extra_tags)
2159 .await
2160 .map_err(VectorError::Other);
2161 }
2162 if expires_in_secs.is_some_and(|s| s > 0) {
2163 return Err(VectorError::Other(
2164 "this Community predates self-destructing messages; the message was not sent".into(),
2165 ));
2166 }
2167 let (community, channel) = self.resolve_channel(channel_id)?;
2168 Self::ensure_v1_writable(&community)?;
2169 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2170 let reply = replied_to.filter(|r| !r.is_empty());
2171 let ms = web_time::SystemTime::now()
2172 .duration_since(web_time::UNIX_EPOCH)
2173 .map(|d| d.as_millis() as u64)
2174 .unwrap_or(0);
2175 let unsigned = envelope::build_inner_typed(
2176 author_pk,
2177 &channel.id,
2178 channel.epoch,
2179 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
2180 content,
2181 ms,
2182 reply,
2183 &[],
2184 );
2185 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2186 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2187 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2188 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2189 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2190 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2191 .await
2192 .map_err(VectorError::Other)?;
2193 let echoed = {
2195 let mut st = state::STATE.lock().await;
2196 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2197 };
2198 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
2199 let _ = crate::db::events::save_message(channel_id, &msg).await;
2200 }
2201 Ok(message_id)
2202 }
2203
2204 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
2208 crate::db::scoped(async move {
2209 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
2210 let path = std::path::Path::new(file_path);
2211 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
2212 if bytes.is_empty() {
2213 return Err(VectorError::Other("Empty file".into()));
2214 }
2215 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
2216 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
2217
2218 let v2_target = match self.v2_community_for_channel(channel_id)? {
2223 Some(id) => Some(
2224 crate::db::community::load_community_v2(&id)
2225 .map_err(VectorError::Other)?
2226 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
2227 ),
2228 None => None,
2229 };
2230 let v1_target = match v2_target {
2231 Some(_) => None,
2232 None => Some(self.resolve_channel(channel_id)?),
2233 };
2234 match (&v2_target, &v1_target) {
2239 (Some(c), _) => {
2240 let cid = crate::simd::hex::bytes_to_hex_32(&c.id().0);
2241 if crate::db::community::get_community_dissolved(&cid).unwrap_or(false) {
2242 return Err(VectorError::Other("this community has been dissolved".into()));
2243 }
2244 }
2245 (None, Some((c, _))) => Self::ensure_v1_writable(c)?,
2246 _ => {}
2247 }
2248 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2249
2250 let file_hash = crate::crypto::sha256_hex(&bytes);
2251 let mime = crate::crypto::mime_from_extension(&extension);
2252 let img_meta = crate::crypto::generate_image_metadata(&bytes);
2253
2254 let download_dir = crate::db::get_download_dir();
2256 let _ = std::fs::create_dir_all(&download_dir);
2257 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
2258 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
2259 let _ = std::fs::write(&local_path, &bytes);
2260
2261 let params = crate::crypto::generate_encryption_params();
2263 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
2264 let encrypted_size = encrypted.len() as u64;
2265
2266 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2267 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2268 let servers = crate::blossom_servers::compute_enabled_servers();
2269 if servers.is_empty() {
2270 return Err(VectorError::Other("No Blossom servers configured".into()));
2271 }
2272 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
2273 let accepted = crate::blossom::upload_blob_with_progress_and_failover(
2274 signer.clone(),
2275 servers,
2276 std::sync::Arc::new(encrypted),
2277 Some(mime),
2278 true,
2279 noop_progress,
2280 Some(3),
2281 Some(std::time::Duration::from_secs(2)),
2282 None,
2283 ).await.map_err(VectorError::Other)?;
2284 let url = accepted.url;
2286
2287 let attachment = crate::types::Attachment {
2288 id: file_hash.clone(),
2289 key: params.key.clone(),
2290 nonce: params.nonce.clone(),
2291 extension: extension.clone(),
2292 name: filename.clone(),
2293 url,
2294 path: local_path.to_string_lossy().to_string(),
2295 size: encrypted_size,
2296 img_meta,
2297 downloading: false,
2298 downloaded: true,
2299 webxdc_topic: extension
2301 .eq_ignore_ascii_case("xdc")
2302 .then(|| crate::webxdc::mint_topic_id(&file_hash, &author_pk.to_hex())),
2303 ..Default::default()
2304 };
2305 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
2306
2307 if let Some(community) = v2_target {
2309 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2310 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2311 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2312 .await
2313 .map_err(VectorError::Other);
2314 }
2315 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2316 let ms = web_time::SystemTime::now()
2317 .duration_since(web_time::UNIX_EPOCH)
2318 .map(|d| d.as_millis() as u64)
2319 .unwrap_or(0);
2320 let unsigned = envelope::build_inner_full(
2321 author_pk, &channel.id, channel.epoch,
2322 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2323 );
2324 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2325 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2326 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2327 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2328 .await.map_err(VectorError::Other)?;
2329 let echoed = {
2331 let mut st = state::STATE.lock().await;
2332 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2333 };
2334 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2335 let _ = crate::db::events::save_message(channel_id, &m).await;
2336 }
2337 Ok(message_id)
2338 })
2339 .await
2340 }
2341
2342 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2344 use crate::community::{service, transport::LiveTransport};
2345 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2346 let community = crate::db::community::load_community_v2(&id)
2347 .map_err(VectorError::Other)?
2348 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2349 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2350 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2351 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2352 .await
2353 .map_err(VectorError::Other);
2354 }
2355 let (community, channel) = self.resolve_channel(channel_id)?;
2356 Self::ensure_v1_writable(&community)?;
2357 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2358 service::publish_typing_signal(&transport, &community, &channel)
2359 .await
2360 .map_err(VectorError::Other)
2361 }
2362
2363 pub async fn send_community_reaction(
2366 &self,
2367 channel_id: &str,
2368 message_id: &str,
2369 emoji: &str,
2370 emoji_url: Option<&str>,
2371 ) -> Result<()> {
2372 crate::db::scoped(async move {
2373 if Self::own_reaction_id(message_id, emoji).await.is_some() {
2375 return Ok(());
2376 }
2377 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2378 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2379 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2380 }
2381 _ => Vec::new(),
2382 };
2383 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2384 let community = crate::db::community::load_community_v2(&id)
2385 .map_err(VectorError::Other)?
2386 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2387 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2388 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2389 let held = {
2394 let st = state::STATE.lock().await;
2395 st.find_message(message_id)
2396 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2397 };
2398 let held = held.or_else(|| {
2399 crate::db::events::event_author(message_id)
2400 .ok()
2401 .flatten()
2402 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2403 });
2404 let target_author = match held {
2405 Some(pk) => pk,
2406 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2407 .await
2408 .map_err(VectorError::Other)?
2409 .iter()
2410 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2411 .map(|f| f.event.opened().author)
2412 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2413 };
2414 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2416 return crate::community::v2::service::send_reaction(
2421 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2422 )
2423 .await
2424 .map(|_| ())
2425 .map_err(VectorError::Other);
2426 }
2427 self.publish_community_control(
2428 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2429 ).await
2430 })
2431 .await
2432 }
2433
2434 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2436 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2437 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2438 let community = crate::db::community::load_community_v2(&id)
2439 .map_err(VectorError::Other)?
2440 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2441 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2442 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2443 let emoji_pairs: Vec<(&str, &str)> = emoji_tags.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
2444 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content, &emoji_pairs)
2445 .await
2446 .map(|_| ())
2447 .map_err(VectorError::Other);
2448 }
2449 self.publish_community_control(
2450 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2451 ).await
2452 }
2453
2454 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2458 let channel_id = {
2459 let st = state::STATE.lock().await;
2460 match st.find_message(message_id) {
2461 Some((chat, _)) => chat.id.clone(),
2462 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2463 }
2464 };
2465 self.delete_community_message_in(&channel_id, message_id).await
2466 }
2467
2468 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2472 use crate::community::{service, transport::LiveTransport};
2473 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2474
2475 let attachment_urls: Vec<String> = {
2478 let st = state::STATE.lock().await;
2479 st.find_message(message_id)
2480 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2481 .unwrap_or_default()
2482 };
2483 let attachment_urls =
2485 crate::db::attachments::urls_unreferenced_elsewhere(&attachment_urls, message_id).unwrap_or_default();
2486
2487 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2488 let community = crate::db::community::load_community_v2(&id)
2491 .map_err(VectorError::Other)?
2492 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2493 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2494 crate::community::v2::service::send_delete(
2495 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2496 )
2497 .await
2498 .map_err(VectorError::Other)?;
2499 } else {
2500 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2502 let _ = service::delete_message(&transport, message_id).await;
2503 }
2504 self.publish_community_control(
2506 channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2507 ).await?;
2508 }
2509 if !attachment_urls.is_empty() {
2511 if let Some(_client) = state::nostr_client() {
2512 if let Ok(signer) = crate::signer::active_signer() {
2513 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2514 }
2515 }
2516 }
2517 let removed_chat = {
2518 let mut st = state::STATE.lock().await;
2519 st.remove_message(message_id).map(|(cid, _)| cid)
2520 };
2521 let _ = crate::db::events::delete_event(message_id).await;
2522 traits::emit_event_json("message_removed", serde_json::json!({
2523 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "deleted",
2524 }));
2525 Ok(())
2526 }
2527
2528 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2534 use crate::community::transport::LiveTransport;
2535 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2536
2537 let author_npub = {
2540 let st = state::STATE.lock().await;
2541 st.find_message(message_id).and_then(|(_, m)| m.npub)
2542 };
2543 let author_npub = match author_npub {
2544 Some(n) => n,
2545 None => crate::db::events::event_author(message_id)
2546 .ok()
2547 .flatten()
2548 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2549 };
2550 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2551 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2552
2553 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2554 let community = crate::db::community::load_community_v2(&id)
2555 .map_err(VectorError::Other)?
2556 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2557 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2558 crate::community::v2::service::moderation_delete(
2559 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2560 )
2561 .await
2562 .map_err(VectorError::Other)?;
2563 } else {
2564 let cid = crate::db::community::community_id_for_channel(channel_id)
2565 .map_err(VectorError::Other)?
2566 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2567 let community = crate::db::community::load_community(&crate::community::CommunityId(
2568 crate::simd::hex::hex_to_bytes_32(&cid),
2569 ))
2570 .map_err(VectorError::Other)?
2571 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2572 let channel = community
2573 .channels
2574 .iter()
2575 .find(|c| c.id.to_hex() == channel_id)
2576 .cloned()
2577 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2578 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2579 .await
2580 .map_err(VectorError::Other)?;
2581 }
2582
2583 let removed_chat = {
2584 let mut st = state::STATE.lock().await;
2585 st.remove_message(message_id).map(|(cid, _)| cid)
2586 };
2587 let _ = crate::db::events::delete_event(message_id).await;
2588 traits::emit_event_json("message_removed", serde_json::json!({
2589 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2590 }));
2591 Ok(())
2592 }
2593
2594 async fn publish_community_control(
2597 &self,
2598 channel_id: &str,
2599 kind: u16,
2600 content: &str,
2601 target: &str,
2602 emoji_tags: &[crate::types::EmojiTag],
2603 ) -> Result<()> {
2604 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2605 let (community, channel) = self.resolve_channel(channel_id)?;
2606 Self::ensure_v1_writable(&community)?;
2607 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2608 let ms = web_time::SystemTime::now()
2609 .duration_since(web_time::UNIX_EPOCH)
2610 .map(|d| d.as_millis() as u64)
2611 .unwrap_or(0);
2612 let unsigned = envelope::build_inner_typed(
2613 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2614 );
2615 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2616 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2617 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2618 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2619 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2620 .await.map_err(VectorError::Other)?;
2621 let outcome = {
2624 let mut st = state::STATE.lock().await;
2625 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2626 };
2627 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2628 if let Some(ev) = edit_event {
2629 let mut ev = (*ev).clone();
2630 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2631 let _ = crate::db::events::save_event(&ev).await;
2632 } else {
2633 let _ = crate::db::events::save_message(channel_id, &message).await;
2634 }
2635 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2636 }
2637 Ok(())
2638 }
2639
2640 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2648 self.sync_community_channel_before(channel_id, limit, None).await
2649 }
2650
2651 pub async fn sync_community_channel_page(
2654 &self,
2655 channel_id: &str,
2656 limit: usize,
2657 before_secs: Option<u64>,
2658 since_secs: Option<u64>,
2659 ) -> Result<(BackfillCount, Vec<String>)> {
2660 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2661 let warnings = if community::v2::realtime::follow_worker_running() {
2662 community::v2::realtime::enqueue_follow(&id);
2663 Vec::new()
2664 } else {
2665 Self::v2_inline_follow(&id).await
2666 };
2667 let count = Self::v2_backfill_channel_counted(
2668 &id, channel_id, limit, 8, since_secs, before_secs,
2669 crate::community::transport::Evidence::Fast, 12,
2670 ).await;
2671 return Ok((count, warnings));
2672 }
2673 let (new_messages, warnings) = self.sync_community_channel(channel_id, limit).await?;
2676 Ok((BackfillCount { fetched: new_messages, new_messages }, warnings))
2677 }
2678
2679 pub async fn sync_community_channel_before(
2688 &self,
2689 channel_id: &str,
2690 limit: usize,
2691 before_secs: Option<u64>,
2692 ) -> Result<(usize, Vec<String>)> {
2693 use crate::community::{send, service, transport::LiveTransport};
2694 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2695 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2700 let warnings = if community::v2::realtime::follow_worker_running() {
2701 community::v2::realtime::enqueue_follow(&id);
2702 Vec::new()
2703 } else {
2704 Self::v2_inline_follow(&id).await
2705 };
2706 let new = Self::v2_backfill_channel(
2711 &id, channel_id, limit, 8, None, before_secs,
2712 crate::community::transport::Evidence::Fast, 12,
2713 ).await;
2714 return Ok((new, warnings));
2715 }
2716 let (community, _) = self.resolve_channel(channel_id)?;
2717 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2718 let mut warnings: Vec<String> = Vec::new();
2719
2720 match service::catch_up_server_root(&transport, &community).await {
2728 Ok(c) if c.removed => {
2729 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2731 return Ok((0, warnings));
2732 }
2733 Ok(_) => {}
2734 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2735 }
2736 let (community, _) = self.resolve_channel(channel_id)?;
2737
2738 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2744 warnings.push(format!("control fold failed: {e}"));
2745 }
2746 if service::am_i_banned(&community) {
2747 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2749 return Ok((0, warnings));
2750 }
2751 let (community, channel) = self.resolve_channel(channel_id)?;
2754 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2755 warnings.push(format!("channel catch-up failed: {e}"));
2756 }
2757 let (community, _) = self.resolve_channel(channel_id)?;
2761 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2762 warnings.push(format!("read-cut resume failed: {e}"));
2763 }
2764 let (community, channel) = self.resolve_channel(channel_id)?;
2765
2766 let session = crate::db::current_session();
2768 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2769 .await
2770 .map_err(VectorError::Other)?;
2771 let new = Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await;
2772 Ok((new, warnings))
2773 }
2774
2775 async fn v1_ingest_channel_page(
2780 channel_id: &str,
2781 events: &[nostr_sdk::prelude::Event],
2782 channel: &crate::community::Channel,
2783 my_pk: nostr_sdk::prelude::PublicKey,
2784 session: &std::sync::Arc<crate::db::Session>,
2785 ) -> usize {
2786 crate::db::scoped(async move {
2787 use crate::community::inbound;
2788 let mut outcomes = {
2789 let mut st = state::STATE.lock().await;
2790 inbound::process_channel_batch(&mut st, events, channel, &my_pk)
2791 };
2792 let mut new = 0usize;
2793 for o in &mut outcomes {
2800 if let inbound::IncomingEvent::NewMessage(m)
2801 | inbound::IncomingEvent::Updated { message: m, .. } = o
2802 {
2803 crate::db::attachments::verify_message_attachments(m, Some(channel_id)).await;
2804 }
2805 }
2806 let mut pending: Vec<&crate::types::Message> = Vec::new();
2807 for o in &outcomes {
2808 if !session.is_live() {
2810 pending.clear();
2811 break;
2812 }
2813 match o {
2814 inbound::IncomingEvent::NewMessage(m) => {
2815 pending.push(m);
2816 new += 1;
2817 }
2818 inbound::IncomingEvent::Updated { message, .. } => {
2819 pending.push(message);
2820 }
2821 inbound::IncomingEvent::Removed { target_id } => {
2822 crate::db::events::flush_message_batch(channel_id, &mut pending, session).await;
2823 crate::state::note_message_deleted(target_id);
2830 if let Err(e) = crate::db::events::add_message_tombstone(target_id) {
2831 crate::log_warn!("[Community delete] tombstone write failed: {e}");
2832 }
2833 let _ = crate::db::events::delete_event(target_id).await;
2834 }
2835 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2836 crate::db::events::flush_message_batch(channel_id, &mut pending, session).await;
2839 let _ = crate::db::events::delete_event(reaction_id).await;
2840 }
2841 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2842 let et = if *joined {
2843 crate::stored_event::SystemEventType::MemberJoined
2844 } else {
2845 crate::stored_event::SystemEventType::MemberLeft
2846 };
2847 let note = invited_by.as_ref().map(|by| match invited_label {
2849 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2850 _ => by.clone(),
2851 });
2852 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;
2853 }
2854 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2855 community::service::persist_webxdc_signal(
2858 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2859 ).await;
2860 }
2861 inbound::IncomingEvent::Kicked { community_id }
2862 | inbound::IncomingEvent::SelfLeft { community_id } => {
2863 crate::db::events::flush_message_batch(channel_id, &mut pending, session).await;
2868 let _ = crate::db::community::delete_community_retain_keys(community_id);
2869 break;
2870 }
2871 inbound::IncomingEvent::Typing { .. } => {
2872 }
2874 }
2875 }
2876 crate::db::events::flush_message_batch(channel_id, &mut pending, session).await;
2877 new
2878 })
2879 .await
2880 }
2881
2882 pub async fn sync_channel_events(
2897 &self,
2898 channel_id: &str,
2899 max_events: usize,
2900 until_s: Option<u64>,
2901 since_s: Option<u64>,
2902 ) -> Result<usize> {
2903 use crate::community::{send, transport::LiveTransport};
2904 let max = max_events.clamp(1, 500);
2905 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2906 let community = crate::db::community::load_community_v2(&id)
2907 .map_err(VectorError::Other)?
2908 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2909 if community.dissolved {
2910 return Err(VectorError::Other("this community has been dissolved".into()));
2911 }
2912 let ch_id = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2913 let ch = community
2914 .channel(&ch_id)
2915 .ok_or_else(|| VectorError::Other("no such channel in this community".into()))?;
2916 if ch.private && ch.key.is_none() {
2919 return Err(VectorError::Other(
2920 "this private channel has no key yet (awaiting rekey delivery)".into(),
2921 ));
2922 }
2923 let new = Self::v2_backfill_channel(
2924 &id, channel_id, max, 1, since_s, until_s,
2925 crate::community::transport::Evidence::Fast, 12,
2926 )
2927 .await;
2928 return Ok(new);
2929 }
2930 let (community, channel) = self.resolve_channel(channel_id)?;
2931 Self::ensure_v1_writable(&community)?;
2932 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2933 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2934 let session = crate::db::current_session();
2935 let events = send::fetch_channel_page(&transport, &community, &channel, until_s, since_s, max)
2936 .await
2937 .map_err(VectorError::Other)?;
2938 Ok(Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await)
2939 }
2940
2941 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2953 use crate::bot_interface::{self, ChatCommandsSnapshot};
2954 use nostr_sdk::prelude::ToBech32;
2955
2956 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2957 let mut relays: Vec<String> = Vec::new();
2958 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2959 if let Some(cid_hex) = community_hex {
2960 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2961 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2962 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2963 relays = community.relays.clone();
2964 } else {
2965 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2966 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2967 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2968 };
2969 relays = community.relays.clone();
2970 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2971 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2972 members.push(pk);
2973 }
2974 }
2975 }
2976 let state = crate::state::STATE.lock().await;
2977 for pk in members {
2978 let Ok(npub) = pk.to_bech32();
2979 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2980 bots.push(pk);
2981 }
2982 }
2983 } else if chat_id.starts_with("npub1") {
2984 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2985 let is_bot = {
2986 let state = crate::state::STATE.lock().await;
2987 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2988 };
2989 if is_bot {
2990 bots.push(pk);
2991 if let Some(client) = crate::state::nostr_client() {
2994 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2995 }
2996 }
2997 }
2998 }
2999
3000 if bots.is_empty() {
3001 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
3002 }
3003 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
3006 relays.sort();
3007 relays.dedup();
3008 bots.sort_by_key(|p| p.to_hex());
3011 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
3012 let commands = bot_interface::assemble_from_store(&bot_hexes);
3013 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
3014 if !fresh {
3015 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
3016 }
3017 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
3018 }
3019
3020 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
3025 use nostr_sdk::prelude::ToBech32;
3026 match Self::load_v2_if_v2(community_id) {
3032 Ok(Some(community)) => {
3033 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3034 let (events, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
3035 if cursor == 0 {
3036 if crate::community::v2::realtime::follow_worker_running() {
3037 crate::community::v2::realtime::enqueue_follow(community.id());
3038 } else {
3039 let c2 = community.clone();
3040 db::spawn_bound(async move {
3041 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
3042 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2).await, Ok(fresh) if !fresh.is_empty()) {
3043 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
3044 }
3045 });
3046 }
3047 }
3048 return crate::community::v2::service::stored_memberlist_from(&community, &events)
3049 .unwrap_or_default()
3050 .into_iter()
3051 .filter_map(|pk| pk.to_bech32().ok())
3052 .map(|npub| serde_json::json!({ "npub": npub }))
3053 .collect();
3054 }
3055 Ok(None) => {} Err(_) => return Vec::new(),
3058 }
3059 crate::db::community::community_member_activity(community_id)
3060 .unwrap_or_default()
3061 .into_iter()
3062 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
3063 .collect()
3064 }
3065
3066 pub fn get_community_banned(&self, community_id: &str) -> Vec<String> {
3076 use nostr_sdk::prelude::{PublicKey, ToBech32};
3077 let cid_hex = match Self::load_v2_if_v2(community_id) {
3080 Ok(Some(c)) => crate::simd::hex::bytes_to_hex_32(&c.id().0),
3081 _ => community_id.to_string(),
3082 };
3083 crate::db::community::get_community_banlist(&cid_hex)
3084 .unwrap_or_default()
3085 .iter()
3086 .filter_map(|hex| PublicKey::parse(hex).ok())
3087 .filter_map(|pk| pk.to_bech32().ok())
3088 .collect()
3089 }
3090
3091 pub fn community_moderation_intel(&self, community_id: &str) -> Result<serde_json::Value> {
3098 let community = Self::load_v2_if_v2(community_id)?
3099 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3100 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3101 let report = Self::policy_console_report(&cid_hex, &community)?;
3102 let invites = crate::db::community::list_public_invites(&cid_hex).unwrap_or_default();
3103 let banlist = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
3104 let caps = self.community_capabilities(community_id).unwrap_or_else(|_| serde_json::json!({}));
3105 let owner_b32 = community.owner().map_err(VectorError::Other)?.to_bech32().unwrap_or_default();
3106
3107 Ok(serde_json::json!({
3108 "community_id": cid_hex,
3109 "name": community.name,
3110 "owner_npub": owner_b32,
3111 "epoch": community.root_epoch.0,
3112 "report": &*report,
3113 "invites": invites.iter().map(|i| serde_json::json!({
3114 "token": i.token,
3115 "label": i.label,
3116 "created_at": i.created_at,
3117 "expires_at": i.expires_at,
3118 "join_count": i.join_count,
3119 })).collect::<Vec<_>>(),
3120 "banlist_count": banlist.len(),
3121 "banlist_max": crate::community::v2::roles::MAX_BANLIST,
3122 "capabilities": caps,
3123 "channels": community.channels.iter().map(|c| serde_json::json!({
3126 "id": crate::simd::hex::bytes_to_hex_32(&c.id.0),
3127 "name": c.name,
3128 })).collect::<Vec<_>>(),
3129 }))
3130 }
3131
3132 pub fn screen_community_message(
3141 &self,
3142 channel_id: &str,
3143 author_npub: &str,
3144 text: &str,
3145 ) -> Result<serde_json::Value> {
3146 use nostr_sdk::prelude::PublicKey;
3147 let Some(id) = self.v2_community_for_channel(channel_id)? else {
3148 return Ok(serde_json::json!({ "findings": [] }));
3149 };
3150 let community = crate::db::community::load_community_v2(&id)
3151 .map_err(VectorError::Other)?
3152 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3153 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3154 let owner = community.owner().map_err(VectorError::Other)?;
3155 let author = PublicKey::parse(author_npub).map_err(|_| VectorError::Other("unreadable author".into()))?;
3156
3157 use crate::community::roles::Permissions;
3160 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3161 | Permissions::MANAGE_CHANNELS
3162 | Permissions::MANAGE_METADATA
3163 | Permissions::KICK
3164 | Permissions::BAN
3165 | Permissions::MANAGE_MESSAGES;
3166 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3167 let hex = author.to_hex();
3168 let roles: Vec<String> =
3169 roster.grants.iter().find(|g| g.member == hex).map(|g| g.role_ids.clone()).unwrap_or_default();
3170 let is_staff = roles
3171 .iter()
3172 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0));
3173
3174 let now_ms = web_time::SystemTime::now()
3175 .duration_since(web_time::UNIX_EPOCH)
3176 .map(|d| d.as_millis() as u64)
3177 .unwrap_or(0);
3178 let findings = crate::community::policy::harness::screen_message(
3179 &cid_hex, &owner, &author, &roles, is_staff, channel_id, text, now_ms,
3180 );
3181 Ok(serde_json::json!({ "findings": findings }))
3182 }
3183
3184 pub fn check_community_raid(&self, community_id: &str) -> Result<serde_json::Value> {
3188 let Some(community) = Self::load_v2_if_v2(community_id)? else {
3189 return Ok(serde_json::json!({ "detected": false }));
3190 };
3191 let caps = self.community_capabilities(community_id)?;
3195 if caps["ban"].as_bool() != Some(true) {
3196 return Ok(serde_json::json!({ "detected": false }));
3197 }
3198 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3199 let report = Self::policy_console_report(&cid_hex, &community)?;
3200 let biggest = report["cohorts"].get(0);
3201 Ok(serde_json::json!({
3202 "detected": report["raid_detected"].as_bool().unwrap_or(false),
3203 "suspects": report["suspects"].as_u64().unwrap_or(0),
3204 "cohort": biggest.and_then(|c| c["size"].as_u64()).unwrap_or(0),
3205 "sample": biggest
3206 .and_then(|c| c["sample"].as_str())
3207 .map(|s| s.chars().take(60).collect::<String>())
3208 .unwrap_or_default(),
3209 }))
3210 }
3211
3212 fn raid_report(cid_hex: &str) -> Result<std::sync::Arc<crate::community::raid::RaidReport>> {
3216 use crate::community::raid;
3217 use crate::community::v2::guestbook::GuestbookEntry;
3218 use nostr_sdk::prelude::PublicKey;
3219 use std::collections::{HashMap, HashSet};
3220
3221 let now_secs = web_time::SystemTime::now()
3222 .duration_since(web_time::UNIX_EPOCH)
3223 .map(|d| d.as_secs())
3224 .unwrap_or(0);
3225 {
3226 let cache = raid_report_cache();
3227 let map = cache.lock().unwrap_or_else(|e| e.into_inner());
3228 if let Some((at, report)) = map.get(cid_hex) {
3229 if now_secs.saturating_sub(*at) < RAID_REPORT_TTL_SECS {
3230 return Ok(report.clone());
3231 }
3232 }
3233 }
3234 let community = crate::db::community::load_community_v2(&crate::community::CommunityId(
3235 crate::simd::hex::hex_to_bytes_32(cid_hex),
3236 ))
3237 .map_err(VectorError::Other)?
3238 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3239 let owner_b32 = community.owner().map_err(VectorError::Other)?.to_bech32().unwrap_or_default();
3240 let me_b32 = state::my_public_key().and_then(|p| p.to_bech32().ok()).unwrap_or_default();
3241
3242 let roster = crate::db::community::get_community_roles(cid_hex).unwrap_or_default();
3244 let staff: HashSet<String> = roster
3245 .grants
3246 .iter()
3247 .filter(|g| !g.role_ids.is_empty())
3248 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3249 .collect();
3250
3251 let members: Vec<String> = crate::community::v2::service::stored_memberlist(&community)
3253 .unwrap_or_default()
3254 .into_iter()
3255 .filter_map(|pk| pk.to_bech32().ok())
3256 .collect();
3257
3258 let (events, _cursor) = crate::db::community::get_guestbook(cid_hex).unwrap_or_default();
3260 let mut joined_at: HashMap<String, u64> = HashMap::new();
3261 let mut invite_label: HashMap<String, String> = HashMap::new();
3262 for e in &events {
3263 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3264 let Ok(b32) = member.to_bech32();
3265 let slot = joined_at.entry(b32.clone()).or_insert(*at_ms);
3267 *slot = (*slot).min(*at_ms);
3268 if let Some((_creator, label)) = invited_by {
3269 invite_label.entry(b32).or_insert_with(|| label.clone());
3270 }
3271 }
3272
3273 let footprints: HashMap<String, crate::db::community::AuthorFootprint> =
3274 crate::db::community::community_author_footprints(cid_hex)
3275 .unwrap_or_default()
3276 .into_iter()
3277 .map(|f| (f.npub.clone(), f))
3278 .collect();
3279
3280 const TEXT_WINDOW: usize = 4_000;
3283 let mut texts: HashMap<String, Vec<String>> = HashMap::new();
3284 for (npub, _at, text) in crate::db::community::community_recent_texts(cid_hex, TEXT_WINDOW).unwrap_or_default() {
3285 texts.entry(npub).or_default().push(text);
3286 }
3287
3288 let signals: Vec<raid::MemberSignals> = members
3289 .iter()
3290 .map(|npub| {
3291 let f = footprints.get(npub);
3292 raid::MemberSignals {
3293 npub: npub.clone(),
3294 joined_at_ms: joined_at.get(npub).copied().unwrap_or(0),
3295 invite_label: invite_label.get(npub).cloned(),
3296 messages: f.map(|f| f.messages).unwrap_or(0),
3297 first_secs: f.map(|f| f.first_secs).unwrap_or(0),
3298 last_secs: f.map(|f| f.last_secs).unwrap_or(0),
3299 texts: texts.get(npub).cloned().unwrap_or_default(),
3300 is_owner: *npub == owner_b32,
3301 is_admin: staff.contains(npub),
3302 is_me: *npub == me_b32,
3303 }
3304 })
3305 .collect();
3306
3307 let params = raid::RaidParams {
3308 known_shortcodes: crate::emoji_packs::known_shortcodes(),
3309 ..Default::default()
3310 };
3311 let report = std::sync::Arc::new(raid::assess(&signals, now_secs, ¶ms));
3312 raid_report_cache()
3313 .lock()
3314 .unwrap_or_else(|e| e.into_inner())
3315 .insert(cid_hex.to_string(), (now_secs, report.clone()));
3316 Ok(report)
3317 }
3318
3319 pub fn policy_presets(&self) -> Result<serde_json::Value> {
3321 let presets: Vec<serde_json::Value> = crate::community::policy::presets::all()
3322 .into_iter()
3323 .map(|p| {
3324 serde_json::json!({
3325 "id": p.id,
3326 "name": p.name,
3327 "description": p.description,
3328 "example": p.example,
3329 "caveat": p.caveat,
3330 "dials": p.dials,
3331 "rules": crate::community::policy::presets::describe(&p.policy),
3335 "bytes": serde_json::to_string(&p.policy).unwrap_or_default(),
3336 })
3337 })
3338 .collect();
3339 let rule_kinds: Vec<serde_json::Value> = crate::community::policy::presets::rule_kinds()
3343 .into_iter()
3344 .map(|k| {
3345 serde_json::json!({
3346 "id": k.id,
3347 "label": k.label,
3348 "description": k.description,
3349 "input": k.input,
3350 "input_label": k.input_label,
3351 "input_hint": k.input_hint,
3352 "rule": k.rule,
3353 })
3354 })
3355 .collect();
3356 Ok(serde_json::json!({ "presets": presets, "rule_kinds": rule_kinds }))
3357 }
3358
3359 pub fn preview_community_policy(&self, community_id: &str, bytes: &str) -> Result<serde_json::Value> {
3363 use crate::community::v2::guestbook::GuestbookEntry;
3364 let community = Self::load_v2_if_v2(community_id)?
3365 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3366 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3367 let owner = community.owner().map_err(VectorError::Other)?;
3368 let me = state::my_public_key();
3369
3370 use crate::community::roles::Permissions;
3371 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3372 | Permissions::MANAGE_CHANNELS
3373 | Permissions::MANAGE_METADATA
3374 | Permissions::KICK
3375 | Permissions::BAN
3376 | Permissions::MANAGE_MESSAGES;
3377 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3378 let staff: std::collections::HashSet<String> = roster
3379 .grants
3380 .iter()
3381 .filter(|g| {
3382 g.role_ids
3383 .iter()
3384 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0))
3385 })
3386 .map(|g| g.member.clone())
3387 .collect();
3388 let roles_of: std::collections::HashMap<String, Vec<String>> =
3389 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3390
3391 let (events, _cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
3392 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3393 let mut invite_label: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3394 for e in &events {
3395 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3396 let hex = member.to_hex();
3397 let slot = joined_at.entry(hex.clone()).or_insert(*at_ms);
3398 *slot = (*slot).min(*at_ms);
3399 if let Some((_c, label)) = invited_by {
3400 invite_label.entry(hex).or_insert_with(|| label.clone());
3401 }
3402 }
3403 let members: Vec<crate::community::policy::harness::MemberRow> =
3404 crate::community::v2::service::stored_memberlist(&community)
3405 .unwrap_or_default()
3406 .into_iter()
3407 .map(|pk| {
3408 let hex = pk.to_hex();
3409 (
3410 pk,
3411 joined_at.get(&hex).copied(),
3412 staff.contains(&hex),
3413 roles_of.get(&hex).cloned().unwrap_or_default(),
3414 invite_label.get(&hex).cloned(),
3415 )
3416 })
3417 .collect();
3418
3419 let now_ms = web_time::SystemTime::now()
3420 .duration_since(web_time::UNIX_EPOCH)
3421 .map(|d| d.as_millis() as u64)
3422 .unwrap_or(0);
3423 let assembled =
3424 crate::community::policy::harness::assemble(&cid_hex, &owner, me.as_ref(), &members, now_ms)
3425 .map_err(VectorError::Other)?;
3426 let preview = crate::community::policy::harness::preview_policy(&assembled, bytes, now_ms);
3427 serde_json::to_value(preview).map_err(|e| VectorError::Other(e.to_string()))
3428 }
3429
3430 pub fn list_community_policies(&self, community_id: &str) -> Result<serde_json::Value> {
3433 let community = Self::load_v2_if_v2(community_id)?
3434 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3435 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3436 let stored = crate::db::community::get_community_policies(&cid_hex).map_err(VectorError::Other)?;
3437 let rows: Vec<serde_json::Value> = stored
3438 .iter()
3439 .map(|p| {
3440 let verdict = serde_json::from_str::<crate::community::policy::document::Policy>(&p.bytes)
3444 .map_err(|e| e.to_string())
3445 .and_then(|doc| doc.validate().map_err(|r| format!("{r:?}")));
3446 serde_json::json!({
3447 "policy_id": p.policy_id,
3448 "hash": p.hash,
3449 "enabled": p.enabled,
3450 "updated_at": p.updated_at,
3451 "bytes": p.bytes,
3452 "valid": verdict.is_ok(),
3453 "error": verdict.err(),
3454 })
3455 })
3456 .collect();
3457 Ok(serde_json::json!({
3458 "community_id": cid_hex,
3459 "policies": rows,
3460 "using_builtin": !stored
3465 .iter()
3466 .any(|p| p.policy_id == crate::community::policy::harness::DEFAULTS_POLICY_ID),
3467 }))
3468 }
3469
3470 pub fn set_community_policy(&self, community_id: &str, policy_id: &str, bytes: &str, enabled: bool) -> Result<serde_json::Value> {
3473 let community = Self::load_v2_if_v2(community_id)?
3474 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3475 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3476 if bytes.len() > crate::community::policy::types::caps::MAX_POLICY_BYTES {
3477 return Err(VectorError::Other("policy exceeds the maximum document size".into()));
3478 }
3479 let doc: crate::community::policy::document::Policy =
3480 serde_json::from_str(bytes).map_err(|e| VectorError::Other(format!("policy is not valid JSON: {e}")))?;
3481 doc.validate().map_err(|r| VectorError::Other(format!("policy rejected: {r:?}")))?;
3482
3483 let hash = crate::community::policy::harness::hash_policy_bytes(bytes.as_bytes());
3484 let hash_hex = crate::simd::hex::bytes_to_hex_32(&hash.0);
3485 let now_secs = web_time::SystemTime::now()
3486 .duration_since(web_time::UNIX_EPOCH)
3487 .map(|d| d.as_secs())
3488 .unwrap_or(0);
3489 crate::db::community::set_community_policy(&cid_hex, policy_id, bytes, &hash_hex, enabled, now_secs)
3490 .map_err(VectorError::Other)?;
3491 Self::invalidate_raid_report(&cid_hex);
3494 Ok(serde_json::json!({ "policy_id": policy_id, "hash": hash_hex, "enabled": enabled }))
3495 }
3496
3497 pub fn delete_community_policy(&self, community_id: &str, policy_id: &str) -> Result<()> {
3498 let community = Self::load_v2_if_v2(community_id)?
3499 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3500 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3501 crate::db::community::delete_community_policy(&cid_hex, policy_id).map_err(VectorError::Other)?;
3502 Self::invalidate_raid_report(&cid_hex);
3503 Ok(())
3504 }
3505
3506 pub fn builtin_policy_json(&self) -> Result<String> {
3509 serde_json::to_string_pretty(&crate::community::policy::harness::default_policy())
3510 .map_err(|e| VectorError::Other(e.to_string()))
3511 }
3512
3513 fn policy_console_report(cid_hex: &str, community: &crate::community::v2::community::CommunityV2) -> Result<std::sync::Arc<serde_json::Value>> {
3521 use crate::community::v2::guestbook::GuestbookEntry;
3522 let now_secs = web_time::SystemTime::now()
3523 .duration_since(web_time::UNIX_EPOCH)
3524 .map(|d| d.as_secs())
3525 .unwrap_or(0);
3526 {
3527 let cache = policy_report_cache();
3528 let guard = cache.lock().unwrap_or_else(|e| e.into_inner());
3529 if let Some((at, cached)) = guard.get(cid_hex) {
3530 if now_secs.saturating_sub(*at) < RAID_REPORT_TTL_SECS {
3531 return Ok(cached.clone());
3532 }
3533 }
3534 }
3535
3536 let owner = community.owner().map_err(VectorError::Other)?;
3537 let me = state::my_public_key();
3538
3539 use crate::community::roles::Permissions;
3543 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3544 | Permissions::MANAGE_CHANNELS
3545 | Permissions::MANAGE_METADATA
3546 | Permissions::KICK
3547 | Permissions::BAN
3548 | Permissions::MANAGE_MESSAGES;
3549 let roster = crate::db::community::get_community_roles(cid_hex).unwrap_or_default();
3550 let staff: std::collections::HashSet<String> = roster
3551 .grants
3552 .iter()
3553 .filter(|g| {
3554 g.role_ids
3555 .iter()
3556 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0))
3557 })
3558 .map(|g| g.member.clone())
3559 .collect();
3560 let roles_of: std::collections::HashMap<String, Vec<String>> =
3561 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3562
3563 let (events, _cursor) = crate::db::community::get_guestbook(cid_hex).unwrap_or_default();
3564 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3565 let mut invite_label: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3566 for e in &events {
3567 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3568 let hex = member.to_hex();
3569 let slot = joined_at.entry(hex.clone()).or_insert(*at_ms);
3571 *slot = (*slot).min(*at_ms);
3572 if let Some((_creator, label)) = invited_by {
3573 invite_label.entry(hex).or_insert_with(|| label.clone());
3574 }
3575 }
3576
3577 let members: Vec<crate::community::policy::harness::MemberRow> =
3578 crate::community::v2::service::stored_memberlist(community)
3579 .unwrap_or_default()
3580 .into_iter()
3581 .map(|pk| {
3582 let hex = pk.to_hex();
3583 (
3584 pk,
3585 joined_at.get(&hex).copied(),
3586 staff.contains(&hex),
3587 roles_of.get(&hex).cloned().unwrap_or_default(),
3588 invite_label.get(&hex).cloned(),
3589 )
3590 })
3591 .collect();
3592
3593 let now_ms = now_secs.saturating_mul(1000);
3594 let assembled =
3595 crate::community::policy::harness::assemble(cid_hex, &owner, me.as_ref(), &members, now_ms)
3596 .map_err(VectorError::Other)?;
3597 let report = std::sync::Arc::new(
3598 crate::community::policy::harness::evaluate_for_console(cid_hex, &assembled, now_ms)
3599 .map_err(VectorError::Other)?,
3600 );
3601 policy_report_cache()
3602 .lock()
3603 .unwrap_or_else(|e| e.into_inner())
3604 .insert(cid_hex.to_string(), (now_secs, report.clone()));
3605 Ok(report)
3606 }
3607
3608 pub fn policy_side_by_side(&self, community_id: &str) -> Result<serde_json::Value> {
3612 use crate::community::v2::guestbook::GuestbookEntry;
3613 use nostr_sdk::prelude::PublicKey;
3614 let community = Self::load_v2_if_v2(community_id)?
3615 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3616 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3617 let owner = community.owner().map_err(VectorError::Other)?;
3618
3619 use crate::community::roles::Permissions;
3622 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3623 | Permissions::MANAGE_CHANNELS
3624 | Permissions::MANAGE_METADATA
3625 | Permissions::KICK
3626 | Permissions::BAN
3627 | Permissions::MANAGE_MESSAGES;
3628 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3629 let staff: std::collections::HashSet<String> = roster
3630 .grants
3631 .iter()
3632 .filter(|g| {
3633 g.role_ids.iter().any(|rid| {
3634 roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0)
3635 })
3636 })
3637 .map(|g| g.member.clone())
3638 .collect();
3639 let roles_of: std::collections::HashMap<String, Vec<String>> =
3640 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3641
3642 let (events, _cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
3643 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3644 for e in &events {
3645 let GuestbookEntry::Join { member, at_ms, .. } = &e.entry else { continue };
3646 let hex = member.to_hex();
3647 let slot = joined_at.entry(hex).or_insert(*at_ms);
3648 *slot = (*slot).min(*at_ms);
3649 }
3650
3651 let members: Vec<(PublicKey, Option<u64>, bool, Vec<String>)> =
3652 crate::community::v2::service::stored_memberlist(&community)
3653 .unwrap_or_default()
3654 .into_iter()
3655 .map(|pk| {
3656 let hex = pk.to_hex();
3657 (pk, joined_at.get(&hex).copied(), staff.contains(&hex), roles_of.get(&hex).cloned().unwrap_or_default())
3658 })
3659 .collect();
3660
3661 let assessments: Vec<(String, String)> = Self::raid_report(&cid_hex)
3663 .map(|r| {
3664 r.members
3665 .iter()
3666 .map(|m| (m.npub.clone(), format!("{:?}", m.verdict).to_lowercase()))
3667 .collect()
3668 })
3669 .unwrap_or_default();
3670
3671 let now_ms = web_time::SystemTime::now()
3672 .duration_since(web_time::UNIX_EPOCH)
3673 .map(|d| d.as_millis() as u64)
3674 .unwrap_or(0);
3675 let diff = crate::community::policy::harness::run_side_by_side(
3676 &cid_hex, &owner, &members, &assessments, now_ms,
3677 )
3678 .map_err(VectorError::Other)?;
3679 serde_json::to_value(diff).map_err(|e| VectorError::Other(e.to_string()))
3680 }
3681
3682 pub fn invalidate_raid_report(community_id: &str) {
3686 policy_report_cache().lock().unwrap_or_else(|e| e.into_inner()).remove(community_id);
3687 raid_report_cache().lock().unwrap_or_else(|e| e.into_inner()).remove(community_id);
3688 }
3689
3690 pub async fn refound_community(&self, community_id: &str, retain: &[&str]) -> Result<()> {
3698 let community = Self::load_v2_if_v2(community_id)?
3699 .ok_or_else(|| VectorError::Other("re-founding requires a Concord v2 community".into()))?;
3700 let removed = Self::members_to_remove(&community, retain)?;
3701 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3702 crate::community::v2::service::refound_community(&transport, &community, &removed)
3703 .await
3704 .map_err(VectorError::Other)?;
3705 Self::invalidate_raid_report(community_id);
3707 Ok(())
3708 }
3709
3710 fn members_to_remove(
3728 community: &crate::community::v2::community::CommunityV2,
3729 retain: &[&str],
3730 ) -> Result<Vec<nostr_sdk::prelude::PublicKey>> {
3731 use nostr_sdk::prelude::PublicKey;
3732 let keep: std::collections::HashSet<PublicKey> =
3733 retain.iter().filter_map(|n| PublicKey::parse(n).ok()).collect();
3734 if keep.is_empty() {
3735 return Ok(Vec::new());
3736 }
3737 let me = state::my_public_key();
3738 let owner = community.owner().ok();
3739 Ok(crate::community::v2::service::stored_memberlist(community)
3740 .map_err(VectorError::Other)?
3741 .into_iter()
3742 .filter(|pk| !keep.contains(pk))
3743 .filter(|pk| Some(*pk) != me && Some(*pk) != owner)
3744 .collect())
3745 }
3746
3747 pub async fn purge_and_refound(
3763 &self,
3764 community_id: &str,
3765 retain: &[&str],
3766 on_progress: &(dyn Fn(usize, usize) + Sync),
3767 ) -> Result<serde_json::Value> {
3768 let community = Self::load_v2_if_v2(community_id)?
3769 .ok_or_else(|| VectorError::Other("re-founding requires a Concord v2 community".into()))?;
3770 let removed = Self::members_to_remove(&community, retain)?;
3771 let keep: Vec<nostr_sdk::prelude::PublicKey> = retain
3776 .iter()
3777 .filter_map(|n| nostr_sdk::prelude::PublicKey::parse(n).ok())
3778 .collect();
3779 log_info!("[Moderation] purge: {} to remove, {} retained", removed.len(), keep.len());
3780 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3781 if keep.is_empty() {
3792 crate::community::v2::service::refound_community(&transport, &community, &removed)
3793 .await
3794 .map_err(VectorError::Other)?;
3795 } else {
3796 crate::community::v2::service::refound_community_retaining(&transport, &community, &removed, &keep)
3797 .await
3798 .map_err(VectorError::Other)?;
3799 }
3800 let fresh = Self::load_v2_if_v2(community_id)?
3804 .ok_or_else(|| VectorError::Other("community gone during the purge".into()))?;
3805 let sweep = crate::community::v2::service::kick_members(&transport, &fresh, &removed, on_progress)
3806 .await
3807 .map_err(VectorError::Other)?;
3808 let _ = crate::community::v2::service::sync_guestbook(&transport, &fresh).await;
3811 log_info!(
3812 "[Moderation] purge: rotated to epoch {}, kicked {}/{} ({} refused, {} failed)",
3813 fresh.root_epoch.0,
3814 sweep.kicked.len(),
3815 removed.len(),
3816 sweep.refused.len(),
3817 sweep.failed.len()
3818 );
3819 Ok(serde_json::json!({
3820 "kicked": sweep.kicked.len(),
3821 "refused": sweep.refused.len(),
3822 "failed": sweep.failed.len(),
3823 "rotated": true,
3824 }))
3825 }
3826
3827 pub async fn kick_community_members(
3830 &self,
3831 community_id: &str,
3832 npubs: &[&str],
3833 on_progress: &(dyn Fn(usize, usize) + Sync),
3834 ) -> Result<serde_json::Value> {
3835 let community = Self::load_v2_if_v2(community_id)?
3836 .ok_or_else(|| VectorError::Other("kicking requires a Concord v2 community".into()))?;
3837 use nostr_sdk::prelude::PublicKey;
3838 let targets: Vec<PublicKey> = npubs.iter().filter_map(|n| PublicKey::parse(n).ok()).collect();
3839 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3840 let sweep = crate::community::v2::service::kick_members(&transport, &community, &targets, on_progress)
3841 .await
3842 .map_err(VectorError::Other)?;
3843 let _ = crate::community::v2::service::sync_guestbook(&transport, &community).await;
3844 log_info!(
3845 "[Moderation] kick sweep: {}/{} kicked ({} refused, {} failed)",
3846 sweep.kicked.len(),
3847 targets.len(),
3848 sweep.refused.len(),
3849 sweep.failed.len()
3850 );
3851 Self::invalidate_raid_report(community_id);
3852 Ok(serde_json::json!({
3853 "kicked": sweep.kicked.len(),
3854 "refused": sweep.refused.iter().map(|(_, why)| why.clone()).collect::<Vec<_>>(),
3855 "failed": sweep.failed.len(),
3856 }))
3857 }
3858
3859 pub async fn revoke_all_public_invites(&self, community_id: &str) -> Result<(usize, usize)> {
3864 let tokens: Vec<String> = crate::db::community::list_public_invites(community_id)
3865 .map_err(VectorError::Other)?
3866 .into_iter()
3867 .map(|i| i.token)
3868 .collect();
3869 let mut revoked = 0usize;
3870 let mut failed = 0usize;
3871 for token in &tokens {
3872 match self.revoke_public_invite(community_id, token).await {
3873 Ok(()) => revoked += 1,
3874 Err(_) => failed += 1,
3875 }
3876 }
3877 Ok((revoked, failed))
3878 }
3879
3880 pub async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
3888 crate::db::scoped(async move {
3889 use crate::community::transport::LiveTransport;
3890 let session = crate::db::current_session();
3891 let lock = crate::community::v2::realtime::follow_lock(id);
3896 let _guard = lock.lock().await;
3897 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3898 let mut warnings: Vec<String> = Vec::new();
3899 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
3900 warnings.push("v2 community not found".to_string());
3901 return warnings;
3902 };
3903 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
3904 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
3905 Ok(f) if f.dissolved => return warnings,
3907 Ok(f) if f.self_removed => {
3908 let _ = crate::db::community::delete_community_retain_keys(&cid_hex);
3916 return warnings;
3917 }
3918 Ok(_) => {}
3919 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
3920 }
3921 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
3922 match crate::community::v2::service::follow_control(&transport, &fresh).await {
3923 Ok(control) if control.moved() => {
3932 if let Ok(Some(latest)) = crate::db::community::load_community_v2(id) {
3933 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &latest, &session).await {
3934 warnings.push(format!("v2 rekey follow failed: {e}"));
3935 }
3936 }
3937 }
3938 Ok(_) => {}
3939 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
3940 }
3941 }
3942 if let Some(me) = crate::my_public_key() {
3947 if crate::db::community::is_author_banned(&cid_hex, &me) {
3948 let _ = crate::db::community::delete_community_retain_keys(&cid_hex);
3949 }
3950 }
3951 warnings
3952 })
3953 .await
3954 }
3955
3956 #[allow(clippy::too_many_arguments)]
3968 pub(crate) async fn v2_backfill_channel(
3969 id: &crate::community::CommunityId,
3970 channel_id: &str,
3971 limit: usize,
3972 max_pages: usize,
3973 since: Option<u64>,
3974 until: Option<u64>,
3975 evidence: crate::community::transport::Evidence,
3976 transport_secs: u64,
3977 ) -> usize {
3978 Self::v2_backfill_channel_counted(id, channel_id, limit, max_pages, since, until, evidence, transport_secs)
3979 .await
3980 .new_messages
3981 }
3982
3983 #[allow(clippy::too_many_arguments)]
3988 pub(crate) async fn v2_backfill_channel_counted(
3989 id: &crate::community::CommunityId,
3990 channel_id: &str,
3991 limit: usize,
3992 max_pages: usize,
3993 since: Option<u64>,
3994 until: Option<u64>,
3995 evidence: crate::community::transport::Evidence,
3996 transport_secs: u64,
3997 ) -> BackfillCount {
3998 let session = crate::db::current_session();
4001 let Some(my_pk) = state::my_public_key() else { return BackfillCount::default() };
4002 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
4006 return BackfillCount::default();
4007 }
4008 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return BackfillCount::default() };
4009 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4010 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
4011 let Ok(page) = crate::community::v2::service::fetch_channel_history(
4012 &transport,
4013 &community,
4014 &ch,
4015 limit.max(50),
4016 max_pages,
4017 since,
4018 until,
4019 evidence,
4020 |page| {
4025 let mut saw_message = false;
4026 for f in page {
4027 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
4028 saw_message = true;
4029 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
4030 return true;
4031 }
4032 }
4033 }
4034 !saw_message
4035 },
4036 )
4037 .await
4038 else {
4039 return BackfillCount::default();
4040 };
4041 let fetched = page.len();
4042 let new_messages = Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await;
4043 BackfillCount { fetched, new_messages }
4044 }
4045
4046 pub(crate) async fn v2_ingest_chat_page(
4050 channel_id: &str,
4051 my_pk: nostr_sdk::prelude::PublicKey,
4052 session: std::sync::Arc<crate::db::Session>,
4053 page: Vec<crate::community::v2::service::FetchedEvent>,
4054 ) -> usize {
4055 crate::db::scoped(async move {
4056 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
4057 let mut new = 0usize;
4058 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
4060 for f in &page {
4061 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
4066 if opened.author != my_pk {
4067 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
4068 let Ok(npub) = ToBech32::to_bech32(&opened.author);
4069 crate::community::service::persist_webxdc_signal(
4070 channel_id,
4071 &npub,
4072 &topic,
4073 addr.as_deref(),
4074 &opened.rumor_id.to_hex(),
4075 opened.at_ms / 1000,
4076 )
4077 .await;
4078 }
4079 }
4080 continue;
4081 }
4082 let outcome = {
4083 let mut st = state::STATE.lock().await;
4084 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
4085 };
4086 if let Some(outcome) = outcome {
4087 if matches!(outcome, ChatPersist::New(_)) {
4088 new += 1;
4089 }
4090 outcomes.push(outcome);
4091 }
4092 }
4093 for o in &mut outcomes {
4100 if let ChatPersist::New(m) | ChatPersist::Updated { message: m, .. } = o {
4101 crate::db::attachments::verify_message_attachments(m, Some(channel_id)).await;
4102 }
4103 }
4104 let mut pending: Vec<&crate::types::Message> = Vec::new();
4105 for outcome in &outcomes {
4106 if !session.is_live() {
4107 pending.clear();
4108 break;
4109 }
4110 match outcome {
4111 ChatPersist::New(m) => pending.push(m),
4112 ChatPersist::Updated { message, edit_event } => match edit_event {
4113 Some(ev) => {
4114 let mut ev = (**ev).clone();
4115 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
4118 ev.chat_id = cid;
4119 }
4120 let _ = crate::db::events::save_event(&ev).await;
4121 }
4122 None => pending.push(message),
4123 },
4124 ChatPersist::Removed(target_id) => {
4125 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4126 let _ = crate::db::events::delete_event(target_id).await;
4127 }
4128 ChatPersist::ReactionRemoved { reaction_id, message } => {
4129 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4130 let _ = crate::db::events::delete_event(reaction_id).await;
4131 pending.push(message);
4132 }
4133 }
4134 }
4135 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4136 drop(pending);
4137 {
4141 let quoted: Vec<&mut crate::types::Message> = outcomes
4142 .iter_mut()
4143 .filter_map(|o| match o {
4144 ChatPersist::New(m)
4145 | ChatPersist::Updated { message: m, .. }
4146 | ChatPersist::ReactionRemoved { message: m, .. } => Some(m),
4147 ChatPersist::Removed(_) => None,
4148 })
4149 .collect();
4150 let _ = crate::db::events::populate_reply_contexts(quoted).await;
4151 }
4152 for outcome in &outcomes {
4158 match outcome {
4159 ChatPersist::New(msg) => crate::traits::emit_message_new(channel_id, msg),
4160 ChatPersist::Updated { message, .. }
4161 | ChatPersist::ReactionRemoved { message, .. } => {
4162 let mut message = message.clone();
4163 let target_id = message.id.clone();
4164 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
4165 }
4166 ChatPersist::Removed(target_id) => crate::traits::emit_event(
4167 "message_removed",
4168 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
4169 ),
4170 }
4171 }
4172 new
4173 })
4174 .await
4175 }
4176
4177 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
4181 if community_id.len() != 64 {
4182 return Ok(None);
4183 }
4184 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4185 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4186 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
4187 _ => Ok(None),
4188 }
4189 }
4190
4191 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
4196 use crate::community::CommunityId;
4197 if community_id.len() != 64 {
4198 return Err(VectorError::Other("malformed community id".into()));
4199 }
4200 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
4201 .map_err(VectorError::Other)?
4202 .ok_or_else(|| VectorError::Other("community not found".into()))
4203 }
4204
4205 fn admin_role_id_of(community_id: &str) -> Result<String> {
4206 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4207 roles.roles.iter()
4210 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
4211 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_FOUNDING_MASK))
4212 .map(|r| r.role_id.clone())
4213 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
4214 }
4215
4216 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
4220 use crate::community::service;
4221 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4222 use crate::community::roles::Permissions;
4223 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
4224 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
4225 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4226 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4229 if banned.contains(&me) && me != owner_hex {
4230 return Ok(serde_json::json!({
4231 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
4232 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
4233 "pin_messages": false, "control_write": false,
4234 }));
4235 }
4236 let control_write = crate::community::v2::control::ControlPlane::of(&v2).can_write();
4242 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
4243 let has_ctl = |p: u64| control_write && has(p);
4244 return Ok(serde_json::json!({
4245 "manage_metadata": has_ctl(Permissions::MANAGE_METADATA), "manage_channels": has_ctl(Permissions::MANAGE_CHANNELS),
4246 "create_invite": has_ctl(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has_ctl(Permissions::BAN),
4247 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has_ctl(Permissions::MANAGE_ROLES),
4248 "manage_admin_role": me == owner_hex && control_write,
4250 "pin_messages": has_ctl(Permissions::PIN_MESSAGES),
4251 "control_write": control_write,
4252 }));
4253 }
4254 let community = Self::load_community_hex(community_id)?;
4255 let caps = service::caller_capabilities(&community);
4256 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
4257 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
4258 .unwrap_or(false);
4259 Ok(serde_json::json!({
4260 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
4261 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
4262 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
4263 "manage_admin_role": manage_admin_role,
4264 "pin_messages": false,
4266 }))
4267 }
4268
4269 pub async fn pin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
4272 use crate::community::{transport::LiveTransport, ChannelId};
4273 let v2 = Self::load_v2_if_v2(community_id)?
4274 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4275 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4276 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4277 crate::community::v2::service::pin_message(&transport, &v2, &ch, message_id)
4278 .await
4279 .map_err(VectorError::Other)
4280 }
4281
4282 pub async fn unpin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
4284 use crate::community::{transport::LiveTransport, ChannelId};
4285 let v2 = Self::load_v2_if_v2(community_id)?
4286 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4287 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4288 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4289 crate::community::v2::service::unpin_message(&transport, &v2, &ch, message_id)
4290 .await
4291 .map_err(VectorError::Other)
4292 }
4293
4294 pub async fn fetch_pinned_attachment(
4302 &self,
4303 community_id: &str,
4304 channel_id: &str,
4305 rumor_id: &str,
4306 ) -> Result<serde_json::Value> {
4307 crate::db::scoped(async move {
4308 use crate::community::ChannelId;
4309 let v2 = Self::load_v2_if_v2(community_id)?
4310 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4311 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4312 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
4313 let pin = pins
4314 .pins
4315 .iter()
4316 .find(|p| p.rumor_id == rumor_id)
4317 .ok_or_else(|| VectorError::Other("that message is not pinned".into()))?;
4318 let dir = crate::db::get_download_dir();
4319 let tag = pin
4320 .tags
4321 .iter()
4322 .find(|t| t.first().map(String::as_str) == Some("imeta"))
4323 .map(|t| nostr_sdk::prelude::Tag::custom("imeta", t[1..].to_vec()))
4324 .ok_or_else(|| VectorError::Other("this pin carries no attachment".into()))?;
4325 let attachment = crate::community::attachments::attachment_from_imeta(&tag, &dir)
4326 .ok_or_else(|| VectorError::Other("this pin's attachment metadata is malformed".into()))?;
4327
4328 let respond = |path: &std::path::Path| {
4329 serde_json::json!({
4330 "path": path.to_string_lossy(),
4331 "name": attachment.name.to_string(),
4332 "extension": attachment.extension.to_string(),
4333 })
4334 };
4335
4336 let expected = attachment.original_hash.as_deref();
4340 let path = std::path::PathBuf::from(&*attachment.path);
4341 if let Ok(bytes) = std::fs::read(&path) {
4342 match expected {
4343 Some(want) if crate::crypto::sha256_hex(&bytes) == want => return Ok(respond(&path)),
4344 None => return Ok(respond(&path)),
4345 _ => {} }
4347 }
4348
4349 let author_npub = nostr_sdk::prelude::PublicKey::from_hex(&pin.author)
4350 .ok()
4351 .and_then(|pk| nostr_sdk::prelude::ToBech32::to_bech32(&pk).ok());
4352 let bytes = self.download_attachment_from(&attachment, author_npub.as_deref()).await?;
4353 if let Some(want) = expected {
4354 if crate::crypto::sha256_hex(&bytes) != want {
4355 return Err(VectorError::Other("downloaded bytes do not match the pinned content hash".into()));
4356 }
4357 }
4358 std::fs::write(&path, &bytes).map_err(|e| VectorError::Other(format!("could not cache the attachment: {e}")))?;
4359 Ok(respond(&path))
4360 })
4361 .await
4362 }
4363
4364 pub fn get_channel_pins(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
4368 use crate::community::ChannelId;
4369 let Some(v2) = Self::load_v2_if_v2(community_id)? else {
4370 return Ok(serde_json::json!({ "pins": [], "sealed": false, "version": 0 }));
4373 };
4374 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4375 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
4376 serde_json::to_value(&pins).map_err(|e| VectorError::Other(e.to_string()))
4377 }
4378
4379 pub fn community_role_graph(&self, community_id: &str) -> Result<serde_json::Value> {
4387 use nostr_sdk::prelude::{PublicKey, ToBech32};
4388 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4389 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4391 let roles: Vec<serde_json::Value> = roster
4392 .roles
4393 .iter()
4394 .map(|r| {
4395 let channel_id = match &r.scope {
4398 crate::community::roles::RoleScope::Channel(c) => Some(c.to_ascii_lowercase()),
4399 crate::community::roles::RoleScope::Server => None,
4400 };
4401 serde_json::json!({
4402 "role_id": r.role_id,
4403 "name": r.name,
4404 "position": r.position,
4405 "color": r.color,
4406 "channel_id": channel_id,
4407 })
4408 })
4409 .collect();
4410 let grants: Vec<serde_json::Value> = roster
4411 .grants
4412 .iter()
4413 .filter(|g| !banned.contains(&g.member))
4414 .filter_map(|g| {
4415 let npub = PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok())?;
4416 Some(serde_json::json!({ "npub": npub, "role_ids": g.role_ids }))
4417 })
4418 .collect();
4419 Ok(serde_json::json!({ "roles": roles, "grants": grants }))
4420 }
4421
4422 pub fn community_roles_view(&self, community_id: &str) -> Result<serde_json::Value> {
4427 use crate::community::roles::{Permissions, RoleScope};
4428 let v2 = Self::load_v2_if_v2(community_id)?
4429 .ok_or_else(|| VectorError::Other("roles need a Concord v2 community".into()))?;
4430 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
4431 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
4432 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4433 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4434 let is_owner = me == owner_hex;
4435 let can_manage = is_owner
4436 || (!banned.contains(&me) && roster.is_authorized(&me, Some(&owner_hex), Permissions::MANAGE_ROLES));
4437 let rank = if is_owner { Some(0) } else { roster.highest_position(&me) };
4438 let grantable = if is_owner { u64::MAX } else { roster.effective_permissions(&me).0 };
4439 let mut roles: Vec<&crate::community::roles::Role> = roster.roles.iter().collect();
4440 roles.sort_by(|a, b| a.position.cmp(&b.position).then_with(|| a.role_id.cmp(&b.role_id)));
4441 let roles: Vec<serde_json::Value> = roles
4442 .into_iter()
4443 .map(|r| {
4444 let holders: Vec<String> = roster
4445 .grants
4446 .iter()
4447 .filter(|g| !banned.contains(&g.member) && g.role_ids.iter().any(|id| id == &r.role_id))
4448 .filter_map(|g| nostr_sdk::prelude::PublicKey::from_hex(&g.member).ok())
4449 .filter_map(|pk| nostr_sdk::prelude::ToBech32::to_bech32(&pk).ok())
4450 .collect();
4451 serde_json::json!({
4452 "role_id": r.role_id,
4453 "name": r.name,
4454 "position": r.position,
4455 "color": r.color,
4456 "permissions": r.permissions.0.to_string(),
4457 "channel_id": match &r.scope { RoleScope::Channel(c) => Some(c.to_ascii_lowercase()), RoleScope::Server => None },
4458 "holders": holders,
4459 "manageable": can_manage && (is_owner || rank.is_some_and(|p| p < r.position)),
4460 })
4461 })
4462 .collect();
4463 let channels: Vec<serde_json::Value> = v2
4464 .channels
4465 .iter()
4466 .map(|c| serde_json::json!({
4467 "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0),
4468 "name": c.name,
4469 "private": c.private,
4470 }))
4471 .collect();
4472 Ok(serde_json::json!({
4473 "is_owner": is_owner,
4474 "can_manage": can_manage,
4475 "rank": rank,
4476 "grantable": grantable.to_string(),
4477 "roles": roles,
4478 "channels": channels,
4479 "max_roles": crate::community::v2::roles::MAX_ROLES_PER_COMMUNITY,
4480 "max_per_member": crate::community::v2::roles::MAX_ROLES_PER_MEMBER,
4481 }))
4482 }
4483
4484 fn parse_permissions(s: &str) -> Result<u64> {
4485 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
4486 return Err(VectorError::Other(format!("permissions must be a decimal string, got {s:?}")));
4487 }
4488 s.parse::<u64>().map_err(|_| VectorError::Other("permissions out of range".into()))
4489 }
4490
4491 fn role_scope(channel_id: Option<&str>) -> Result<crate::community::roles::RoleScope> {
4492 use crate::community::roles::RoleScope;
4493 match channel_id {
4494 None => Ok(RoleScope::Server),
4495 Some(c) if c.len() == 64 && c.bytes().all(|b| b.is_ascii_hexdigit()) => Ok(RoleScope::Channel(c.to_ascii_lowercase())),
4496 Some(_) => Err(VectorError::Other("channel_id must be 32-byte hex".into())),
4497 }
4498 }
4499
4500 pub async fn create_role(&self, community_id: &str, name: &str, color: u32, permissions: &str, channel_id: Option<&str>) -> Result<String> {
4502 let v2 = Self::load_v2_if_v2(community_id)?
4503 .ok_or_else(|| VectorError::Other("roles need a Concord v2 community".into()))?;
4504 let perms = Self::parse_permissions(permissions)?;
4505 let scope = Self::role_scope(channel_id)?;
4506 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4507 let id = crate::community::v2::service::create_role(&transport, &v2, name, color, perms, scope)
4508 .await
4509 .map_err(VectorError::Other)?;
4510 Self::converge_v2_authority(&transport, community_id).await;
4511 Ok(id)
4512 }
4513
4514 pub async fn edit_role(&self, community_id: &str, role_id: &str, name: &str, color: u32, permissions: &str, channel_id: Option<&str>) -> Result<()> {
4516 let v2 = Self::load_v2_if_v2(community_id)?
4517 .ok_or_else(|| VectorError::Other("roles need a Concord v2 community".into()))?;
4518 let perms = Self::parse_permissions(permissions)?;
4519 let scope = Self::role_scope(channel_id)?;
4520 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4521 crate::community::v2::service::edit_role(&transport, &v2, role_id, name, color, perms, scope)
4522 .await
4523 .map_err(VectorError::Other)?;
4524 Self::converge_v2_authority(&transport, community_id).await;
4525 Ok(())
4526 }
4527
4528 pub async fn reorder_roles(&self, community_id: &str, ordered: &[String]) -> Result<usize> {
4530 let v2 = Self::load_v2_if_v2(community_id)?
4531 .ok_or_else(|| VectorError::Other("roles need a Concord v2 community".into()))?;
4532 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4533 let moved = crate::community::v2::service::reorder_roles(&transport, &v2, ordered)
4534 .await
4535 .map_err(VectorError::Other)?;
4536 if moved > 0 {
4537 Self::converge_v2_authority(&transport, community_id).await;
4538 }
4539 Ok(moved)
4540 }
4541
4542 pub async fn set_member_roles(&self, community_id: &str, npub: &str, role_ids: Vec<String>) -> Result<()> {
4544 let v2 = Self::load_v2_if_v2(community_id)?
4545 .ok_or_else(|| VectorError::Other("roles need a Concord v2 community".into()))?;
4546 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4547 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4548 crate::community::v2::service::set_member_roles(&transport, &v2, &member, role_ids)
4549 .await
4550 .map_err(VectorError::Other)?;
4551 Self::converge_v2_authority(&transport, community_id).await;
4552 Ok(())
4553 }
4554
4555 pub fn member_has_permission(&self, community_id: &str, npub: &str, permission: u64) -> Result<bool> {
4571 use nostr_sdk::prelude::PublicKey;
4572 let who = PublicKey::parse(npub).map_err(|_| VectorError::Other(format!("invalid npub: {npub}")))?.to_hex();
4573 let owner_hex = match Self::load_v2_if_v2(community_id)? {
4574 Some(v2) => v2.owner().map_err(VectorError::Other)?.to_hex(),
4575 None => Self::load_community_hex(community_id)?
4576 .owner_attestation
4577 .as_ref()
4578 .and_then(|att| crate::community::owner::verify_owner_attestation(att, community_id))
4579 .map(|pk| pk.to_hex())
4580 .unwrap_or_default(),
4581 };
4582 if who != owner_hex && crate::db::community::get_community_banlist(community_id).unwrap_or_default().contains(&who) {
4583 return Ok(false);
4584 }
4585 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4586 Ok(roster.is_authorized(&who, Some(&owner_hex), permission))
4587 }
4588
4589 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
4592 use nostr_sdk::prelude::{PublicKey, ToBech32};
4593 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4594 let owner = v2.owner().map_err(VectorError::Other)?;
4595 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4596 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4598 let admins: Vec<String> = roster.grants.iter()
4599 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
4600 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
4601 .collect();
4602 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
4603 }
4604 let community = Self::load_community_hex(community_id)?;
4605 let owner = community.owner_attestation.as_ref()
4606 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
4607 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
4608 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4609 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
4610 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
4611 .collect();
4612 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
4613 }
4614
4615 async fn converge_v2_authority(
4624 transport: &crate::community::transport::LiveTransport,
4625 community_id: &str,
4626 ) {
4627 crate::db::scoped(async move {
4628 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
4631 let _ = crate::community::v2::service::follow_control(transport, &fresh).await;
4632 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh).await {
4637 if !added.is_empty() {
4638 traits::emit_event_json(
4639 "community_refreshed",
4640 serde_json::json!({ "community_id": community_id }),
4641 );
4642 }
4643 }
4644 }
4645 })
4646 .await
4647 }
4648
4649 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
4651 use crate::community::{service, transport::LiveTransport};
4652 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4653 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4654 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4655 crate::community::v2::service::grant_admin(&transport, &v2, &member)
4656 .await
4657 .map_err(VectorError::Other)?;
4658 Self::converge_v2_authority(&transport, community_id).await;
4659 return Ok(());
4660 }
4661 let community = Self::load_community_hex(community_id)?;
4662 let role_id = Self::admin_role_id_of(community_id)?;
4663 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
4664 }
4665
4666 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
4668 use crate::community::{service, transport::LiveTransport};
4669 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4670 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4671 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4672 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
4673 .await
4674 .map_err(VectorError::Other)?;
4675 Self::converge_v2_authority(&transport, community_id).await;
4676 return Ok(());
4677 }
4678 let community = Self::load_community_hex(community_id)?;
4679 let role_id = Self::admin_role_id_of(community_id)?;
4680 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
4681 }
4682
4683 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
4685 crate::db::scoped(async move {
4686 use crate::community::{service, transport::LiveTransport};
4687 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4688 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4689 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4690 crate::community::v2::service::kick_member(&transport, &v2, &pk)
4691 .await
4692 .map_err(VectorError::Other)?;
4693 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2).await {
4698 if !fresh.is_empty() {
4699 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
4700 }
4701 }
4702 Self::converge_v2_authority(&transport, community_id).await;
4703 return Ok(());
4704 }
4705 let community = Self::load_community_hex(community_id)?;
4706 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
4707 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
4708 })
4709 .await
4710 }
4711
4712 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
4718 self.set_members_banned(community_id, &[npub], banned).await
4719 }
4720
4721 pub async fn set_members_banned(&self, community_id: &str, npubs: &[&str], banned: bool) -> Result<()> {
4731 use crate::community::{service, transport::LiveTransport, CommunityId};
4732 if npubs.is_empty() {
4733 return Ok(());
4734 }
4735 let mut pks: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(npubs.len());
4737 for n in npubs {
4738 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4739 if !pks.contains(&pk) {
4740 pks.push(pk);
4741 }
4742 }
4743 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4744 if community_id.len() == 64 {
4748 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4749 if let Some(crate::community::ConcordProtocol::V2) = crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4753 return crate::community::v2::service::set_members_banned(&transport, &cid, &pks, banned)
4754 .await
4755 .map_err(VectorError::Other);
4756 }
4757 }
4758 let hexes: Vec<String> = pks.iter().map(|p| p.to_hex()).collect();
4760 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
4761 list.retain(|h| !hexes.contains(h));
4762 if banned {
4763 list.extend(hexes);
4764 }
4765 let community = Self::load_community_hex(community_id)?;
4766 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
4767 Self::invalidate_raid_report(community_id);
4770 Ok(())
4771 }
4772
4773 pub async fn rescue_stranded_members(&self, community_id: &str, npubs: &[&str]) -> Result<serde_json::Value> {
4784 use nostr_sdk::prelude::PublicKey;
4785 let community = Self::load_v2_if_v2(community_id)?
4786 .ok_or_else(|| VectorError::Other("rescuing requires a Concord v2 community".into()))?;
4787 let mut targets: Vec<PublicKey> = Vec::with_capacity(npubs.len());
4788 for n in npubs {
4789 let pk = PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4790 if !targets.contains(&pk) {
4791 targets.push(pk);
4792 }
4793 }
4794 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
4795 let report = crate::community::v2::service::rescue_stranded_members(&transport, &community, &targets)
4796 .await
4797 .map_err(VectorError::Other)?;
4798 serde_json::to_value(report).map_err(|e| VectorError::Other(e.to_string()))
4799 }
4800
4801 pub async fn repair_community_roster(&self, community_id: &str) -> Result<serde_json::Value> {
4812 use crate::community::CommunityId;
4813 if community_id.len() != 64 {
4814 return Err(VectorError::Other("malformed community id".into()));
4815 }
4816 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4817 if crate::db::community::load_community_v2(&cid).map_err(VectorError::Other)?.is_none() {
4818 return Err(VectorError::Other("not a held Concord v2 community".into()));
4819 }
4820 let before = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4821 let before_at = crate::db::community::get_community_roles_at(community_id).map_err(VectorError::Other)?;
4822 crate::db::community::set_community_roles(community_id, &before, 0).map_err(VectorError::Other)?;
4823 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
4825 let community = crate::db::community::load_community_v2(&cid)
4826 .map_err(VectorError::Other)?
4827 .ok_or_else(|| VectorError::Other("community vanished mid-repair".into()))?;
4828 let follow = crate::community::v2::service::follow_control(&transport, &community).await;
4829 let after = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4830 Ok(serde_json::json!({
4831 "provenance_before": before_at,
4832 "provenance_after": crate::db::community::get_community_roles_at(community_id).unwrap_or(0),
4833 "roles_before": before.roles.len(),
4834 "grants_before": before.grants.len(),
4835 "roles_after": after.roles.len(),
4836 "grants_after": after.grants.len(),
4837 "fold_error": follow.as_ref().err().cloned(),
4838 "repaired": after.grants.len() > before.grants.len(),
4839 }))
4840 }
4841
4842 pub async fn explain_epoch_state(&self, community_id: &str) -> Result<serde_json::Value> {
4852 use crate::community::{transport::LiveTransport, CommunityId};
4853 if community_id.len() != 64 {
4854 return Err(VectorError::Other("malformed community id".into()));
4855 }
4856 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4857 let community = crate::db::community::load_community_v2(&cid)
4858 .map_err(VectorError::Other)?
4859 .ok_or_else(|| VectorError::Other("not a held Concord v2 community".into()))?;
4860 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(15));
4861 crate::community::v2::service::explain_epoch_state(&transport, &community)
4862 .await
4863 .map_err(VectorError::Other)
4864 }
4865
4866 pub async fn contain_raid(
4879 &self,
4880 community_id: &str,
4881 npubs: &[&str],
4882 window_start_secs: u64,
4883 cut_also: &[&str],
4884 ) -> Result<crate::community::v2::service::ContainmentReport> {
4885 use crate::community::{transport::LiveTransport, CommunityId};
4886 if community_id.len() != 64 {
4887 return Err(VectorError::Other("malformed community id".into()));
4888 }
4889 let mut pks: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(npubs.len());
4890 for n in npubs {
4891 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4892 if !pks.contains(&pk) {
4893 pks.push(pk);
4894 }
4895 }
4896 let mut extra: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(cut_also.len());
4897 for n in cut_also {
4898 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4899 if !extra.contains(&pk) {
4900 extra.push(pk);
4901 }
4902 }
4903 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4904 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4905 Some(crate::community::ConcordProtocol::V2) => {}
4906 _ => return Err(VectorError::Other("raid containment requires a Concord v2 community".into())),
4909 }
4910 let session = crate::db::current_session();
4913 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
4914 let report = crate::community::v2::service::contain_raid(&transport, &cid, &pks, window_start_secs, &extra)
4915 .await
4916 .map_err(VectorError::Other)?;
4917 if !session.is_live() {
4918 return Err(VectorError::Other("account changed during raid containment".into()));
4919 }
4920 Self::invalidate_raid_report(community_id);
4921 Ok(report)
4922 }
4923
4924 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
4928 use crate::community::{service, transport::LiveTransport, CommunityId};
4929 if community_id.len() != 64 {
4930 return Err(VectorError::Other("malformed community id".into()));
4931 }
4932 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4933 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4934 if let Ok(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid) {
4937 let community = crate::db::community::load_community_v2(&cid)
4938 .map_err(VectorError::Other)?
4939 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
4940 return crate::community::v2::service::dissolve_community(&transport, &community)
4941 .await
4942 .map_err(VectorError::Other);
4943 }
4944 let community = Self::load_community_hex(community_id)?;
4945 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
4946 }
4947
4948 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
4951 use crate::community::{service, transport::LiveTransport, CommunityId};
4952 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4953 if community_id.len() == 64 {
4958 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4959 if let Ok(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid) {
4960 let community = crate::db::community::load_community_v2(&cid)
4961 .map_err(VectorError::Other)?
4962 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
4963 let mut meta = community.metadata();
4964 if let Some(n) = name {
4965 meta.name = n.to_string();
4966 }
4967 if let Some(d) = description {
4968 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
4969 }
4970 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
4971 .await
4972 .map_err(VectorError::Other);
4973 }
4974 }
4975 let mut community = Self::load_community_hex(community_id)?;
4976 if let Some(n) = name { community.name = n.to_string(); }
4977 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
4978 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
4979 }
4980
4981
4982
4983 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
4986 crate::db::scoped(async move {
4987 use crate::community::{transport::LiveTransport, CommunityId};
4988 if community_id.len() != 64 {
4989 return Err(VectorError::Other("malformed community id".into()));
4990 }
4991 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4992 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4994 let channel_ids: Vec<String> =
4995 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
4996 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4997 crate::community::v2::service::leave_community(&transport, &v2)
4998 .await
4999 .map_err(VectorError::Other)?;
5000 let mut st = state::STATE.lock().await;
5001 st.chats.retain(|c| !channel_ids.contains(&c.id));
5002 return Ok(());
5003 }
5004 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
5005 let channel_ids: Vec<String> = community
5006 .as_ref()
5007 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
5008 .unwrap_or_default();
5009 if let Some(ref c) = community {
5011 if let Some(primary) = c.channels.first() {
5012 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
5013 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
5014 }
5015 }
5016 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
5018 {
5019 let mut st = state::STATE.lock().await;
5020 st.chats.retain(|c| !channel_ids.contains(&c.id));
5021 }
5022 Ok(())
5023 })
5024 .await
5025 }
5026
5027 fn ensure_v1_writable(community: &crate::community::Community) -> Result<()> {
5035 if crate::db::community::get_community_dissolved(&community.id.to_hex()).unwrap_or(false) {
5036 return Err(VectorError::Other("this community has been dissolved".into()));
5037 }
5038 Ok(())
5039 }
5040
5041 fn resolve_channel(
5042 &self,
5043 channel_id: &str,
5044 ) -> Result<(crate::community::Community, crate::community::Channel)> {
5045 use crate::community::CommunityId;
5046 let community_id = crate::db::community::community_id_for_channel(channel_id)
5047 .map_err(VectorError::Other)?
5048 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
5049 if community_id.len() != 64 {
5050 return Err(VectorError::Other("malformed community id".into()));
5051 }
5052 let community = crate::db::community::load_community(&CommunityId(
5053 crate::simd::hex::hex_to_bytes_32(&community_id),
5054 ))
5055 .map_err(VectorError::Other)?
5056 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
5057 let channel = community
5058 .channels
5059 .iter()
5060 .find(|c| c.id.to_hex() == channel_id)
5061 .cloned()
5062 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
5063 Ok((community, channel))
5064 }
5065
5066
5067 pub async fn sync_dms(
5084 &self,
5085 since_days: Option<u64>,
5086 handler: &dyn InboundEventHandler,
5087 ) -> Result<(u32, u32)> {
5088 crate::db::scoped(async move {
5089 use futures_util::StreamExt;
5090 use nostr_sdk::prelude::*;
5091
5092 let client = state::nostr_client()
5093 .ok_or(VectorError::Other("Not connected".into()))?;
5094 let my_pk = state::my_public_key()
5095 .ok_or(VectorError::Other("Not logged in".into()))?;
5096
5097 let (items, filter) = if let Some(days) = since_days {
5102 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
5103 let items = db::wrappers::load_negentropy_items_since(since_ts)
5104 .unwrap_or_default();
5105 let filter = Filter::new()
5106 .pubkey(my_pk)
5107 .kind(Kind::GiftWrap)
5108 .since(Timestamp::from_secs(since_ts));
5109 (items, filter)
5110 } else {
5111 let items = db::wrappers::load_negentropy_items().unwrap_or_default();
5112 let filter = Filter::new()
5113 .pubkey(my_pk)
5114 .kind(Kind::GiftWrap);
5115 (items, filter)
5116 };
5117
5118 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
5119
5120 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
5122 .direction(nostr_sdk::prelude::SyncDirection::Down)
5123 .initial_timeout(std::time::Duration::from_secs(10))
5124 .dry_run();
5125
5126 let relay_map = client.relays().await;
5130 let (all_relays, no_neg_relays): (Vec<_>, Vec<_>) =
5131 relay_map.iter()
5132 .map(|(url, relay)| (url.clone(), relay.clone()))
5133 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
5134 drop(relay_map);
5135 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
5136 if !skipped_no_neg.is_empty() {
5137 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
5138 }
5139
5140 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
5144 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
5145 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
5146 .min(neg_outer);
5147 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
5148 for (url, relay) in &all_relays {
5149 let url = url.clone();
5150 let relay = relay.clone();
5151 let f = filter.clone();
5152 let i = items.clone();
5153 let o = sync_opts.clone();
5154 relay_futs.push(async move {
5155 if !negentropy::wait_connected(&relay, connect_allowance).await {
5156 return (url, None, false);
5157 }
5158 let result = crate::rt::time::timeout(
5161 neg_outer,
5162 relay.sync(f).items(i).opts(o),
5163 ).await;
5164 let connected = relay.status() == RelayStatus::Connected;
5165 (url, Some(result), connected)
5166 });
5167 }
5168
5169 let cap_session = crate::db::current_session();
5171 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
5172 while let Some((url, result, connected)) = relay_futs.next().await {
5173 let Some(result) = result else {
5174 log_warn!("[SyncDMs] {} skipped: not connected", url);
5175 continue;
5176 };
5177 match result {
5178 Ok(Ok(recon)) => {
5179 let count = recon.remote.len();
5180 all_missing.extend(recon.remote);
5181 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
5182 negentropy::record_neg_support(url.as_str(), true);
5183 }
5184 Ok(Err(e)) => {
5185 log_warn!("[SyncDMs] {} failed: {}", url, e);
5186 if cap_session.is_live()
5187 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
5188 {
5189 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
5190 negentropy::record_neg_support(url.as_str(), false);
5191 }
5192 }
5193 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
5194 }
5195 }
5196
5197 let mut total_events = 0u32;
5198 let mut new_messages = 0u32;
5199
5200 if !skipped_no_neg.is_empty() {
5205 let req_filter = filter.clone().limit(500);
5206 match client
5207 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
5208 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
5209 ))
5210 .timeout(std::time::Duration::from_secs(20))
5211 .await
5212 {
5213 Ok(stream) => {
5214 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
5215 let stream = stream.ready_chunks(event_handler::UNWRAP_BATCH);
5216 tokio::pin!(stream);
5217 while let Some(chunk) = stream.next().await {
5218 let events: Vec<Event> = chunk
5222 .into_iter()
5223 .filter_map(|(_relay, res)| res.ok())
5224 .filter(|event| seen.insert(event.id.to_bytes()))
5225 .collect();
5226 total_events += events.len() as u32;
5227 for prepared in event_handler::prepare_events(&events, my_pk).await {
5228 if event_handler::commit_prepared_event(prepared, false, handler).await {
5229 new_messages += 1;
5230 }
5231 }
5232 }
5233 }
5234 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
5235 }
5236 }
5237
5238 if all_missing.is_empty() {
5239 log_info!("[SyncDMs] No missing events");
5240 return Ok((total_events, new_messages));
5241 }
5242
5243 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
5245 let ids: Vec<EventId> = all_missing.into_iter().collect();
5246 let relay_strs: Vec<String> = client.relays().await.keys()
5247 .map(|u| u.to_string()).collect();
5248
5249 const BATCH_SIZE: usize = 500;
5250
5251 for batch in ids.chunks(BATCH_SIZE) {
5252 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
5255 match client
5256 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
5257 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
5258 ))
5259 .timeout(std::time::Duration::from_secs(30))
5260 .await
5261 {
5262 Ok(stream) => {
5263 let prepared_stream = stream
5264 .filter_map(|(_relay, res)| async move { res.ok() })
5265 .ready_chunks(event_handler::UNWRAP_BATCH)
5266 .map(move |events| {
5267 db::spawn_bound(async move {
5268 event_handler::prepare_events(&events, my_pk).await
5269 })
5270 })
5271 .buffer_unordered(8);
5272 tokio::pin!(prepared_stream);
5273
5274 while let Some(result) = prepared_stream.next().await {
5275 let Ok(batch) = result else { continue };
5276 total_events += batch.len() as u32;
5277 for prepared in batch {
5278 if event_handler::commit_prepared_event(prepared, false, handler).await {
5279 new_messages += 1;
5280 }
5281 }
5282 }
5283 }
5284 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
5285 }
5286 }
5287
5288 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
5289 Ok((total_events, new_messages))
5290 })
5291 .await
5292 }
5293
5294 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
5303 use nostr_sdk::prelude::*;
5304 let client = state::nostr_client()
5305 .ok_or(VectorError::Other("Not connected".into()))?;
5306 let my_pk = state::my_public_key()
5307 .ok_or(VectorError::Other("Not logged in".into()))?;
5308
5309 let filter = Filter::new()
5310 .pubkey(my_pk)
5311 .kind(Kind::GiftWrap)
5312 .limit(0);
5313
5314 let output = client.subscribe(filter).await
5315 .map_err(|e| VectorError::Nostr(e.to_string()))?;
5316 Ok(output.value)
5317 }
5318
5319 pub async fn sync_communities(&self) -> Result<Vec<String>> {
5334 let mut all_warnings: Vec<String> = Vec::new();
5335 {
5339 use crate::community::{transport::LiveTransport, v2::service as v2};
5340 let bootstrap: Vec<String> = match crate::state::nostr_client() {
5341 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
5342 None => Vec::new(),
5343 };
5344 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
5345 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
5346 let joined = outcome.joined;
5349 for c in &joined {
5350 if community::v2::realtime::follow_worker_running() {
5351 community::v2::realtime::enqueue_follow(c.id());
5352 } else {
5353 let cid_hex = crate::simd::hex::bytes_to_hex_32(&c.id().0);
5354 all_warnings.extend(
5355 Self::v2_inline_follow(c.id()).await.into_iter().map(|w| format!("{}: {w}", &cid_hex[..8])),
5356 );
5357 }
5358 }
5359 if !joined.is_empty() {
5360 if let Some(client) = crate::state::nostr_client() {
5361 community::v2::realtime::refresh_subscription(&client).await;
5362 }
5363 }
5364 }
5365 }
5366
5367 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
5368 for id in ids {
5369 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
5370 if community::v2::realtime::follow_worker_running() {
5373 community::v2::realtime::enqueue_follow(&id);
5374 } else {
5375 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
5376 all_warnings.extend(
5377 Self::v2_inline_follow(&id).await.into_iter().map(|w| format!("{}: {w}", &cid_hex[..8])),
5378 );
5379 }
5380 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
5387 for ch in &c.channels {
5388 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
5389 let _ = Self::v2_backfill_channel(
5390 &id, &hex, 50, 2, None, None,
5391 crate::community::transport::Evidence::Fast, 12,
5392 ).await;
5393 }
5394 }
5395 continue;
5396 }
5397 if let Ok(Some(community)) = db::community::load_community(&id) {
5398 for ch in &community.channels {
5399 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
5400 }
5401 }
5402 }
5403 Ok(all_warnings)
5404 }
5405
5406
5407 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
5439 self.listen_with(handler, CatchUp::Inline).await
5440 }
5441
5442 pub async fn listen_with(&self, handler: Arc<dyn InboundEventHandler>, catch_up: CatchUp) -> Result<()> {
5444 use nostr_sdk::prelude::*;
5445
5446 let client = state::nostr_client()
5447 .ok_or(VectorError::Other("Not connected".into()))?;
5448 let my_pk = state::my_public_key()
5449 .ok_or(VectorError::Other("Not logged in".into()))?;
5450
5451 community::v2::streamauth::ensure_responder(&client);
5458
5459 community::v2::realtime::spawn_follow_worker(handler.clone());
5462
5463 let listening_since = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0);
5470 let notifications = client.notifications();
5471 let dm_sub_id = self.subscribe_dms().await?;
5472 let (live_tx, mut live_rx) = tokio::sync::mpsc::channel::<(LiveRoute, Box<nostr_sdk::prelude::Event>)>(4096);
5473 let dm_sid = dm_sub_id.clone();
5474 db::spawn_bound(async move {
5475 let mut notifications = notifications;
5476 while let Some(notification) = notifications.next().await {
5477 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
5481 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
5482 sending::note_relay_ok(event_id, *status);
5483 }
5484 }
5485 let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification else { continue };
5486 let route = if subscription_id == dm_sid {
5487 LiveRoute::Dm
5488 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
5489 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
5490 {
5491 LiveRoute::CommunityV1
5493 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
5494 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
5495 {
5496 LiveRoute::CommunityV2
5497 } else {
5498 continue;
5499 };
5500 if live_tx.send((route, event)).await.is_err() {
5501 break;
5502 }
5503 }
5504 });
5505
5506 if catch_up == CatchUp::Inline {
5515 let _ = self.sync_communities().await;
5516 let _ = self.sync_dms(None, &event_handler::LiveOnly::from(&*handler, listening_since)).await;
5517 }
5518
5519 community::realtime::refresh_subscription(&client).await;
5522 community::v2::realtime::refresh_subscription(&client).await;
5523
5524 handler.on_subscription_ready(db::community::list_community_ids().map(|v| v.len()).unwrap_or(0));
5530
5531 if let Some(monitor) = client.monitor().filter(|_| catch_up == CatchUp::Inline) {
5538 let mut rx = monitor.subscribe();
5539 let handler = handler.clone();
5540 db::spawn_bound(async move {
5541 let mut last_resync: Option<web_time::Instant> = None;
5544 while let Ok(notification) = rx.recv().await {
5545 let MonitorNotification::StatusChanged { status, .. } = notification;
5546 if status == RelayStatus::Connected {
5547 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
5548 continue;
5549 }
5550 let _ = VectorCore.sync_communities().await;
5551 let resync_from = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0);
5552 let _ = VectorCore.sync_dms(None, &event_handler::LiveOnly::from(&*handler, resync_from)).await;
5553 if let Some(c) = state::nostr_client() {
5554 community::realtime::refresh_subscription(&c).await;
5555 community::v2::realtime::refresh_subscription(&c).await;
5556 }
5557 last_resync = Some(web_time::Instant::now());
5558 }
5559 }
5560 });
5561 }
5562
5563 {
5567 let client_health = client.clone();
5568 db::spawn_bound(async move {
5569 crate::rt::time::sleep(std::time::Duration::from_secs(30)).await; loop {
5571 for (url, relay) in client_health.relays().await {
5572 match relay.status() {
5573 RelayStatus::Connected => {
5574 let probe = crate::rt::time::timeout(
5575 std::time::Duration::from_secs(10),
5576 client_health
5577 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
5578 url.to_string(),
5579 [Filter::new().kind(Kind::Metadata).limit(1)],
5580 ))
5581 .timeout(std::time::Duration::from_secs(8)),
5582 )
5583 .await;
5584 if !matches!(probe, Ok(Ok(_))) {
5585 relay.disconnect();
5586 crate::rt::time::sleep(std::time::Duration::from_millis(500)).await;
5587 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
5588 }
5589 }
5590 RelayStatus::Terminated | RelayStatus::Disconnected => {
5591 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
5592 }
5593 _ => {}
5594 }
5595 }
5596 crate::rt::time::sleep(std::time::Duration::from_secs(60)).await;
5597 }
5598 });
5599 }
5600
5601 let client_for_closure = client.clone();
5602
5603 while let Some((route, event)) = live_rx.recv().await {
5606 match route {
5607 LiveRoute::Dm => {
5609 let prepared = event_handler::prepare_event(*event, &client_for_closure, my_pk).await;
5610 event_handler::commit_prepared_event(prepared, true, &*handler).await;
5611 }
5612 LiveRoute::CommunityV1 => community::realtime::dispatch_event(*event, handler.clone()).await,
5614 LiveRoute::CommunityV2 => community::v2::realtime::dispatch_event(*event, handler.clone()).await,
5616 }
5617 }
5618
5619 Ok(())
5620 }
5621
5622 pub async fn logout(&self) {
5624 if let Some(client) = state::nostr_client() {
5625 let _ = client.disconnect().await;
5626 }
5627 db::close_database();
5628 }
5629
5630 pub async fn swap_session(&self) {
5638 state::clear_message_tombstones();
5640
5641 #[cfg(feature = "xdc")]
5644 crate::xdc::session::leave_all(std::time::Duration::from_secs(4)).await;
5645
5646 if let Some(client) = state::take_nostr_client() {
5649 let _ = client.shutdown().await;
5650 }
5651 db::close_database();
5652
5653 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
5655 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
5656 {
5657 use zeroize::Zeroize;
5658 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
5659 if let Some(s) = g.as_mut() { s.zeroize(); }
5660 *g = None;
5661 }
5662 if let Ok(mut g) = state::PENDING_NSEC.lock() {
5663 if let Some(s) = g.as_mut() { s.zeroize(); }
5664 *g = None;
5665 }
5666 }
5667
5668 crate::community::realtime::clear().await;
5676 crate::community::v2::realtime::clear().await;
5677 crate::community::transport::clear_plane_pool();
5680 }
5681}
5682
5683#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
5684mod transport_policy_tests {
5685 use std::time::Duration;
5686
5687 #[test]
5690 fn tor_transport_policy() {
5691 let short = Duration::from_secs(5);
5692 let long = Duration::from_secs(300);
5693
5694 crate::tor::set_tor_enabled_pref(false);
5697 assert_eq!(super::tor_proxy_target(), None);
5698 assert_eq!(super::relay_connect_timeout(short), short);
5699 assert_eq!(super::relay_request_timeout(short), short);
5700
5701 crate::tor::set_tor_enabled_pref(true);
5705 assert!(matches!(
5706 crate::tor::transport_state(),
5707 crate::tor::TorTransportState::RequiredButInactive
5708 ));
5709 assert_eq!(
5715 super::tor_proxy_target(),
5716 Some(crate::tor::blackhole_proxy_addr()),
5717 "Tor enabled but inactive must blackhole, never connect direct"
5718 );
5719 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
5720 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
5721
5722 for tor in [true, false] {
5725 crate::tor::set_tor_enabled_pref(tor);
5726 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
5727 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
5728 }
5729 }
5730}
5731
5732#[cfg(test)]
5733mod facade_tests {
5734 use super::*;
5735
5736 #[tokio::test]
5739 async fn download_attachment_rejects_private_url() {
5740 let att = crate::types::Attachment {
5741 url: "http://169.254.169.254/latest/meta-data/".to_string(),
5742 ..Default::default()
5743 };
5744 match VectorCore.download_attachment(&att).await {
5745 Err(VectorError::Other(msg)) => {
5746 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
5747 }
5748 other => panic!("expected SSRF rejection, got {other:?}"),
5749 }
5750 }
5751
5752 #[tokio::test]
5753 async fn download_attachment_rejects_empty_url() {
5754 let att = crate::types::Attachment::default();
5755 assert!(VectorCore.download_attachment(&att).await.is_err());
5756 }
5757
5758 #[tokio::test]
5762 async fn list_communities_and_channel_routing_are_protocol_aware() {
5763 use crate::community::transport::memory::MemoryRelay;
5764 use nostr_sdk::prelude::Keys;
5765
5766 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
5767 crate::db::close_database();
5768 crate::db::clear_id_caches();
5769 let tmp = tempfile::tempdir().unwrap();
5770 let acct = {
5772 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
5773 let mut s = String::from("npub1");
5774 for i in 0..58 {
5775 s.push(B[(i * 7 + 3) % 32] as char);
5776 }
5777 s
5778 };
5779 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
5780 crate::db::set_app_data_dir(crate::db::shared_test_data_dir().to_path_buf());
5781 crate::db::set_current_account(acct.clone()).unwrap();
5782 crate::db::init_database(&acct).unwrap();
5783 let _ = crate::state::take_nostr_client();
5784 let me = Keys::generate();
5785 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
5786 crate::state::set_my_public_key(me.public_key());
5787
5788 let relay = MemoryRelay::new();
5790 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
5791 .await
5792 .unwrap();
5793 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
5794
5795 let listed = VectorCore.list_communities().await;
5797 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
5798 assert_eq!(v2["name"], "V2 Guild");
5799 assert_eq!(v2["is_owner"], true);
5800 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
5801
5802 assert_eq!(
5804 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
5805 Some(community.identity.community_id),
5806 "a v2 channel is routed to v2"
5807 );
5808 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
5810 }
5811
5812 #[test]
5817 fn v2_invite_url_base_derivation_round_trips() {
5818 use crate::community::v2::derive::TOKEN_LEN;
5819 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
5820 use nostr_sdk::prelude::Keys;
5821 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
5822 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
5823 let signer = Keys::generate();
5824 let token = [0x07u8; TOKEN_LEN];
5825 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
5826 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
5827 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
5828 let parsed = parse_invite_link(&url).unwrap();
5829 assert_eq!(parsed.link_signer, signer.public_key());
5830 assert_eq!(parsed.token, token);
5831 }
5832}
5833
5834#[cfg(test)]
5835mod history_paging_tests {
5836 use super::*;
5837
5838 fn msg(at: u64, id_byte: u8, content: &str) -> Message {
5839 Message {
5840 id: format!("{:02x}", id_byte).repeat(32),
5841 content: content.to_string(),
5842 at,
5843 ..Default::default()
5844 }
5845 }
5846
5847 #[tokio::test]
5851 async fn history_pages_through_a_same_ms_wall_and_a_deleted_cursor() {
5852 let chat_id = "test-history-paging-wall";
5853 {
5854 let mut st = state::STATE.lock().await;
5855 st.ensure_community_chat(chat_id);
5856 for m in [msg(500, 0x01, "old"), msg(900, 0xaa, "wall-a"), msg(900, 0xbb, "wall-b"), msg(900, 0xcc, "wall-c")] {
5858 st.add_message_to_chat(chat_id, &m);
5859 }
5860 }
5861 let core = VectorCore;
5862
5863 let newest = core.get_messages_before(chat_id, None, 2).await;
5865 assert_eq!(newest.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["wall-b", "wall-c"]);
5866
5867 let cursor = (newest[0].at, newest[0].id.as_str().to_string());
5869 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
5870 assert_eq!(page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["old", "wall-a"]);
5871
5872 {
5875 let mut st = state::STATE.lock().await;
5876 let chat = st.get_chat_mut(chat_id).unwrap();
5877 chat.messages.remove_by_hex_id(&cursor.1);
5878 }
5879 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
5880 assert_eq!(
5881 page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(),
5882 ["old", "wall-a"],
5883 "a deleted cursor pages identically"
5884 );
5885
5886 assert!(core.get_messages_before("test-history-paging-nochat", None, 5).await.is_empty());
5888 }
5889}
5890
5891#[cfg(test)]
5892mod moderation_cache_audit {
5893 #[test]
5899 fn every_moderation_action_drops_the_memoised_verdict() {
5900 let src = include_str!("lib.rs");
5901 for name in [
5902 "pub async fn refound_community",
5903 "pub async fn kick_community_members",
5904 "pub async fn set_members_banned",
5905 "pub async fn contain_raid",
5906 ] {
5907 let start = src.find(name).unwrap_or_else(|| panic!("{name} not found — rename?"));
5908 let rest = &src[start + name.len()..];
5910 let end = rest.find("\n pub ").unwrap_or(rest.len());
5911 assert!(
5912 rest[..end].contains("invalidate_raid_report"),
5913 "{name} mutates membership but never drops the memoised console verdict"
5914 );
5915 }
5916 }
5917
5918 #[test]
5923 fn an_adopted_change_drops_the_verdict_too() {
5924 let src = include_str!("db/community.rs");
5925 for name in ["pub fn set_community_banlist", "pub fn set_community_roles", "pub fn set_guestbook"] {
5926 let start = src.find(name).unwrap_or_else(|| panic!("{name} not found — rename?"));
5927 let rest = &src[start + name.len()..];
5928 let end = rest.find("\npub fn ").unwrap_or(rest.len());
5929 assert!(
5930 rest[..end].contains("forget_console_verdict"),
5931 "{name} changes who the console reports on but leaves the memo standing"
5932 );
5933 }
5934 }
5935}