1#[macro_use]
29mod macros;
30
31pub mod logging;
33pub mod error;
34pub mod traits;
35
36use crate::event_ext::FinalizeUnsignedWithId;
38use nostr_sdk::prelude::{FinalizeEventAsync, ToBech32};
39
40pub mod event_ext;
42pub mod tags;
43pub mod types;
44pub mod profile;
45pub mod chat;
46pub mod compact;
47
48pub mod state;
50
51#[cfg(debug_assertions)]
53pub mod stats;
54
55pub mod crypto;
57
58pub mod signer;
60
61pub mod nip55;
63
64pub mod db;
66pub mod spawn_audit;
69
70pub mod net;
72pub mod negentropy;
73pub mod blossom;
74pub mod blossom_servers;
75pub mod blossom_capabilities;
76pub mod inbox_relays;
77pub mod emoji_packs;
78pub mod emoji_usage;
79pub mod badges;
80pub mod bot_interface;
81pub mod webxdc;
82#[cfg(feature = "tor")]
83pub mod tor;
84
85#[derive(Debug)]
97pub struct VectorAuthenticator;
98
99impl nostr_sdk::prelude::Authenticator for VectorAuthenticator {
100 fn make_auth_event<'a>(
101 &'a self,
102 relay_url: &'a nostr_sdk::prelude::RelayUrl,
103 challenge: &'a str,
104 ) -> signer::BoxedFuture<'a, std::result::Result<nostr_sdk::prelude::Event, nostr_sdk::prelude::Error>>
105 {
106 Box::pin(async move {
107 let signer =
108 signer::active_signer().map_err(nostr_sdk::prelude::Error::other)?;
109 Ok(
110 nostr_sdk::prelude::ClientAuthentication::new(challenge, relay_url.clone())
111 .finalize_async(&signer)
112 .await?,
113 )
114 })
115 }
116}
117
118pub fn nostr_client_builder() -> nostr_sdk::prelude::ClientBuilder {
129 apply_tor_proxy(
130 nostr_sdk::prelude::ClientBuilder::new()
131 .authenticator(VectorAuthenticator)
132 .connect_timeout(relay_connect_timeout(std::time::Duration::from_secs(15))),
135 )
136}
137
138pub trait ClientRelayExt {
150 fn add_managed_relay<'client, 'url, U>(
152 &'client self,
153 url: U,
154 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
155 where
156 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>;
157}
158
159impl ClientRelayExt for nostr_sdk::prelude::Client {
160 fn add_managed_relay<'client, 'url, U>(
161 &'client self,
162 url: U,
163 ) -> nostr_sdk::prelude::AddRelay<'client, 'url>
164 where
165 U: Into<nostr_sdk::prelude::RelayUrlArg<'url>>,
166 {
167 self.add_relay(url).reconnect(false)
168 }
169}
170
171pub async fn resubscribe_relay_after_reconnect(
186 client: &nostr_sdk::prelude::Client,
187 relay: &nostr_sdk::prelude::RelayUrl,
188) {
189 for (id, per_relay) in client.subscriptions().await {
190 let Some(filters) = per_relay.get(relay) else { continue };
191 if filters.is_empty() {
192 continue;
193 }
194 let _ = client
195 .subscribe(nostr_sdk::prelude::ReqTarget::single(relay.clone(), filters.clone()))
196 .with_id(id)
197 .await;
198 }
199}
200
201#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
206const TOR_RELAY_CONNECT_FLOOR: std::time::Duration = std::time::Duration::from_secs(60);
207
208pub fn relay_connect_timeout(clearnet: std::time::Duration) -> std::time::Duration {
217 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
218 {
219 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
220 return clearnet.max(TOR_RELAY_CONNECT_FLOOR);
221 }
222 }
223 clearnet
224}
225
226#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
228const TOR_RELAY_REQUEST_FLOOR: std::time::Duration = std::time::Duration::from_secs(30);
229
230pub fn relay_request_timeout(clearnet: std::time::Duration) -> std::time::Duration {
236 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
237 {
238 if !matches!(tor::transport_state(), tor::TorTransportState::Disabled) {
239 return clearnet.max(TOR_RELAY_REQUEST_FLOOR);
240 }
241 }
242 clearnet
243}
244
245pub fn apply_tor_proxy(
252 builder: nostr_sdk::prelude::ClientBuilder,
253) -> nostr_sdk::prelude::ClientBuilder {
254 #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
255 let builder = builder.proxy(nostr_sdk::prelude::Proxy::custom(|_url| tor_proxy_target()));
256 builder
257}
258
259#[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
265fn tor_proxy_target() -> Option<std::net::SocketAddr> {
266 match tor::transport_state() {
267 tor::TorTransportState::Active(addr) => Some(addr),
268 tor::TorTransportState::RequiredButInactive => Some(tor::blackhole_proxy_addr()),
271 tor::TorTransportState::Disabled => None,
272 }
273}
274
275pub async fn sign_builder(
280 builder: nostr_sdk::prelude::EventBuilder,
281) -> std::result::Result<nostr_sdk::prelude::Event, String> {
282 let signer = signer::active_signer()?;
283 builder
284 .finalize_async(&signer)
285 .await
286 .map_err(|e| e.to_string())
287}
288
289pub async fn sign_and_send(
293 client: &nostr_sdk::prelude::Client,
294 builder: nostr_sdk::prelude::EventBuilder,
295) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String> {
296 let event = sign_builder(builder).await?;
297 client
298 .send_event(&event)
299 .await
300 .map_err(|e| e.to_string())
301}
302
303pub async fn send_gift_wrap<'u, I, U, T>(
309 client: &nostr_sdk::prelude::Client,
310 relays: I,
311 receiver: &nostr_sdk::prelude::PublicKey,
312 rumor: nostr_sdk::prelude::UnsignedEvent,
313 extra_tags: T,
314) -> std::result::Result<nostr_sdk::prelude::SendEventOutput, String>
315where
316 I: IntoIterator<Item = U>,
317 U: Into<nostr_sdk::prelude::RelayUrlArg<'u>>,
318 T: IntoIterator<Item = nostr_sdk::prelude::Tag>,
319{
320 let signer = signer::active_signer()?;
321 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(*receiver, rumor)
322 .extra_tags(extra_tags)
323 .finalize_async(&signer)
324 .await
325 .map_err(|e| e.to_string())?;
326 let targets: Vec<nostr_sdk::prelude::RelayUrlArg<'u>> =
327 relays.into_iter().map(Into::into).collect();
328 if targets.is_empty() {
329 client.send_event(&wrap).await.map_err(|e| e.to_string())
330 } else {
331 client
332 .send_event(&wrap)
333 .to(targets)
334 .await
335 .map_err(|e| e.to_string())
336 }
337}
338
339pub fn community_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
352 nostr_sdk::prelude::RelayCapabilities::GOSSIP
353}
354
355pub fn discovery_relay_capabilities() -> nostr_sdk::prelude::RelayCapabilities {
361 community_relay_capabilities()
362}
363
364pub mod stored_event;
366
367pub mod rumor;
369
370pub mod sending;
372
373pub mod pinned_chats;
375pub mod synced_prefs;
376pub mod wallpaper;
377
378pub mod deletion;
380pub mod self_destruct;
381
382pub mod simd;
384
385pub mod community;
387
388pub mod event_handler;
390
391pub use types::{Message, Attachment, Reaction, EditEntry, ImageMetadata, SiteMetadata, LoginResult, AttachmentFile, mention, extract_mentions};
393pub use profile::{Profile, ProfileFlags, SlimProfile, Status};
394pub use chat::{Chat, ChatType, ChatMetadata, SerializableChat};
395pub use compact::{CompactMessage, CompactMessageVec, NpubInterner};
396pub use state::{
397 ChatState, MY_SECRET_KEY, STATE, ENCRYPTION_KEY,
398 nostr_client, my_public_key, has_active_session,
399 set_nostr_client, set_my_public_key,
400 take_nostr_client, clear_my_public_key,
401 set_pending_bunker_setup, pending_bunker_setup, clear_pending_bunker_setup,
402 set_pending_nip55_setup, pending_nip55_setup, clear_pending_nip55_setup,
403};
404pub use crypto::{GuardedKey, GuardedSigner};
405pub use signer::{
406 SignerKind, signer_kind, set_signer_kind, is_bunker, is_keyless,
407 BUNKER_SIGNER, bunker_signer, set_bunker_signer, take_bunker_signer,
408 build_bunker_signer, prewarm_bunker, drain_bunker_state,
409 parse_bunker_remote_pubkey, parse_bunker_relays,
410 BunkerConnectionState, bunker_state, set_bunker_state,
411 VectorAuthUrlHandler, attempt_bunker_login, WatchedBunkerSigner,
412 vector_metadata, build_nostrconnect_uri, build_nostrconnect_session,
413 VECTOR_APP_NAME, VECTOR_APP_URL, VECTOR_APP_ICON,
414};
415pub use nip55::{
416 Nip55Backend, Nip55Error, Nip55ResolverOutcome, Nip55Signer, Nip55State,
417 set_nip55_backend, nip55_backend, nip55_state, set_nip55_state, drain_nip55_state,
418 nip55_is_installed, nip55_pair, nip55_perms_json,
419 VECTOR_NIP55_SIGN_KINDS, VECTOR_NIP55_ENCRYPT_TYPES,
420};
421pub use error::{VectorError, Result};
422pub use traits::{EventEmitter, NoOpEmitter, set_event_emitter, emit_event};
423pub use db::{set_app_data_dir, get_app_data_dir};
424pub use sending::{SendCallback, NoOpSendCallback, SendConfig, SendResult};
425pub use deletion::{delete_own_dm, DeleteOutcome};
426pub use stored_event::{StoredEvent, StoredEventBuilder, SystemEventType};
427pub use rumor::{RumorEvent, RumorContext, ConversationType, RumorProcessingResult, process_rumor};
428pub use profile::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
429pub use event_handler::{InboundEventHandler, NoOpEventHandler, PreparedEvent, process_event};
430
431use std::path::PathBuf;
432use std::sync::Arc;
433
434pub struct CoreConfig {
440 pub data_dir: PathBuf,
442 pub event_emitter: Option<Box<dyn EventEmitter>>,
444}
445
446#[derive(Clone, Copy)]
468pub struct VectorCore;
469
470#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
477pub struct BackfillCount {
478 pub fetched: usize,
479 pub new_messages: usize,
480}
481
482impl VectorCore {
483 pub fn init(config: CoreConfig) -> Result<Self> {
485 db::set_app_data_dir(config.data_dir);
487
488 if let Some(emitter) = config.event_emitter {
490 traits::set_event_emitter(emitter);
491 }
492
493 let _ = rustls::crypto::ring::default_provider().install_default();
495
496 Ok(VectorCore)
497 }
498
499 pub fn accounts(&self) -> Result<Vec<String>> {
501 db::get_accounts().map_err(VectorError::from)
502 }
503
504 pub async fn login(&self, key: &str, password: Option<&str>) -> Result<LoginResult> {
506 use nostr_sdk::prelude::*;
507
508 let keys = if key.starts_with("nsec1") {
510 let secret = SecretKey::from_bech32(key)
511 .map_err(|e| VectorError::Nostr(format!("Invalid nsec: {}", e)))?;
512 Keys::new(secret)
513 } else {
514 Keys::from_mnemonic(key, None)
516 .map_err(|e| VectorError::Nostr(format!("Key derivation failed: {}", e)))?
517 };
518
519 let public_key = keys.public_key();
520 let npub = public_key.to_bech32()
521 .map_err(|e| VectorError::Nostr(format!("Failed to encode npub: {}", e)))?;
522
523 let secret_bytes = keys.secret_key().to_secret_bytes();
525 state::MY_SECRET_KEY.set(secret_bytes, &[&state::ENCRYPTION_KEY]);
526 state::set_my_public_key(public_key);
527
528 db::set_current_account(npub.clone())?;
530 db::init_database(&npub)?;
531
532 {
534 let nsec = keys.secret_key().to_bech32()
535 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))?;
536 *state::PENDING_NSEC.lock().unwrap() = Some(nsec.clone());
537
538 let existing_encrypted = db::get_pkey().ok().flatten().is_some_and(|v| !v.starts_with("nsec1"));
545 if !(state::resolve_encryption_enabled_from_db() && existing_encrypted) {
546 db::set_pkey(&nsec)?;
547 }
548 }
549
550 let has_encryption = state::resolve_encryption_enabled_from_db();
553
554 if has_encryption {
555 if let Some(pwd) = password {
556 let key = crate::crypto::hash_pass(pwd).await;
557 state::ENCRYPTION_KEY.set(key, &[&state::MY_SECRET_KEY]);
558 }
559 }
560 state::init_encryption_enabled();
563
564 let client = crate::nostr_client_builder()
567 .monitor(Monitor::new(1024))
569 .build();
570
571 for relay in state::TRUSTED_RELAYS {
573 client.add_managed_relay(*relay).await.ok();
574 }
575
576 client.connect().await;
578
579 let _ = { state::set_nostr_client(client); Ok::<(), ()>(()) };
580
581 Ok(LoginResult { npub, has_encryption })
582 }
583
584 pub fn generate_nsec(&self) -> Result<String> {
587 use nostr_sdk::prelude::*;
588 Keys::generate().secret_key().to_bech32()
589 .map_err(|e| VectorError::Nostr(format!("Failed to encode nsec: {}", e)))
590 }
591
592 pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<sending::SendResult> {
597 let config = SendConfig { self_send: false, ..SendConfig::headless() };
598 sending::send_dm(to_npub, content, None, &config, Arc::new(NoOpSendCallback)).await
599 .map_err(|e| VectorError::Other(e))
600 }
601
602 pub async fn send_dm_reply(&self, to_npub: &str, replied_to: &str, content: &str) -> Result<sending::SendResult> {
604 let config = SendConfig { self_send: false, ..SendConfig::headless() };
605 sending::send_dm(to_npub, content, Some(replied_to), &config, Arc::new(NoOpSendCallback)).await
606 .map_err(|e| VectorError::Other(e))
607 }
608
609 pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
616 self.download_attachment_from(attachment, None).await
617 }
618
619 pub async fn download_attachment_from(
624 &self,
625 attachment: &Attachment,
626 author_npub: Option<&str>,
627 ) -> Result<Vec<u8>> {
628 use futures_util::StreamExt;
629 const MAX_DOWNLOAD: usize = 256 * 1024 * 1024;
630 if attachment.url.is_empty() {
631 return Err(VectorError::Other("attachment has no URL".into()));
632 }
633 let client = crate::net::build_http_client(std::time::Duration::from_secs(120)).map_err(VectorError::Other)?;
634 let mut last_err = String::from("download failed");
635 let mut candidates: Vec<String> = vec![attachment.url.clone()];
636 candidates.extend(attachment.fallback_urls.iter().cloned());
637 let mut hash_swap_tried = false;
638 let mut i = 0;
639 'sources: while i < candidates.len() {
640 let url = candidates[i].clone();
641 i += 1;
642 let extend_with_swap = |candidates: &mut Vec<String>, servers: &[String]| {
645 let extra = crate::blossom::hash_swap_candidates(&attachment.url, servers);
646 for c in extra {
647 if !candidates.contains(&c) {
648 candidates.push(c);
649 }
650 }
651 };
652 macro_rules! next_source {
653 () => {{
654 log_net_fail!("[Download] source failed ({}): {}", url, last_err);
655 if i == candidates.len() && !hash_swap_tried {
656 hash_swap_tried = true;
657 let servers = crate::blossom_servers::author_swap_servers(author_npub, false).await;
658 extend_with_swap(&mut candidates, &servers);
659 }
660 continue 'sources;
661 }};
662 }
663 if let Err(e) = crate::net::validate_url_not_private(&url) {
667 last_err = e.to_string();
668 next_source!();
669 }
670 let resp = match client.get(&url).send().await {
671 Ok(r) => r,
672 Err(e) => {
673 last_err = format!("download: {e}");
674 next_source!();
675 }
676 };
677 if !resp.status().is_success() {
678 last_err = format!("download failed: HTTP {}", resp.status());
679 next_source!();
680 }
681 let mut encrypted: Vec<u8> = Vec::with_capacity(
684 resp.content_length().map(|l| (l as usize).min(MAX_DOWNLOAD)).unwrap_or(64 * 1024),
685 );
686 let mut stream = resp.bytes_stream();
687 while let Some(chunk) = stream.next().await {
688 let chunk = match chunk {
689 Ok(c) => c,
690 Err(e) => {
691 last_err = format!("read body: {e}");
692 next_source!();
693 }
694 };
695 if encrypted.len() + chunk.len() > MAX_DOWNLOAD {
696 return Err(VectorError::Other("attachment exceeds 256 MiB cap".into()));
697 }
698 encrypted.extend_from_slice(&chunk);
699 }
700 match crate::crypto::decrypt_data(&encrypted, &attachment.key, &attachment.nonce) {
701 Ok(plain) => {
702 if i > 1 {
703 log_net_info!("[Download] fallback source {}/{} served {}", i, candidates.len(), url);
704 }
705 return Ok(plain);
706 }
707 Err(e) => {
708 last_err = format!("decrypt: {e}");
711 next_source!();
712 }
713 }
714 }
715 log_net_fail!("[Download] all {} source(s) failed for {}: {}", candidates.len(), attachment.url, last_err);
716 Err(VectorError::Other(last_err))
717 }
718
719 pub async fn send_file(&self, to_npub: &str, file_path: &str) -> Result<sending::SendResult> {
721 let path = std::path::Path::new(file_path);
722 let bytes = std::fs::read(path)
723 .map_err(|e| VectorError::Io(e))?;
724 let filename = path.file_name()
725 .and_then(|n| n.to_str())
726 .unwrap_or("file");
727 let extension = path.extension()
728 .and_then(|e| e.to_str())
729 .unwrap_or("bin");
730
731 sending::send_file_dm(
732 to_npub,
733 std::sync::Arc::new(bytes),
734 filename,
735 extension,
736 None,
737 &SendConfig::default(),
738 Arc::new(NoOpSendCallback),
739 ).await.map_err(|e| VectorError::Other(e))
740 }
741
742 async fn own_reaction_id(message_id: &str, emoji: &str) -> Option<String> {
747 use nostr_sdk::prelude::ToBech32;
748 let me = state::my_public_key()?.to_bech32().ok()?;
749 let st = state::STATE.lock().await;
750 let (_, message) = st.find_message(message_id)?;
751 message
752 .reactions
753 .iter()
754 .find(|r| r.author_id == me && r.emoji == emoji)
755 .map(|r| r.id.clone())
756 }
757
758 pub async fn send_reaction(
763 &self,
764 to_npub: &str,
765 reference_id: &str,
766 emoji: &str,
767 emoji_url: Option<&str>,
768 ) -> Result<String> {
769 use nostr_sdk::prelude::*;
770
771 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
772 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
773
774 if let Some(existing) = Self::own_reaction_id(reference_id, emoji).await {
776 return Ok(existing);
777 }
778
779 badges::check_new_reaction_allowance(reference_id, emoji)
781 .await
782 .map_err(VectorError::Other)?;
783
784 let reference_event = EventId::from_hex(reference_id)
785 .map_err(|e| VectorError::Nostr(e.to_string()))?;
786 let receiver_pubkey = PublicKey::from_bech32(to_npub)
787 .map_err(|e| VectorError::Nostr(e.to_string()))?;
788
789 let custom_emoji_tag = emoji_url.and_then(|url| {
791 if !emoji.starts_with(':') || !emoji.ends_with(':') || emoji.len() < 3 || url.is_empty() {
792 return None;
793 }
794 let shortcode = &emoji[1..emoji.len() - 1];
795 if shortcode.is_empty() { return None; }
796 Some(Tag::custom("emoji", [shortcode.to_string(), url.to_string()]))
797 });
798
799 let reaction_target = nostr_sdk::prelude::nip25::ReactionTarget {
800 event_id: reference_event,
801 public_key: receiver_pubkey,
802 coordinate: None,
803 kind: Some(Kind::PrivateDirectMessage),
804 relay_hint: None,
805 };
806 let mut builder =
807 nostr_sdk::prelude::nip25::ReactionBuilder::new(reaction_target, emoji)
808 .into_event_builder();
809 if let Some(tag) = custom_emoji_tag {
810 builder = builder.tag(tag);
811 }
812 let rumor = builder.finalize_unsigned_with_id(my_public_key);
813 let inner_rumor_id = rumor.id;
814 let rumor_id = inner_rumor_id.ok_or(VectorError::Other("Failed to get rumor ID".into()))?.to_hex();
815
816 let outcome = inbox_relays::send_gift_wrap_retained(&client, &receiver_pubkey, rumor.clone(), [])
820 .await.map_err(VectorError::Other)?;
821 if !outcome.output.success.is_empty() {
822 if let Some(rid) = inner_rumor_id {
823 if let Err(e) = db::nip17_keys::store_wrap_key(
824 &outcome.wrap_event_id, &rid, &receiver_pubkey,
825 db::nip17_keys::WrapRole::Recipient,
826 &outcome.wrap_secret, &outcome.targeted_relays,
827 ) {
828 crate::log_warn!("[Reaction] failed to persist wrap key: {}", e);
829 }
830 }
831 }
832
833 let self_wrap_client = client.clone();
836 db::spawn_bound(async move {
837 if let Ok(self_outcome) = inbox_relays::send_gift_wrap_retained(
838 &self_wrap_client, &my_public_key, rumor, [],
839 ).await {
840 if !self_outcome.output.success.is_empty() {
841 if let Some(rid) = inner_rumor_id {
842 let _ = db::nip17_keys::store_wrap_key(
843 &self_outcome.wrap_event_id, &rid, &my_public_key,
844 db::nip17_keys::WrapRole::SelfSend,
845 &self_outcome.wrap_secret, &self_outcome.targeted_relays,
846 );
847 }
848 }
849 }
850 });
851
852 let reaction = Reaction {
854 id: rumor_id.clone(),
855 reference_id: reference_id.to_string(),
856 author_id: my_public_key.to_bech32().unwrap_or_else(|_| my_public_key.to_hex()),
857 emoji: emoji.to_string(),
858 emoji_url: emoji_url.map(|s| s.to_string()),
859 };
860 let msg_for_save = {
861 let mut st = state::STATE.lock().await;
862 match st.add_reaction_to_message(reference_id, reaction) {
863 Some((cid, true)) => st.find_message(reference_id).map(|(_, m)| (cid, m)),
864 _ => None,
865 }
866 };
867 if let Some((cid, mut msg)) = msg_for_save {
868 let _ = db::events::save_message(&cid, &msg).await;
869 traits::emit_message_update(&cid, reference_id, &mut msg).await;
870 }
871
872 Ok(rumor_id)
873 }
874
875 pub async fn send_typing(&self, to_npub: &str) -> Result<()> {
878 use nostr_sdk::prelude::*;
879
880 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
881 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
882 let pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
883
884 let expiry = Timestamp::from_secs(Timestamp::now().as_secs() + 30);
885 let rumor = EventBuilder::new(Kind::ApplicationSpecificData, "typing")
886 .tag(Tag::public_key(pubkey))
887 .tag(Tag::custom("d", vec!["vector"]))
888 .tag(Tag::expiration(expiry))
889 .finalize_unsigned_with_id(my_public_key);
890
891 let signer = signer::active_signer().map_err(VectorError::Other)?;
893 let wrap = nostr_sdk::prelude::GiftWrapBuilder::new(pubkey, rumor.clone())
894 .extra_tags([Tag::expiration(expiry)])
895 .finalize_async(&signer)
896 .await
897 .map_err(|e| VectorError::Nostr(e.to_string()))?;
898 client
899 .send_event(&wrap)
900 .to(state::active_trusted_relays().await)
901 .await
902 .map_err(|e| VectorError::Nostr(e.to_string()))?;
903 Ok(())
904 }
905
906 pub async fn edit_dm(&self, to_npub: &str, message_id: &str, new_content: &str) -> Result<String> {
910 crate::db::scoped(async move {
911 use nostr_sdk::prelude::*;
912
913 let client = state::nostr_client().ok_or(VectorError::Other("Not connected".into()))?;
914 let my_public_key = state::my_public_key().ok_or(VectorError::Other("Not logged in".into()))?;
915 let my_npub = my_public_key.to_bech32().map_err(|e| VectorError::Nostr(e.to_string()))?;
916 let receiver_pubkey = PublicKey::from_bech32(to_npub).map_err(|e| VectorError::Nostr(e.to_string()))?;
917 let reference_event = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
918
919 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
921
922 let mut builder = EventBuilder::new(
923 Kind::from_u16(stored_event::event_kind::MESSAGE_EDIT),
924 new_content,
925 ).tag(Tag::event(reference_event));
926 for et in &emoji_tags {
927 builder = builder.tag(Tag::custom(
928 "emoji",
929 [et.shortcode.clone(), et.url.clone()],
930 ));
931 }
932 let rumor = builder.finalize_unsigned_with_id(my_public_key);
933 let edit_id = rumor.id.ok_or(VectorError::Other("Failed to get edit rumor ID".into()))?.to_hex();
934 let edit_ts_ms = rumor.created_at.as_secs() * 1000;
935
936 let msg_for_emit = {
938 let mut st = state::STATE.lock().await;
939 st.update_message_in_chat(to_npub, message_id, |msg| {
940 msg.apply_edit(new_content.to_string(), edit_ts_ms, emoji_tags.clone());
941 msg.preview_metadata = None;
942 })
943 };
944 if let Some(mut msg) = msg_for_emit {
945 traits::emit_message_update(to_npub, message_id, &mut msg).await;
946 if let Ok(db_chat_id) = db::id_cache::get_chat_id_by_identifier(to_npub) {
947 let _ = db::events::save_edit_event(
948 &edit_id, message_id, new_content, &emoji_tags, db_chat_id, None, &my_npub,
949 ).await;
950 }
951 }
952
953 inbox_relays::send_gift_wrap(&client, &receiver_pubkey, rumor.clone(), [])
954 .await.map_err(VectorError::Other)?;
955
956 let self_wrap_client = client.clone();
957 let self_wrap_session = crate::db::current_session();
958 db::spawn_bound(async move {
959 if !self_wrap_session.is_live() { return; }
960 let Ok(signer) = signer::active_signer() else { return };
961 if let Ok(wrap) = nostr_sdk::prelude::GiftWrapBuilder::new(my_public_key, rumor)
962 .finalize_async(&signer)
963 .await
964 {
965 let _ = self_wrap_client.send_event(&wrap).await;
966 }
967 });
968
969 Ok(edit_id)
970 })
971 .await
972 }
973
974 pub async fn delete_dm(&self, message_id: &str) -> Result<deletion::DeleteOutcome> {
976 use nostr_sdk::prelude::*;
977 let rumor_id = EventId::from_hex(message_id).map_err(|e| VectorError::Nostr(e.to_string()))?;
978 deletion::delete_own_dm(&rumor_id).await.map_err(VectorError::Other)
979 }
980
981 pub async fn get_chats(&self) -> Vec<SerializableChat> {
983 let state = state::STATE.lock().await;
984 state.chats.iter()
985 .map(|c| c.to_serializable_with_last_n(1, &state.interner))
986 .collect()
987 }
988
989 pub async fn get_messages(&self, chat_id: &str, limit: usize, offset: usize) -> Vec<Message> {
991 let state = state::STATE.lock().await;
992 if let Some(chat) = state.get_chat(chat_id) {
993 let msgs = chat.get_all_messages(&state.interner);
994 let start = offset.min(msgs.len());
995 let end = (offset + limit).min(msgs.len());
996 msgs[start..end].to_vec()
997 } else {
998 Vec::new()
999 }
1000 }
1001
1002 pub async fn get_message(&self, message_id: &str) -> Option<(String, Message)> {
1011 {
1012 let state = state::STATE.lock().await;
1013 if let Some((chat, msg)) = state.find_message(message_id) {
1014 return Some((chat.id.clone(), msg));
1015 }
1016 }
1017 db::events::get_message_by_id(message_id).await.ok().flatten()
1018 }
1019
1020 pub async fn get_messages_with_attachment(&self, content_hash: &str) -> Vec<(String, Message)> {
1029 let Ok(ids) = db::attachments::events_with_attachment_hash(content_hash) else { return Vec::new() };
1030 let mut out = Vec::with_capacity(ids.len());
1031 for id in ids {
1032 if let Some(found) = self.get_message(&id).await {
1033 out.push(found);
1034 }
1035 }
1036 out
1037 }
1038
1039 pub async fn get_messages_before(
1049 &self,
1050 chat_id: &str,
1051 before: Option<(u64, &str)>,
1052 limit: usize,
1053 ) -> Vec<Message> {
1054 let state = state::STATE.lock().await;
1055 let Some(chat) = state.get_chat(chat_id) else {
1056 return Vec::new();
1057 };
1058 let mut msgs = chat.get_all_messages(&state.interner);
1059 if let Some((at, id)) = before {
1060 msgs.retain(|m| (m.at, m.id.as_str()) < (at, id));
1061 }
1062 msgs.sort_by(|a, b| (a.at, a.id.as_str()).cmp(&(b.at, b.id.as_str())));
1063 if msgs.len() > limit {
1064 msgs.drain(..msgs.len() - limit);
1065 }
1066 msgs
1067 }
1068
1069 pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile> {
1071 let state = state::STATE.lock().await;
1072 state.get_profile(npub)
1073 .map(|p| SlimProfile::from_profile(p, &state.interner))
1074 }
1075
1076 pub async fn load_profile(&self, npub: &str) -> bool {
1078 profile::sync::load_profile(npub.to_string(), &NoOpProfileSyncHandler).await
1079 }
1080
1081 pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1083 profile::sync::update_profile(
1084 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1085 &NoOpProfileSyncHandler,
1086 ).await
1087 }
1088
1089 pub async fn update_bot_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
1092 profile::sync::update_bot_profile(
1093 name.to_string(), avatar.to_string(), banner.to_string(), about.to_string(),
1094 &NoOpProfileSyncHandler,
1095 ).await
1096 }
1097
1098 pub async fn update_status(&self, status: &str) -> bool {
1100 profile::sync::update_status(status.to_string()).await
1101 }
1102
1103 pub async fn upload_public_image(&self, file_path: &str) -> Result<String> {
1109 let path = std::path::Path::new(file_path);
1110 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
1111 if bytes.is_empty() {
1112 return Err(VectorError::Other("Empty image file".into()));
1113 }
1114 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
1115 let mime = crate::crypto::mime_from_extension(&extension);
1116 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
1117 let signer = crate::signer::active_signer()
1118 .map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
1119 let servers = crate::blossom_servers::compute_enabled_servers();
1120 if servers.is_empty() {
1121 return Err(VectorError::Other("No Blossom servers configured".into()));
1122 }
1123 crate::blossom::upload_blob_with_failover(
1126 signer,
1127 servers,
1128 std::sync::Arc::new(bytes),
1129 Some(mime),
1130 Some(std::time::Duration::from_secs(20)),
1131 )
1132 .await
1133 .map_err(VectorError::Other)
1134 }
1135
1136 pub async fn block_user(&self, npub: &str) -> bool {
1138 profile::sync::block_user(npub.to_string(), &NoOpProfileSyncHandler).await
1139 }
1140
1141 pub async fn unblock_user(&self, npub: &str) -> bool {
1143 profile::sync::unblock_user(npub.to_string(), &NoOpProfileSyncHandler).await
1144 }
1145
1146 pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
1148 profile::sync::set_nickname(npub.to_string(), nickname.to_string(), &NoOpProfileSyncHandler).await
1149 }
1150
1151 pub async fn get_blocked_users(&self) -> Vec<SlimProfile> {
1153 profile::sync::get_blocked_users().await
1154 }
1155
1156 pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority) {
1158 profile::sync::queue_profile_sync(npub.to_string(), priority, false);
1159 }
1160
1161 pub fn my_npub(&self) -> Option<String> {
1163 state::my_public_key()
1164 .and_then(|pk| ToBech32::to_bech32(&pk).ok())
1165 }
1166
1167 pub async fn list_communities(&self) -> Vec<serde_json::Value> {
1174 use crate::community::ConcordProtocol;
1175 let ids = crate::db::community::list_community_ids().unwrap_or_default();
1176 let mut out = Vec::new();
1177 for id in ids {
1178 match crate::db::community::community_protocol(&id).ok().flatten() {
1180 Some(ConcordProtocol::V2) => {
1181 if let Ok(Some(c)) = crate::db::community::load_community_v2(&id) {
1182 let me = state::my_public_key();
1183 let is_owner = me.is_some_and(|m| c.owner().is_ok_and(|o| o == m));
1184 out.push(serde_json::json!({
1185 "community_id": crate::simd::hex::bytes_to_hex_32(&c.identity.community_id.0),
1186 "version": 2,
1187 "name": c.name,
1188 "description": c.description,
1189 "is_owner": is_owner,
1190 "dissolved": c.dissolved,
1195 "channels": c.channels.iter()
1201 .map(|ch| serde_json::json!({
1202 "channel_id": crate::simd::hex::bytes_to_hex_32(&ch.id.0),
1203 "name": ch.name,
1204 "private": ch.private,
1205 "readable": !(ch.private && ch.key.is_none()),
1206 "epoch": ch.epoch.0,
1207 }))
1208 .collect::<Vec<_>>(),
1209 }));
1210 }
1211 }
1212 _ => {
1213 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
1214 out.push(serde_json::json!({
1215 "community_id": c.id.to_hex(),
1216 "version": 1,
1217 "name": c.name,
1218 "description": c.description,
1219 "is_owner": crate::community::service::is_proven_owner(&c),
1220 "dissolved": c.dissolved,
1221 "channels": c.channels.iter()
1222 .map(|ch| serde_json::json!({ "channel_id": ch.id.to_hex(), "name": ch.name }))
1223 .collect::<Vec<_>>(),
1224 }));
1225 }
1226 }
1227 }
1228 }
1229 out
1230 }
1231
1232 pub async fn create_community_v2(&self, name: &str) -> Result<serde_json::Value> {
1237 use crate::community::{v2::service as v2, transport::LiveTransport};
1238 let relays: Vec<String> = crate::state::active_trusted_relays()
1239 .await
1240 .iter()
1241 .map(|s| s.to_string())
1242 .collect();
1243 if relays.is_empty() {
1244 return Err(VectorError::Other("no relays available to host the Community".into()));
1245 }
1246 let session = crate::db::current_session();
1247 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1248 let community = v2::create_community(&transport, name, relays, None)
1249 .await
1250 .map_err(VectorError::Other)?;
1251 self.register_v2_chats(&community, &session).await;
1252 if let Some(client) = state::nostr_client() {
1254 crate::community::v2::realtime::refresh_subscription(&client).await;
1255 }
1256 Ok(Self::v2_summary(&community))
1257 }
1258
1259 fn v2_community_for_channel(&self, channel_id: &str) -> Result<Option<crate::community::CommunityId>> {
1265 use crate::community::ConcordProtocol;
1266 let Some(cid_hex) = crate::db::community::community_id_for_channel(channel_id).map_err(VectorError::Other)? else {
1267 return Ok(None);
1268 };
1269 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
1270 Ok(match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
1271 Some(ConcordProtocol::V2) => Some(cid),
1272 _ => None,
1273 })
1274 }
1275
1276 fn v2_summary(community: &crate::community::v2::community::CommunityV2) -> serde_json::Value {
1278 let me = state::my_public_key();
1279 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1280 serde_json::json!({
1281 "community_id": crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0),
1282 "version": 2,
1283 "name": community.name,
1284 "description": community.description,
1285 "is_owner": is_owner,
1286 "primary_channel": community.primary_channel().map(|c| crate::simd::hex::bytes_to_hex_32(&c.id.0)),
1291 "channels": community.channels.iter()
1292 .map(|c| serde_json::json!({ "channel_id": crate::simd::hex::bytes_to_hex_32(&c.id.0), "name": c.name, "private": c.private }))
1293 .collect::<Vec<_>>(),
1294 })
1295 }
1296
1297 pub async fn register_v2_chats(&self, community: &crate::community::v2::community::CommunityV2, __session: &std::sync::Arc<crate::db::Session>) {
1303 register_v2_chats_inner(community).await
1304 }
1305}
1306
1307pub(crate) async fn register_v2_chats_inner(community: &crate::community::v2::community::CommunityV2) {
1310 crate::db::scoped(async move {
1311 let owner_npub = community.owner().ok().and_then(|p| ToBech32::to_bech32(&p).ok());
1312 let me = state::my_public_key();
1313 let is_owner = me.is_some_and(|m| community.owner().is_ok_and(|o| o == m));
1314 let id_hex = crate::simd::hex::bytes_to_hex_32(&community.identity.community_id.0);
1315 let Some(primary) = community.primary_channel() else { return };
1318 let primary_hex = crate::simd::hex::bytes_to_hex_32(&primary.id.0);
1319 let slims = {
1324 let mut st = state::STATE.lock().await;
1325 let mut slims = Vec::new();
1326 for ch in &community.channels {
1327 let ch_hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
1328 st.upsert_community_chat(
1329 &ch_hex,
1330 &community.name,
1331 community.description.as_deref().unwrap_or(""),
1332 &id_hex,
1333 is_owner,
1334 community.icon.is_some(),
1335 owner_npub.as_deref(),
1336 Some(community.created_at_ms),
1337 community.dissolved,
1338 crate::community::ConcordProtocol::V2,
1339 &ch.name,
1340 &primary_hex,
1341 );
1342 if let Some(chat) = st.chats.iter().find(|c| c.id == ch_hex) {
1343 slims.push(crate::db::chats::SlimChatDB::from_chat(chat, &st.interner));
1344 }
1345 }
1346 slims
1347 };
1348 for slim in &slims {
1350 let _ = crate::db::chats::save_slim_chat(slim);
1351 }
1352 })
1353 .await
1354}
1355
1356const RAID_REPORT_TTL_SECS: u64 = 90;
1360
1361struct RaidReportCache;
1363
1364#[allow(clippy::type_complexity)]
1365struct PolicyReportCache;
1366
1367fn policy_report_cache(
1371) -> std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, (u64, std::sync::Arc<serde_json::Value>)>>> {
1372 crate::db::current_session().scoped::<PolicyReportCache, _>()
1373}
1374
1375fn raid_report_cache(
1376) -> std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, (u64, std::sync::Arc<crate::community::raid::RaidReport>)>>> {
1377 crate::db::current_session().scoped::<RaidReportCache, _>()
1378}
1379
1380impl VectorCore {
1381 pub async fn join_community(&self, invite_url: &str) -> Result<serde_json::Value> {
1385 use crate::community::{public_invite, service, transport::LiveTransport};
1386 if crate::community::v2::invite::parse_invite_link(invite_url).is_ok() {
1390 let session = crate::db::current_session();
1391 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1392 let community = crate::community::v2::service::accept_public_link(&transport, invite_url)
1393 .await
1394 .map_err(VectorError::Other)?;
1395 self.register_v2_chats(&community, &session).await;
1396 if let Some(client) = state::nostr_client() {
1397 crate::community::v2::realtime::refresh_subscription(&client).await;
1398 }
1399 if crate::community::v2::realtime::follow_worker_running() {
1404 crate::community::v2::realtime::enqueue_follow(community.id());
1405 } else {
1406 let seed_community = community.clone();
1407 db::spawn_bound(async move {
1408 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1409 if matches!(
1410 crate::community::v2::service::sync_guestbook(&transport, &seed_community).await,
1411 Ok(fresh) if !fresh.is_empty()
1412 ) {
1413 let cid_hex = crate::simd::hex::bytes_to_hex_32(&seed_community.id().0);
1414 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
1415 }
1416 });
1417 }
1418 return Ok(Self::v2_summary(&community));
1419 }
1420 let (relays, token) = public_invite::parse_invite_url(invite_url)
1421 .map_err(|e| VectorError::Other(e.to_string()))?;
1422 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1423 let bundle = service::fetch_public_invite(&transport, &relays, &token)
1424 .await
1425 .map_err(VectorError::Other)?;
1426 let now = std::time::SystemTime::now()
1427 .duration_since(std::time::UNIX_EPOCH)
1428 .map(|d| d.as_secs())
1429 .unwrap_or(0);
1430 let probe_view = crate::community::invite::accept_invite(&bundle.join).map_err(VectorError::Other)?;
1433 crate::community::migration::gate_fresh_v1_join(&transport, &probe_view, now)
1434 .await
1435 .map_err(VectorError::Other)?;
1436 let community = service::accept_public_invite(&bundle, now).map_err(VectorError::Other)?;
1437 let attribution = bundle.creator_npub.clone().map(|by| (by, bundle.label.clone()));
1440 self.finalize_member_join(community, &transport, attribution).await
1441 }
1442
1443 pub fn list_pending_invites(&self) -> Result<Vec<serde_json::Value>> {
1446 let rows = crate::db::community::list_pending_invites().map_err(VectorError::Other)?;
1447 Ok(rows.iter().map(|p| {
1448 if let Ok(v2) = crate::community::v2::invite::CommunityInvite::from_bundle_json(&p.bundle_json) {
1451 serde_json::json!({
1452 "community_id": p.community_id,
1453 "name": v2.name,
1454 "inviter_npub": p.inviter_npub,
1455 "version": 2,
1456 })
1457 } else {
1458 let name = crate::community::invite::CommunityInvite::from_json(&p.bundle_json)
1459 .ok().map(|i| i.name).unwrap_or_default();
1460 serde_json::json!({
1461 "community_id": p.community_id,
1462 "name": name,
1463 "inviter_npub": p.inviter_npub,
1464 "version": 1,
1465 })
1466 }
1467 }).collect())
1468 }
1469
1470 pub async fn accept_pending_invite(&self, community_id: &str) -> Result<serde_json::Value> {
1474 crate::db::scoped(async move {
1475 use crate::community::transport::LiveTransport;
1476 let bundle_json = crate::db::community::get_pending_invite(community_id)
1477 .map_err(VectorError::Other)?
1478 .ok_or_else(|| VectorError::Other(format!("no pending invite for {community_id}")))?;
1479 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1480
1481 if crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json).is_ok() {
1483 let session = crate::db::current_session();
1484 let inviter = crate::db::community::list_pending_invites()
1486 .ok()
1487 .and_then(|rows| rows.into_iter().find(|p| p.community_id == community_id).map(|p| p.inviter_npub));
1488 let community = match crate::community::v2::service::accept_parked_invite(&transport, &bundle_json, inviter.as_deref()).await {
1500 Ok(c) => c,
1501 Err(e) if e == crate::community::v2::service::ERR_DISSOLVED => {
1502 let relays = crate::community::v2::invite::CommunityInvite::from_bundle_json(&bundle_json)
1503 .map(|i| i.relays)
1504 .unwrap_or_default();
1505 crate::community::v2::service::retire_dead_invite(&transport, community_id, &relays).await;
1506 return Err(VectorError::Other("this community has been dissolved — the invite was removed".into()));
1507 }
1508 Err(e) => return Err(VectorError::Other(e)),
1509 };
1510 self.register_v2_chats(&community, &session).await;
1511 if let Some(client) = state::nostr_client() {
1512 crate::community::v2::realtime::refresh_subscription(&client).await;
1513 }
1514 crate::community::v2::realtime::enqueue_follow(community.id());
1515 let _ = crate::db::community::delete_pending_invite(community_id);
1516 return Ok(Self::v2_summary(&community));
1517 }
1518
1519 use crate::community::invite::{accept_invite, CommunityInvite};
1521 let invite = CommunityInvite::from_json(&bundle_json).map_err(VectorError::Other)?;
1522 let community = accept_invite(&invite).map_err(VectorError::Other)?;
1523 let now = std::time::SystemTime::now()
1527 .duration_since(std::time::UNIX_EPOCH)
1528 .map(|d| d.as_secs())
1529 .unwrap_or(0);
1530 crate::community::migration::gate_fresh_v1_join(&transport, &community, now)
1531 .await
1532 .map_err(VectorError::Other)?;
1533 let summary = self.finalize_member_join(community, &transport, None).await?;
1535 let _ = crate::db::community::delete_pending_invite(community_id);
1536 Ok(summary)
1537 })
1538 .await
1539 }
1540
1541 pub(crate) async fn finalize_member_join<T: crate::community::transport::Transport + ?Sized>(
1546 &self,
1547 community: crate::community::Community,
1548 transport: &T,
1549 attribution: Option<(String, Option<String>)>,
1550 ) -> Result<serde_json::Value> {
1551 use crate::community::service;
1552 if let Ok(Some(v2)) = crate::db::community::get_migrated_to(&community.id.to_hex()) {
1559 return Ok(serde_json::json!({
1560 "community_id": v2,
1561 "version": 2,
1562 "migrated": true,
1563 }));
1564 }
1565 crate::db::community::save_community(&community).map_err(VectorError::Other)?;
1569 if let Ok(c) = service::catch_up_server_root(transport, &community).await {
1572 if c.removed {
1573 let _ = crate::db::community::delete_community(&community.id.to_hex());
1574 return Err(VectorError::Other("you have been removed from this community".into()));
1575 }
1576 }
1577 let community = crate::db::community::load_community(&community.id)
1578 .map_err(VectorError::Other)?
1579 .unwrap_or(community);
1580 let _ = service::fetch_and_apply_control(transport, &community).await;
1584 if service::am_i_banned(&community) {
1585 let _ = crate::db::community::delete_community(&community.id.to_hex());
1586 return Err(VectorError::Other("you are banned from this community".into()));
1587 }
1588 let community = crate::db::community::load_community(&community.id)
1590 .map_err(VectorError::Other)?
1591 .unwrap_or(community);
1592 let owner_npub = community
1593 .owner_attestation
1594 .as_ref()
1595 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
1596 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
1597 {
1598 let created_at_ms = crate::db::community::community_created_at_ms(&community.id);
1599 let primary_hex = community.channels.first().map(|c| c.id.to_hex()).unwrap_or_default();
1600 let mut st = state::STATE.lock().await;
1601 for ch in &community.channels {
1602 st.upsert_community_chat(
1603 &ch.id.to_hex(),
1604 &community.name,
1605 community.description.as_deref().unwrap_or(""),
1606 &community.id.to_hex(),
1607 crate::community::service::is_proven_owner(&community),
1608 community.icon.is_some(),
1609 owner_npub.as_deref(),
1610 created_at_ms,
1611 community.dissolved,
1612 crate::community::ConcordProtocol::V1,
1613 &ch.name,
1614 &primary_hex,
1615 );
1616 }
1617 }
1618 if let Some(primary) = community.channels.first() {
1621 let _ = service::publish_presence(transport, &community, primary, true, attribution).await;
1622 }
1623 Ok(serde_json::json!({
1624 "community_id": community.id.to_hex(),
1625 "version": 1,
1626 "name": community.name,
1627 "channels": community.channels.iter()
1628 .map(|c| serde_json::json!({ "channel_id": c.id.to_hex(), "name": c.name }))
1629 .collect::<Vec<_>>(),
1630 }))
1631 }
1632
1633
1634 fn v2_community(community_id: &str) -> Result<crate::community::v2::community::CommunityV2> {
1638 use crate::community::CommunityId;
1639 if community_id.len() != 64 {
1640 return Err(VectorError::Other("malformed community id".into()));
1641 }
1642 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1643 match crate::db::community::community_protocol(&cid).ok().flatten() {
1644 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid)
1645 .map_err(VectorError::Other)?
1646 .ok_or_else(|| VectorError::Other("v2 community not found".into())),
1647 Some(_) => Err(VectorError::Other(
1648 "channel management is Concord v2 only — this community still uses the legacy protocol".into(),
1649 )),
1650 None => Err(VectorError::Other("community not found".into())),
1651 }
1652 }
1653
1654 fn channel_id_of(channel_id: &str) -> Result<crate::community::ChannelId> {
1655 crate::simd::hex::hex_to_bytes_32_checked(channel_id)
1656 .map(crate::community::ChannelId)
1657 .ok_or_else(|| VectorError::Other("malformed channel id".into()))
1658 }
1659
1660 pub async fn create_channel(&self, community_id: &str, name: &str, private: bool) -> Result<String> {
1665 use crate::community::{v2::service, transport::LiveTransport};
1666 let community = Self::v2_community(community_id)?;
1667 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1668 let id = if private {
1669 service::create_private_channel(&transport, &community, name).await
1670 } else {
1671 service::create_public_channel(&transport, &community, name).await
1672 }
1673 .map_err(VectorError::Other)?;
1674 if let Some(client) = state::nostr_client() {
1677 crate::community::v2::realtime::refresh_subscription(&client).await;
1678 }
1679 Ok(crate::simd::hex::bytes_to_hex_32(&id.0))
1680 }
1681
1682 pub async fn rename_channel(&self, community_id: &str, channel_id: &str, name: &str) -> Result<()> {
1685 use crate::community::{v2::service, transport::LiveTransport};
1686 let community = Self::v2_community(community_id)?;
1687 let id = Self::channel_id_of(channel_id)?;
1688 let mut meta = community
1689 .channel(&id)
1690 .ok_or_else(|| VectorError::Other("unknown channel".into()))?
1691 .metadata();
1692 meta.name = name.to_string();
1693 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1694 service::edit_channel_metadata(&transport, &community, &id, &meta)
1695 .await
1696 .map_err(VectorError::Other)
1697 }
1698
1699 pub async fn delete_channel(&self, community_id: &str, channel_id: &str) -> Result<()> {
1702 use crate::community::{v2::service, transport::LiveTransport};
1703 let community = Self::v2_community(community_id)?;
1704 let id = Self::channel_id_of(channel_id)?;
1705 let name = community.channel(&id).map(|c| c.name.clone()).unwrap_or_default();
1706 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1707 service::delete_channel(&transport, &community, &id, &name)
1708 .await
1709 .map_err(VectorError::Other)
1710 }
1711
1712 pub async fn grant_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1715 use crate::community::{v2::service, transport::LiveTransport};
1716 let community = Self::v2_community(community_id)?;
1717 let id = Self::channel_id_of(channel_id)?;
1718 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1719 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1720 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1721 service::grant_channel_access(&transport, &community, &id, &member)
1722 .await
1723 .map_err(VectorError::Other)
1724 }
1725
1726 pub async fn revoke_channel_access(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<()> {
1730 use crate::community::{v2::service, transport::LiveTransport};
1731 let community = Self::v2_community(community_id)?;
1732 let id = Self::channel_id_of(channel_id)?;
1733 let member = nostr_sdk::prelude::PublicKey::parse(npub)
1734 .map_err(|e| VectorError::Other(format!("bad npub: {e}")))?;
1735 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1736 service::revoke_channel_access(&transport, &community, &id, &member)
1737 .await
1738 .map_err(VectorError::Other)
1739 }
1740
1741 pub fn member_can_read_channel(&self, community_id: &str, channel_id: &str, npub: &str) -> Result<bool> {
1764 use nostr_sdk::prelude::PublicKey;
1765 let community = Self::v2_community(community_id)?;
1766 let id = Self::channel_id_of(channel_id)?;
1767 let ch = community.channel(&id).ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1768 let who = PublicKey::parse(npub).map_err(|_| VectorError::Other(format!("invalid npub: {npub}")))?.to_hex();
1769 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1770 if crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default().contains(&who) {
1771 return Ok(false);
1772 }
1773 if !ch.private {
1774 return Ok(true);
1775 }
1776 if community.owner().map(|o| o.to_hex()) == Ok(who.clone()) {
1777 return Ok(true);
1778 }
1779 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1780 let access_ids = roster.channel_role_ids(&crate::simd::hex::bytes_to_hex_32(&id.0));
1781 Ok(roster
1782 .grants
1783 .iter()
1784 .filter(|g| g.member == who)
1785 .any(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid))))
1786 }
1787
1788 pub fn channel_access(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
1789 use nostr_sdk::prelude::{PublicKey, ToBech32};
1790 let community = Self::v2_community(community_id)?;
1791 let id = Self::channel_id_of(channel_id)?;
1792 let ch = community
1793 .channel(&id)
1794 .ok_or_else(|| VectorError::Other("unknown channel".into()))?;
1795 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
1798 let roster = crate::db::community::get_community_roles(&cid_hex).map_err(VectorError::Other)?;
1799 let banned = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
1800 let chan_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
1801 let access_ids = roster.channel_role_ids(&chan_hex);
1802 let roles: Vec<serde_json::Value> = roster
1803 .channel_roles(&chan_hex)
1804 .into_iter()
1805 .map(|r| serde_json::json!({ "role_id": r.role_id, "name": r.name }))
1806 .collect();
1807 let members: Vec<String> = roster
1808 .grants
1809 .iter()
1810 .filter(|g| !banned.contains(&g.member))
1811 .filter(|g| g.role_ids.iter().any(|rid| access_ids.contains(rid)))
1812 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
1813 .collect();
1814 Ok(serde_json::json!({
1815 "channel_id": chan_hex,
1816 "private": ch.private,
1817 "readable": !(ch.private && ch.key.is_none()),
1818 "owner": community.owner().ok().and_then(|o| o.to_bech32().ok()),
1819 "roles": roles,
1820 "members": members,
1821 }))
1822 }
1823
1824 pub async fn create_public_invite(
1829 &self,
1830 community_id: &str,
1831 expires_at_ms: Option<u64>,
1832 label: Option<String>,
1833 ) -> Result<String> {
1834 use crate::community::{service, transport::LiveTransport, CommunityId};
1835 if community_id.len() != 64 {
1836 return Err(VectorError::Other("malformed community id".into()));
1837 }
1838 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1839 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1841 crate::db::community::community_protocol(&cid).ok()
1842 {
1843 let community = crate::db::community::load_community_v2(&cid)
1844 .map_err(VectorError::Other)?
1845 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1846 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1847 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
1850 let minted =
1851 crate::community::v2::service::mint_public_link(&transport, &community, base, expires_at_ms, label)
1852 .await
1853 .map_err(VectorError::Other)?;
1854 return Ok(minted.url);
1855 }
1856 let community = crate::db::community::load_community(&CommunityId(
1857 crate::simd::hex::hex_to_bytes_32(community_id),
1858 ))
1859 .map_err(VectorError::Other)?
1860 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1861 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
1862 let expires_at_secs = expires_at_ms.map(|ms| ms / 1000);
1863 let (_token, url) = service::create_public_invite(&transport, &community, expires_at_secs, label)
1864 .await
1865 .map_err(VectorError::Other)?;
1866 Ok(url)
1867 }
1868
1869 pub async fn invite_to_community(&self, community_id: &str, invitee_npub: &str) -> Result<serde_json::Value> {
1873 crate::db::scoped(async move {
1874 use crate::community::{service, CommunityId};
1875 use crate::sending::{send_rumor_dm, NoOpSendCallback, SendCallback, SendConfig};
1876
1877 let my_pk = crate::state::my_public_key()
1878 .ok_or_else(|| VectorError::Other("Public key not set".into()))?;
1879
1880 if community_id.len() != 64 {
1881 return Err(VectorError::Other("malformed community id".into()));
1882 }
1883 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1884 if let Some(Some(crate::community::ConcordProtocol::V2)) =
1890 crate::db::community::community_protocol(&cid).ok()
1891 {
1892 let recipient = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1893 .map_err(|e| VectorError::Other(format!("bad invitee npub: {e}")))?;
1894 let client = crate::state::nostr_client().ok_or_else(|| VectorError::Other("Not connected".into()))?;
1895 let bundle = {
1903 let lock = crate::community::v2::realtime::follow_lock(&cid);
1904 let _rotation = lock.lock().await;
1905 let community = crate::db::community::load_community_v2(&cid)
1906 .map_err(VectorError::Other)?
1907 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
1908 crate::community::v2::service::bundle_of(
1909 &community,
1910 crate::community::v2::service::BundleAudience::Member(recipient),
1911 Some(my_pk),
1912 None,
1913 None,
1914 )
1915 };
1916 let bundle_json = serde_json::to_string(&bundle).map_err(|e| VectorError::Other(e.to_string()))?;
1917 let expires_at = nostr_sdk::prelude::Timestamp::now().as_secs()
1920 + crate::community::invite::DIRECT_INVITE_EXPIRY_SECS;
1921 let expiry_tag = nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(expires_at));
1922 let rumor = nostr_sdk::prelude::EventBuilder::new(
1923 nostr_sdk::prelude::Kind::Custom(crate::community::v2::kind::DIRECT_INVITE),
1924 bundle_json,
1925 )
1926 .tag(expiry_tag.clone())
1927 .finalize_unsigned_with_id(my_pk);
1928 let k_tag = nostr_sdk::prelude::Tag::custom(
1929 "k",
1930 [crate::community::v2::kind::DIRECT_INVITE.to_string()],
1931 );
1932 crate::inbox_relays::send_gift_wrap(&client, &recipient, rumor, [k_tag, expiry_tag])
1933 .await
1934 .map_err(VectorError::Other)?;
1935 return Ok(serde_json::json!({ "invited": invitee_npub, "version": 2 }));
1936 }
1937 let community = crate::db::community::load_community(&CommunityId(
1938 crate::simd::hex::hex_to_bytes_32(community_id),
1939 ))
1940 .map_err(VectorError::Other)?
1941 .ok_or_else(|| VectorError::Other("community not found".into()))?;
1942
1943 if !service::caller_has_permission(&community, crate::community::roles::Permissions::CREATE_INVITE) {
1944 return Err(VectorError::Other("You need the create-invite permission to invite someone".into()));
1945 }
1946 let invitee_hex = nostr_sdk::prelude::PublicKey::parse(invitee_npub)
1947 .map_err(|_| VectorError::Other("invalid npub".into()))?
1948 .to_hex();
1949 if crate::db::community::get_community_banlist(community_id)
1950 .map_err(VectorError::Other)?
1951 .iter()
1952 .any(|b| b == &invitee_hex)
1953 {
1954 return Err(VectorError::Other("That member is banned from this community and can't be invited".into()));
1955 }
1956
1957
1958 let now = nostr_sdk::prelude::Timestamp::now().as_secs();
1959 let rumor = crate::community::invite::build_invite_rumor(&community, my_pk, now)
1960 .map_err(VectorError::Other)?;
1961 let pending_id = format!("community-invite-{}", community_id);
1962 let config = SendConfig { self_send: false, ..SendConfig::gui() };
1964 let callback: Arc<dyn SendCallback> = Arc::new(NoOpSendCallback);
1965
1966 let result = send_rumor_dm(invitee_npub, &pending_id, rumor, &config, callback)
1967 .await
1968 .map_err(VectorError::Other)?;
1969
1970 Ok(serde_json::json!({
1971 "community_id": community_id,
1972 "invitee": invitee_npub,
1973 "wrap_event_id": result.event_id,
1974 }))
1975 })
1976 .await
1977 }
1978
1979 pub fn list_public_invites(&self, community_id: &str) -> Result<Vec<crate::db::community::PublicInviteRecord>> {
1984 crate::db::community::list_public_invites(community_id).map_err(VectorError::Other)
1985 }
1986
1987 pub async fn revoke_public_invite(&self, community_id: &str, token: &str) -> Result<()> {
1992 use crate::community::{service, transport::LiveTransport, CommunityId};
1993 if community_id.len() != 64 {
1994 return Err(VectorError::Other("malformed community id".into()));
1995 }
1996 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
1997 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
1998 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
2001 let community = crate::db::community::load_community_v2(&cid)
2002 .map_err(VectorError::Other)?
2003 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2004 return crate::community::v2::service::revoke_public_link(&transport, &community, token)
2005 .await
2006 .map_err(VectorError::Other);
2007 }
2008 let token_bytes = crate::simd::hex::hex_to_bytes_32(token);
2009 let community = crate::db::community::load_community(&cid)
2010 .map_err(VectorError::Other)?
2011 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2012 service::revoke_public_invite(&transport, &community, &token_bytes)
2013 .await
2014 .map_err(VectorError::Other)
2015 }
2016
2017 pub async fn send_community_message(
2019 &self,
2020 channel_id: &str,
2021 content: &str,
2022 replied_to: Option<&str>,
2023 ) -> Result<String> {
2024 self.send_community_message_expiring(channel_id, content, replied_to, None).await
2025 }
2026
2027 pub async fn send_community_message_expiring(
2039 &self,
2040 channel_id: &str,
2041 content: &str,
2042 replied_to: Option<&str>,
2043 expires_in_secs: Option<u64>,
2044 ) -> Result<String> {
2045 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2046 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2048 let community = crate::db::community::load_community_v2(&id)
2049 .map_err(VectorError::Other)?
2050 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2051 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2052 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2053 let reply = match replied_to.filter(|r| !r.is_empty()) {
2056 Some(parent_id) => {
2057 let author_hex = {
2058 let st = state::STATE.lock().await;
2059 st.find_message(parent_id)
2060 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2061 .map(|pk| pk.to_hex())
2062 .unwrap_or_default()
2063 };
2064 Some((parent_id.to_string(), author_hex))
2065 }
2066 None => None,
2067 };
2068 let reply_ref = reply.as_ref().map(|(id, author)| (id.as_str(), author.as_str()));
2069 let emoji_owned = crate::emoji_packs::resolve_outbound_emoji_tags(content);
2072 let emoji_pairs: Vec<(&str, &str)> = emoji_owned.iter().map(|t| (t.shortcode.as_str(), t.url.as_str())).collect();
2073 let mut extra_tags = Vec::new();
2074 if let Some(secs) = expires_in_secs.filter(|s| *s > 0) {
2075 let at = std::time::SystemTime::now()
2076 .duration_since(std::time::UNIX_EPOCH)
2077 .map(|d| d.as_secs())
2078 .unwrap_or(0)
2079 .saturating_add(secs);
2080 extra_tags.push(nostr_sdk::prelude::Tag::expiration(nostr_sdk::prelude::Timestamp::from_secs(at)));
2081 }
2082 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, content, reply_ref, &emoji_pairs, extra_tags)
2083 .await
2084 .map_err(VectorError::Other);
2085 }
2086 if expires_in_secs.is_some_and(|s| s > 0) {
2087 return Err(VectorError::Other(
2088 "this Community predates self-destructing messages; the message was not sent".into(),
2089 ));
2090 }
2091 let (community, channel) = self.resolve_channel(channel_id)?;
2092 Self::ensure_v1_writable(&community)?;
2093 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2094 let reply = replied_to.filter(|r| !r.is_empty());
2095 let ms = std::time::SystemTime::now()
2096 .duration_since(std::time::UNIX_EPOCH)
2097 .map(|d| d.as_millis() as u64)
2098 .unwrap_or(0);
2099 let unsigned = envelope::build_inner_typed(
2100 author_pk,
2101 &channel.id,
2102 channel.epoch,
2103 crate::stored_event::event_kind::COMMUNITY_MESSAGE,
2104 content,
2105 ms,
2106 reply,
2107 &[],
2108 );
2109 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2110 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2111 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2112 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2113 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2114 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2115 .await
2116 .map_err(VectorError::Other)?;
2117 let echoed = {
2119 let mut st = state::STATE.lock().await;
2120 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2121 };
2122 if let Some(inbound::IncomingEvent::NewMessage(msg)) = echoed {
2123 let _ = crate::db::events::save_message(channel_id, &msg).await;
2124 }
2125 Ok(message_id)
2126 }
2127
2128 pub async fn send_community_file(&self, channel_id: &str, file_path: &str) -> Result<String> {
2132 crate::db::scoped(async move {
2133 use crate::community::{attachments, envelope, inbound, service, transport::LiveTransport};
2134 let path = std::path::Path::new(file_path);
2135 let bytes = std::fs::read(path).map_err(VectorError::Io)?;
2136 if bytes.is_empty() {
2137 return Err(VectorError::Other("Empty file".into()));
2138 }
2139 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file").to_string();
2140 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("bin").to_lowercase();
2141
2142 let v2_target = match self.v2_community_for_channel(channel_id)? {
2147 Some(id) => Some(
2148 crate::db::community::load_community_v2(&id)
2149 .map_err(VectorError::Other)?
2150 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?,
2151 ),
2152 None => None,
2153 };
2154 let v1_target = match v2_target {
2155 Some(_) => None,
2156 None => Some(self.resolve_channel(channel_id)?),
2157 };
2158 match (&v2_target, &v1_target) {
2163 (Some(c), _) => {
2164 let cid = crate::simd::hex::bytes_to_hex_32(&c.id().0);
2165 if crate::db::community::get_community_dissolved(&cid).unwrap_or(false) {
2166 return Err(VectorError::Other("this community has been dissolved".into()));
2167 }
2168 }
2169 (None, Some((c, _))) => Self::ensure_v1_writable(c)?,
2170 _ => {}
2171 }
2172 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2173
2174 let file_hash = crate::crypto::sha256_hex(&bytes);
2175 let mime = crate::crypto::mime_from_extension(&extension);
2176 let img_meta = crate::crypto::generate_image_metadata(&bytes);
2177
2178 let download_dir = crate::db::get_download_dir();
2180 let _ = std::fs::create_dir_all(&download_dir);
2181 let local_name = if filename.is_empty() { format!("{}.{}", &file_hash, extension) } else { filename.clone() };
2182 let local_path = crate::crypto::resolve_unique_filename(&download_dir, &local_name);
2183 let _ = std::fs::write(&local_path, &bytes);
2184
2185 let params = crate::crypto::generate_encryption_params();
2187 let encrypted = crate::crypto::encrypt_data(&bytes, ¶ms)?;
2188 let encrypted_size = encrypted.len() as u64;
2189
2190 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2191 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2192 let servers = crate::blossom_servers::compute_enabled_servers();
2193 if servers.is_empty() {
2194 return Err(VectorError::Other("No Blossom servers configured".into()));
2195 }
2196 let noop_progress: crate::blossom::ProgressCallback = std::sync::Arc::new(|_, _| Ok(()));
2197 let url = crate::blossom::upload_blob_with_progress_and_failover(
2198 signer.clone(),
2199 servers,
2200 std::sync::Arc::new(encrypted),
2201 Some(mime),
2202 true,
2203 noop_progress,
2204 Some(3),
2205 Some(std::time::Duration::from_secs(2)),
2206 None,
2207 ).await.map_err(VectorError::Other)?;
2208
2209 let attachment = crate::types::Attachment {
2210 id: file_hash.clone(),
2211 key: params.key.clone(),
2212 nonce: params.nonce.clone(),
2213 extension: extension.clone(),
2214 name: filename.clone(),
2215 url,
2216 path: local_path.to_string_lossy().to_string(),
2217 size: encrypted_size,
2218 img_meta,
2219 downloading: false,
2220 downloaded: true,
2221 ..Default::default()
2222 };
2223 let imeta = vec![attachments::attachment_to_imeta(&attachment)];
2224
2225 if let Some(community) = v2_target {
2227 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2228 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2229 return crate::community::v2::service::send_chat_message(&transport, &community, &ch, "", None, &[], imeta)
2230 .await
2231 .map_err(VectorError::Other);
2232 }
2233 let (community, channel) = v1_target.expect("v1 target resolved when no v2 community matched");
2234 let ms = std::time::SystemTime::now()
2235 .duration_since(std::time::UNIX_EPOCH)
2236 .map(|d| d.as_millis() as u64)
2237 .unwrap_or(0);
2238 let unsigned = envelope::build_inner_full(
2239 author_pk, &channel.id, channel.epoch,
2240 stored_event::event_kind::COMMUNITY_MESSAGE, "", ms, None, &[], &imeta,
2241 );
2242 let message_id = unsigned.id.ok_or_else(|| VectorError::Other("inner event has no id".into()))?.to_hex();
2243 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2244 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(30));
2245 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2246 .await.map_err(VectorError::Other)?;
2247 let echoed = {
2249 let mut st = state::STATE.lock().await;
2250 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2251 };
2252 if let Some(inbound::IncomingEvent::NewMessage(m)) = echoed {
2253 let _ = crate::db::events::save_message(channel_id, &m).await;
2254 }
2255 Ok(message_id)
2256 })
2257 .await
2258 }
2259
2260 pub async fn send_community_typing(&self, channel_id: &str) -> Result<()> {
2262 use crate::community::{service, transport::LiveTransport};
2263 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2264 let community = crate::db::community::load_community_v2(&id)
2265 .map_err(VectorError::Other)?
2266 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2267 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2268 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2269 return crate::community::v2::service::send_typing(&transport, &community, &ch)
2270 .await
2271 .map_err(VectorError::Other);
2272 }
2273 let (community, channel) = self.resolve_channel(channel_id)?;
2274 Self::ensure_v1_writable(&community)?;
2275 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(8));
2276 service::publish_typing_signal(&transport, &community, &channel)
2277 .await
2278 .map_err(VectorError::Other)
2279 }
2280
2281 pub async fn send_community_reaction(
2284 &self,
2285 channel_id: &str,
2286 message_id: &str,
2287 emoji: &str,
2288 emoji_url: Option<&str>,
2289 ) -> Result<()> {
2290 crate::db::scoped(async move {
2291 if Self::own_reaction_id(message_id, emoji).await.is_some() {
2293 return Ok(());
2294 }
2295 let emoji_tags: Vec<crate::types::EmojiTag> = match emoji_url {
2296 Some(url) if emoji.starts_with(':') && emoji.ends_with(':') && emoji.len() >= 3 && !url.is_empty() => {
2297 vec![crate::types::EmojiTag { shortcode: emoji[1..emoji.len() - 1].to_string(), url: url.to_string() }]
2298 }
2299 _ => Vec::new(),
2300 };
2301 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2302 let community = crate::db::community::load_community_v2(&id)
2303 .map_err(VectorError::Other)?
2304 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2305 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2306 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2307 let held = {
2312 let st = state::STATE.lock().await;
2313 st.find_message(message_id)
2314 .and_then(|(_, m)| m.npub.as_deref().and_then(|n| nostr_sdk::prelude::PublicKey::parse(n).ok()))
2315 };
2316 let held = held.or_else(|| {
2317 crate::db::events::event_author(message_id)
2318 .ok()
2319 .flatten()
2320 .and_then(|n| nostr_sdk::prelude::PublicKey::parse(&n).ok())
2321 });
2322 let target_author = match held {
2323 Some(pk) => pk,
2324 None => crate::community::v2::service::fetch_channel(&transport, &community, &ch, 500)
2325 .await
2326 .map_err(VectorError::Other)?
2327 .iter()
2328 .find(|f| f.event.opened().rumor_id.to_hex() == message_id)
2329 .map(|f| f.event.opened().author)
2330 .ok_or_else(|| VectorError::Other("reacted-to message not found".into()))?,
2331 };
2332 let pair = emoji_tags.first().map(|t| (t.shortcode.as_str(), t.url.as_str()));
2334 return crate::community::v2::service::send_reaction(
2339 &transport, &community, &ch, message_id, &target_author.to_hex(), crate::community::v2::kind::MESSAGE, emoji, pair,
2340 )
2341 .await
2342 .map(|_| ())
2343 .map_err(VectorError::Other);
2344 }
2345 self.publish_community_control(
2346 channel_id, stored_event::event_kind::COMMUNITY_REACTION, emoji, message_id, &emoji_tags,
2347 ).await
2348 })
2349 .await
2350 }
2351
2352 pub async fn edit_community_message(&self, channel_id: &str, message_id: &str, new_content: &str) -> Result<()> {
2354 let emoji_tags = emoji_packs::resolve_outbound_emoji_tags(new_content);
2355 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2356 let community = crate::db::community::load_community_v2(&id)
2357 .map_err(VectorError::Other)?
2358 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2359 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2360 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2361 return crate::community::v2::service::send_edit(&transport, &community, &ch, message_id, new_content)
2362 .await
2363 .map(|_| ())
2364 .map_err(VectorError::Other);
2365 }
2366 self.publish_community_control(
2367 channel_id, stored_event::event_kind::COMMUNITY_EDIT, new_content, message_id, &emoji_tags,
2368 ).await
2369 }
2370
2371 pub async fn delete_community_message(&self, message_id: &str) -> Result<()> {
2375 let channel_id = {
2376 let st = state::STATE.lock().await;
2377 match st.find_message(message_id) {
2378 Some((chat, _)) => chat.id.clone(),
2379 None => return Err(VectorError::Other("message not found (already deleted?)".into())),
2380 }
2381 };
2382 self.delete_community_message_in(&channel_id, message_id).await
2383 }
2384
2385 pub async fn delete_community_message_in(&self, channel_id: &str, message_id: &str) -> Result<()> {
2389 use crate::community::{service, transport::LiveTransport};
2390 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2391
2392 let attachment_urls: Vec<String> = {
2395 let st = state::STATE.lock().await;
2396 st.find_message(message_id)
2397 .map(|(_, msg)| msg.attachments.iter().flat_map(|a| a.all_urls().map(str::to_string)).collect())
2398 .unwrap_or_default()
2399 };
2400
2401 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2402 let community = crate::db::community::load_community_v2(&id)
2405 .map_err(VectorError::Other)?
2406 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2407 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(&channel_id));
2408 crate::community::v2::service::send_delete(
2409 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE,
2410 )
2411 .await
2412 .map_err(VectorError::Other)?;
2413 } else {
2414 if crate::db::community::get_message_key(message_id).map(|k| k.is_some()).unwrap_or(false) {
2416 let _ = service::delete_message(&transport, message_id).await;
2417 }
2418 self.publish_community_control(
2420 &channel_id, stored_event::event_kind::COMMUNITY_DELETE, "", message_id, &[],
2421 ).await?;
2422 }
2423 if !attachment_urls.is_empty() {
2425 if let Some(_client) = state::nostr_client() {
2426 if let Ok(signer) = crate::signer::active_signer() {
2427 crate::blossom::delete_blobs_best_effort(signer, attachment_urls);
2428 }
2429 }
2430 }
2431 let removed_chat = {
2432 let mut st = state::STATE.lock().await;
2433 st.remove_message(message_id).map(|(cid, _)| cid)
2434 };
2435 let _ = crate::db::events::delete_event(message_id).await;
2436 traits::emit_event_json("message_removed", serde_json::json!({
2437 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(&channel_id), "reason": "deleted",
2438 }));
2439 Ok(())
2440 }
2441
2442 pub async fn hide_community_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
2448 use crate::community::transport::LiveTransport;
2449 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2450
2451 let author_npub = {
2454 let st = state::STATE.lock().await;
2455 st.find_message(message_id).and_then(|(_, m)| m.npub)
2456 };
2457 let author_npub = match author_npub {
2458 Some(n) => n,
2459 None => crate::db::events::event_author(message_id)
2460 .ok()
2461 .flatten()
2462 .ok_or_else(|| VectorError::Other("can't resolve the target message's author".into()))?,
2463 };
2464 let author = nostr_sdk::prelude::PublicKey::parse(&author_npub)
2465 .map_err(|_| VectorError::Other("target message has an unreadable author".into()))?;
2466
2467 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2468 let community = crate::db::community::load_community_v2(&id)
2469 .map_err(VectorError::Other)?
2470 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2471 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2472 crate::community::v2::service::moderation_delete(
2473 &transport, &community, &ch, message_id, crate::community::v2::kind::MESSAGE, &author,
2474 )
2475 .await
2476 .map_err(VectorError::Other)?;
2477 } else {
2478 let cid = crate::db::community::community_id_for_channel(channel_id)
2479 .map_err(VectorError::Other)?
2480 .ok_or_else(|| VectorError::Other("unknown community channel".into()))?;
2481 let community = crate::db::community::load_community(&crate::community::CommunityId(
2482 crate::simd::hex::hex_to_bytes_32(&cid),
2483 ))
2484 .map_err(VectorError::Other)?
2485 .ok_or_else(|| VectorError::Other("community not found".into()))?;
2486 let channel = community
2487 .channels
2488 .iter()
2489 .find(|c| c.id.to_hex() == channel_id)
2490 .cloned()
2491 .ok_or_else(|| VectorError::Other("channel not found in community".into()))?;
2492 crate::community::service::publish_owner_hide(&transport, &community, &channel, message_id)
2493 .await
2494 .map_err(VectorError::Other)?;
2495 }
2496
2497 let removed_chat = {
2498 let mut st = state::STATE.lock().await;
2499 st.remove_message(message_id).map(|(cid, _)| cid)
2500 };
2501 let _ = crate::db::events::delete_event(message_id).await;
2502 traits::emit_event_json("message_removed", serde_json::json!({
2503 "id": message_id, "chat_id": removed_chat.as_deref().unwrap_or(channel_id), "reason": "hidden",
2504 }));
2505 Ok(())
2506 }
2507
2508 async fn publish_community_control(
2511 &self,
2512 channel_id: &str,
2513 kind: u16,
2514 content: &str,
2515 target: &str,
2516 emoji_tags: &[crate::types::EmojiTag],
2517 ) -> Result<()> {
2518 use crate::community::{envelope, inbound, service, transport::LiveTransport};
2519 let (community, channel) = self.resolve_channel(channel_id)?;
2520 Self::ensure_v1_writable(&community)?;
2521 let author_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2522 let ms = std::time::SystemTime::now()
2523 .duration_since(std::time::UNIX_EPOCH)
2524 .map(|d| d.as_millis() as u64)
2525 .unwrap_or(0);
2526 let unsigned = envelope::build_inner_typed(
2527 author_pk, &channel.id, channel.epoch, kind, content, ms, Some(target), emoji_tags,
2528 );
2529 let _client = state::nostr_client().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2530 let signer = crate::signer::active_signer().map_err(|e| VectorError::Other(format!("Signer unavailable: {e}")))?;
2531 let inner = unsigned.finalize_async(&signer).await.map_err(|e| VectorError::Other(format!("sign: {e}")))?;
2532 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2533 let outer = service::send_signed_message(&transport, &community, &channel, &inner)
2534 .await.map_err(VectorError::Other)?;
2535 let outcome = {
2538 let mut st = state::STATE.lock().await;
2539 inbound::process_incoming(&mut st, &outer, &channel, &author_pk)
2540 };
2541 if let Some(inbound::IncomingEvent::Updated { target_id, mut message, edit_event }) = outcome {
2542 if let Some(ev) = edit_event {
2543 let mut ev = (*ev).clone();
2544 if let Ok(cid) = crate::db::id_cache::get_chat_id_by_identifier(channel_id) { ev.chat_id = cid; }
2545 let _ = crate::db::events::save_event(&ev).await;
2546 } else {
2547 let _ = crate::db::events::save_message(channel_id, &message).await;
2548 }
2549 traits::emit_message_update(channel_id, &target_id, &mut message).await;
2550 }
2551 Ok(())
2552 }
2553
2554 pub async fn sync_community_channel(&self, channel_id: &str, limit: usize) -> Result<(usize, Vec<String>)> {
2562 self.sync_community_channel_before(channel_id, limit, None).await
2563 }
2564
2565 pub async fn sync_community_channel_page(
2568 &self,
2569 channel_id: &str,
2570 limit: usize,
2571 before_secs: Option<u64>,
2572 since_secs: Option<u64>,
2573 ) -> Result<(BackfillCount, Vec<String>)> {
2574 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2575 let warnings = if community::v2::realtime::follow_worker_running() {
2576 community::v2::realtime::enqueue_follow(&id);
2577 Vec::new()
2578 } else {
2579 Self::v2_inline_follow(&id).await
2580 };
2581 let count = Self::v2_backfill_channel_counted(
2582 &id, channel_id, limit, 8, since_secs, before_secs,
2583 crate::community::transport::Evidence::Fast, 12,
2584 ).await;
2585 return Ok((count, warnings));
2586 }
2587 let (new_messages, warnings) = self.sync_community_channel(channel_id, limit).await?;
2590 Ok((BackfillCount { fetched: new_messages, new_messages }, warnings))
2591 }
2592
2593 pub async fn sync_community_channel_before(
2602 &self,
2603 channel_id: &str,
2604 limit: usize,
2605 before_secs: Option<u64>,
2606 ) -> Result<(usize, Vec<String>)> {
2607 use crate::community::{send, service, transport::LiveTransport};
2608 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2609 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2614 let warnings = if community::v2::realtime::follow_worker_running() {
2615 community::v2::realtime::enqueue_follow(&id);
2616 Vec::new()
2617 } else {
2618 Self::v2_inline_follow(&id).await
2619 };
2620 let new = Self::v2_backfill_channel(
2625 &id, channel_id, limit, 8, None, before_secs,
2626 crate::community::transport::Evidence::Fast, 12,
2627 ).await;
2628 return Ok((new, warnings));
2629 }
2630 let (community, _) = self.resolve_channel(channel_id)?;
2631 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2632 let mut warnings: Vec<String> = Vec::new();
2633
2634 match service::catch_up_server_root(&transport, &community).await {
2642 Ok(c) if c.removed => {
2643 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2645 return Ok((0, warnings));
2646 }
2647 Ok(_) => {}
2648 Err(e) => warnings.push(format!("base catch-up failed: {e}")),
2649 }
2650 let (community, _) = self.resolve_channel(channel_id)?;
2651
2652 if let Err(e) = service::fetch_and_apply_control(&transport, &community).await {
2658 warnings.push(format!("control fold failed: {e}"));
2659 }
2660 if service::am_i_banned(&community) {
2661 let _ = crate::db::community::delete_community_retain_keys(&community.id.to_hex());
2663 return Ok((0, warnings));
2664 }
2665 let (community, channel) = self.resolve_channel(channel_id)?;
2668 if let Err(e) = service::catch_up_channel_rekeys(&transport, &community, &channel.id).await {
2669 warnings.push(format!("channel catch-up failed: {e}"));
2670 }
2671 let (community, _) = self.resolve_channel(channel_id)?;
2675 if let Err(e) = service::retry_pending_read_cut(&transport, &community).await {
2676 warnings.push(format!("read-cut resume failed: {e}"));
2677 }
2678 let (community, channel) = self.resolve_channel(channel_id)?;
2679
2680 let session = crate::db::current_session();
2682 let events = send::fetch_channel_page(&transport, &community, &channel, None, None, limit.max(1))
2683 .await
2684 .map_err(VectorError::Other)?;
2685 let new = Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await;
2686 Ok((new, warnings))
2687 }
2688
2689 async fn v1_ingest_channel_page(
2694 channel_id: &str,
2695 events: &[nostr_sdk::prelude::Event],
2696 channel: &crate::community::Channel,
2697 my_pk: nostr_sdk::prelude::PublicKey,
2698 session: &std::sync::Arc<crate::db::Session>,
2699 ) -> usize {
2700 crate::db::scoped(async move {
2701 use crate::community::inbound;
2702 let outcomes = {
2703 let mut st = state::STATE.lock().await;
2704 inbound::process_channel_batch(&mut st, &events, &channel, &my_pk)
2705 };
2706 let mut new = 0usize;
2707 let mut pending: Vec<&crate::types::Message> = Vec::new();
2711 for o in &outcomes {
2712 if !session.is_live() {
2714 pending.clear();
2715 break;
2716 }
2717 match o {
2718 inbound::IncomingEvent::NewMessage(m) => {
2719 pending.push(m);
2720 new += 1;
2721 }
2722 inbound::IncomingEvent::Updated { message, .. } => {
2723 pending.push(message);
2724 }
2725 inbound::IncomingEvent::Removed { target_id } => {
2726 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2727 crate::state::note_message_deleted(target_id);
2734 if let Err(e) = crate::db::events::add_message_tombstone(target_id) {
2735 crate::log_warn!("[Community delete] tombstone write failed: {e}");
2736 }
2737 let _ = crate::db::events::delete_event(target_id).await;
2738 }
2739 inbound::IncomingEvent::ReactionRemoved { reaction_id, .. } => {
2740 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2743 let _ = crate::db::events::delete_event(reaction_id).await;
2744 }
2745 inbound::IncomingEvent::Presence { npub, joined, event_id, created_at, invited_by, invited_label } => {
2746 let et = if *joined {
2747 crate::stored_event::SystemEventType::MemberJoined
2748 } else {
2749 crate::stored_event::SystemEventType::MemberLeft
2750 };
2751 let note = invited_by.as_ref().map(|by| match invited_label {
2753 Some(l) if !l.is_empty() => format!("{by}|{l}"),
2754 _ => by.clone(),
2755 });
2756 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;
2757 }
2758 inbound::IncomingEvent::WebxdcPeer { npub, topic_id, node_addr, event_id, created_at } => {
2759 community::service::persist_webxdc_signal(
2762 channel_id, npub, topic_id, node_addr.as_deref(), event_id, *created_at,
2763 ).await;
2764 }
2765 inbound::IncomingEvent::Kicked { community_id }
2766 | inbound::IncomingEvent::SelfLeft { community_id } => {
2767 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2772 let _ = crate::db::community::delete_community_retain_keys(community_id);
2773 break;
2774 }
2775 inbound::IncomingEvent::Typing { .. } => {
2776 }
2778 }
2779 }
2780 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
2781 new
2782 })
2783 .await
2784 }
2785
2786 pub async fn sync_channel_events(
2801 &self,
2802 channel_id: &str,
2803 max_events: usize,
2804 until_s: Option<u64>,
2805 since_s: Option<u64>,
2806 ) -> Result<usize> {
2807 use crate::community::{send, transport::LiveTransport};
2808 let max = max_events.clamp(1, 500);
2809 if let Some(id) = self.v2_community_for_channel(channel_id)? {
2810 let community = crate::db::community::load_community_v2(&id)
2811 .map_err(VectorError::Other)?
2812 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
2813 if community.dissolved {
2814 return Err(VectorError::Other("this community has been dissolved".into()));
2815 }
2816 let ch_id = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
2817 let ch = community
2818 .channel(&ch_id)
2819 .ok_or_else(|| VectorError::Other("no such channel in this community".into()))?;
2820 if ch.private && ch.key.is_none() {
2823 return Err(VectorError::Other(
2824 "this private channel has no key yet (awaiting rekey delivery)".into(),
2825 ));
2826 }
2827 let new = Self::v2_backfill_channel(
2828 &id, channel_id, max, 1, since_s, until_s,
2829 crate::community::transport::Evidence::Fast, 12,
2830 )
2831 .await;
2832 return Ok(new);
2833 }
2834 let (community, channel) = self.resolve_channel(channel_id)?;
2835 Self::ensure_v1_writable(&community)?;
2836 let my_pk = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?;
2837 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
2838 let session = crate::db::current_session();
2839 let events = send::fetch_channel_page(&transport, &community, &channel, until_s, since_s, max)
2840 .await
2841 .map_err(VectorError::Other)?;
2842 Ok(Self::v1_ingest_channel_page(channel_id, &events, &channel, my_pk, &session).await)
2843 }
2844
2845 pub async fn get_chat_commands(&self, chat_id: &str) -> crate::bot_interface::ChatCommandsSnapshot {
2857 use crate::bot_interface::{self, ChatCommandsSnapshot};
2858 use nostr_sdk::prelude::ToBech32;
2859
2860 let mut bots: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2861 let mut relays: Vec<String> = Vec::new();
2862 let community_hex = crate::db::community::community_id_for_channel(chat_id).ok().flatten();
2863 if let Some(cid_hex) = community_hex {
2864 let mut members: Vec<nostr_sdk::prelude::PublicKey> = Vec::new();
2865 if let Ok(Some(community)) = Self::load_v2_if_v2(&cid_hex) {
2866 members = community::v2::service::stored_memberlist(&community).unwrap_or_default();
2867 relays = community.relays.clone();
2868 } else {
2869 let id = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(&cid_hex));
2870 let Ok(Some(community)) = crate::db::community::load_community(&id) else {
2871 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2872 };
2873 relays = community.relays.clone();
2874 for (npub, _) in crate::db::community::community_member_activity(&cid_hex).unwrap_or_default() {
2875 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(&npub) {
2876 members.push(pk);
2877 }
2878 }
2879 }
2880 let state = crate::state::STATE.lock().await;
2881 for pk in members {
2882 let Ok(npub) = pk.to_bech32();
2883 if state.get_profile(&npub).map(|p| p.flags.is_bot()).unwrap_or(false) {
2884 bots.push(pk);
2885 }
2886 }
2887 } else if chat_id.starts_with("npub1") {
2888 if let Ok(pk) = nostr_sdk::prelude::PublicKey::parse(chat_id) {
2889 let is_bot = {
2890 let state = crate::state::STATE.lock().await;
2891 state.get_profile(chat_id).map(|p| p.flags.is_bot()).unwrap_or(false)
2892 };
2893 if is_bot {
2894 bots.push(pk);
2895 if let Some(client) = crate::state::nostr_client() {
2898 relays = client.relays().await.keys().map(|u| u.to_string()).collect();
2899 }
2900 }
2901 }
2902 }
2903
2904 if bots.is_empty() {
2905 return ChatCommandsSnapshot { bots: 0, commands: Vec::new(), fresh: true };
2906 }
2907 relays.extend(bot_interface::DISCOVERY_RELAYS.iter().map(|s| s.to_string()));
2910 relays.sort();
2911 relays.dedup();
2912 bots.sort_by_key(|p| p.to_hex());
2915 let bot_hexes: Vec<String> = bots.iter().map(|p| p.to_hex()).collect();
2916 let commands = bot_interface::assemble_from_store(&bot_hexes);
2917 let fresh = bot_interface::commands_fresh(chat_id, &bot_hexes);
2918 if !fresh {
2919 bot_interface::spawn_commands_refresh(chat_id.to_string(), bots.clone(), relays);
2920 }
2921 ChatCommandsSnapshot { bots: bots.len(), commands, fresh }
2922 }
2923
2924 pub async fn get_community_members(&self, community_id: &str) -> Vec<serde_json::Value> {
2929 use nostr_sdk::prelude::ToBech32;
2930 match Self::load_v2_if_v2(community_id) {
2936 Ok(Some(community)) => {
2937 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
2938 let (_, cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
2939 if cursor == 0 {
2940 if crate::community::v2::realtime::follow_worker_running() {
2941 crate::community::v2::realtime::enqueue_follow(community.id());
2942 } else {
2943 let c2 = community.clone();
2944 db::spawn_bound(async move {
2945 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
2946 if matches!(crate::community::v2::service::sync_guestbook(&transport, &c2).await, Ok(fresh) if !fresh.is_empty()) {
2947 emit_event("community_refreshed", &serde_json::json!({ "community_id": cid_hex }));
2948 }
2949 });
2950 }
2951 }
2952 return crate::community::v2::service::stored_memberlist(&community)
2953 .unwrap_or_default()
2954 .into_iter()
2955 .filter_map(|pk| pk.to_bech32().ok())
2956 .map(|npub| serde_json::json!({ "npub": npub }))
2957 .collect();
2958 }
2959 Ok(None) => {} Err(_) => return Vec::new(),
2962 }
2963 crate::db::community::community_member_activity(community_id)
2964 .unwrap_or_default()
2965 .into_iter()
2966 .map(|(npub, last_active)| serde_json::json!({ "npub": npub, "last_active": last_active }))
2967 .collect()
2968 }
2969
2970 pub fn get_community_banned(&self, community_id: &str) -> Vec<String> {
2980 use nostr_sdk::prelude::{PublicKey, ToBech32};
2981 let cid_hex = match Self::load_v2_if_v2(community_id) {
2984 Ok(Some(c)) => crate::simd::hex::bytes_to_hex_32(&c.id().0),
2985 _ => community_id.to_string(),
2986 };
2987 crate::db::community::get_community_banlist(&cid_hex)
2988 .unwrap_or_default()
2989 .iter()
2990 .filter_map(|hex| PublicKey::parse(hex).ok())
2991 .filter_map(|pk| pk.to_bech32().ok())
2992 .collect()
2993 }
2994
2995 pub fn community_moderation_intel(&self, community_id: &str) -> Result<serde_json::Value> {
3002 let community = Self::load_v2_if_v2(community_id)?
3003 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3004 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3005 let report = Self::policy_console_report(&cid_hex, &community)?;
3006 let invites = crate::db::community::list_public_invites(&cid_hex).unwrap_or_default();
3007 let banlist = crate::db::community::get_community_banlist(&cid_hex).unwrap_or_default();
3008 let caps = self.community_capabilities(community_id).unwrap_or_else(|_| serde_json::json!({}));
3009 let owner_b32 = community.owner().map_err(VectorError::Other)?.to_bech32().unwrap_or_default();
3010
3011 Ok(serde_json::json!({
3012 "community_id": cid_hex,
3013 "name": community.name,
3014 "owner_npub": owner_b32,
3015 "epoch": community.root_epoch.0,
3016 "report": &*report,
3017 "invites": invites.iter().map(|i| serde_json::json!({
3018 "token": i.token,
3019 "label": i.label,
3020 "created_at": i.created_at,
3021 "expires_at": i.expires_at,
3022 "join_count": i.join_count,
3023 })).collect::<Vec<_>>(),
3024 "banlist_count": banlist.len(),
3025 "banlist_max": crate::community::v2::roles::MAX_BANLIST,
3026 "capabilities": caps,
3027 "channels": community.channels.iter().map(|c| serde_json::json!({
3030 "id": crate::simd::hex::bytes_to_hex_32(&c.id.0),
3031 "name": c.name,
3032 })).collect::<Vec<_>>(),
3033 }))
3034 }
3035
3036 pub fn screen_community_message(
3045 &self,
3046 channel_id: &str,
3047 author_npub: &str,
3048 text: &str,
3049 ) -> Result<serde_json::Value> {
3050 use nostr_sdk::prelude::PublicKey;
3051 let Some(id) = self.v2_community_for_channel(channel_id)? else {
3052 return Ok(serde_json::json!({ "findings": [] }));
3053 };
3054 let community = crate::db::community::load_community_v2(&id)
3055 .map_err(VectorError::Other)?
3056 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
3057 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3058 let owner = community.owner().map_err(VectorError::Other)?;
3059 let author = PublicKey::parse(author_npub).map_err(|_| VectorError::Other("unreadable author".into()))?;
3060
3061 use crate::community::roles::Permissions;
3064 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3065 | Permissions::MANAGE_CHANNELS
3066 | Permissions::MANAGE_METADATA
3067 | Permissions::KICK
3068 | Permissions::BAN
3069 | Permissions::MANAGE_MESSAGES;
3070 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3071 let hex = author.to_hex();
3072 let roles: Vec<String> =
3073 roster.grants.iter().find(|g| g.member == hex).map(|g| g.role_ids.clone()).unwrap_or_default();
3074 let is_staff = roles
3075 .iter()
3076 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0));
3077
3078 let now_ms = std::time::SystemTime::now()
3079 .duration_since(std::time::UNIX_EPOCH)
3080 .map(|d| d.as_millis() as u64)
3081 .unwrap_or(0);
3082 let findings = crate::community::policy::harness::screen_message(
3083 &cid_hex, &owner, &author, &roles, is_staff, channel_id, text, now_ms,
3084 );
3085 Ok(serde_json::json!({ "findings": findings }))
3086 }
3087
3088 pub fn check_community_raid(&self, community_id: &str) -> Result<serde_json::Value> {
3092 let Some(community) = Self::load_v2_if_v2(community_id)? else {
3093 return Ok(serde_json::json!({ "detected": false }));
3094 };
3095 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3096 let report = Self::policy_console_report(&cid_hex, &community)?;
3097 let biggest = report["cohorts"].get(0);
3098 Ok(serde_json::json!({
3099 "detected": report["raid_detected"].as_bool().unwrap_or(false),
3100 "suspects": report["suspects"].as_u64().unwrap_or(0),
3101 "cohort": biggest.and_then(|c| c["size"].as_u64()).unwrap_or(0),
3102 "sample": biggest
3103 .and_then(|c| c["sample"].as_str())
3104 .map(|s| s.chars().take(60).collect::<String>())
3105 .unwrap_or_default(),
3106 }))
3107 }
3108
3109 fn raid_report(cid_hex: &str) -> Result<std::sync::Arc<crate::community::raid::RaidReport>> {
3113 use crate::community::raid;
3114 use crate::community::v2::guestbook::GuestbookEntry;
3115 use nostr_sdk::prelude::PublicKey;
3116 use std::collections::{HashMap, HashSet};
3117
3118 let now_secs = std::time::SystemTime::now()
3119 .duration_since(std::time::UNIX_EPOCH)
3120 .map(|d| d.as_secs())
3121 .unwrap_or(0);
3122 {
3123 let cache = raid_report_cache();
3124 let map = cache.lock().unwrap_or_else(|e| e.into_inner());
3125 if let Some((at, report)) = map.get(cid_hex) {
3126 if now_secs.saturating_sub(*at) < RAID_REPORT_TTL_SECS {
3127 return Ok(report.clone());
3128 }
3129 }
3130 }
3131 let community = crate::db::community::load_community_v2(&crate::community::CommunityId(
3132 crate::simd::hex::hex_to_bytes_32(cid_hex),
3133 ))
3134 .map_err(VectorError::Other)?
3135 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3136 let owner_b32 = community.owner().map_err(VectorError::Other)?.to_bech32().unwrap_or_default();
3137 let me_b32 = state::my_public_key().and_then(|p| p.to_bech32().ok()).unwrap_or_default();
3138
3139 let roster = crate::db::community::get_community_roles(cid_hex).unwrap_or_default();
3141 let staff: HashSet<String> = roster
3142 .grants
3143 .iter()
3144 .filter(|g| !g.role_ids.is_empty())
3145 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
3146 .collect();
3147
3148 let members: Vec<String> = crate::community::v2::service::stored_memberlist(&community)
3150 .unwrap_or_default()
3151 .into_iter()
3152 .filter_map(|pk| pk.to_bech32().ok())
3153 .collect();
3154
3155 let (events, _cursor) = crate::db::community::get_guestbook(cid_hex).unwrap_or_default();
3157 let mut joined_at: HashMap<String, u64> = HashMap::new();
3158 let mut invite_label: HashMap<String, String> = HashMap::new();
3159 for e in &events {
3160 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3161 let Ok(b32) = member.to_bech32();
3162 let slot = joined_at.entry(b32.clone()).or_insert(*at_ms);
3164 *slot = (*slot).min(*at_ms);
3165 if let Some((_creator, label)) = invited_by {
3166 invite_label.entry(b32).or_insert_with(|| label.clone());
3167 }
3168 }
3169
3170 let footprints: HashMap<String, crate::db::community::AuthorFootprint> =
3171 crate::db::community::community_author_footprints(cid_hex)
3172 .unwrap_or_default()
3173 .into_iter()
3174 .map(|f| (f.npub.clone(), f))
3175 .collect();
3176
3177 const TEXT_WINDOW: usize = 4_000;
3180 let mut texts: HashMap<String, Vec<String>> = HashMap::new();
3181 for (npub, _at, text) in crate::db::community::community_recent_texts(cid_hex, TEXT_WINDOW).unwrap_or_default() {
3182 texts.entry(npub).or_default().push(text);
3183 }
3184
3185 let signals: Vec<raid::MemberSignals> = members
3186 .iter()
3187 .map(|npub| {
3188 let f = footprints.get(npub);
3189 raid::MemberSignals {
3190 npub: npub.clone(),
3191 joined_at_ms: joined_at.get(npub).copied().unwrap_or(0),
3192 invite_label: invite_label.get(npub).cloned(),
3193 messages: f.map(|f| f.messages).unwrap_or(0),
3194 first_secs: f.map(|f| f.first_secs).unwrap_or(0),
3195 last_secs: f.map(|f| f.last_secs).unwrap_or(0),
3196 texts: texts.get(npub).cloned().unwrap_or_default(),
3197 is_owner: *npub == owner_b32,
3198 is_admin: staff.contains(npub),
3199 is_me: *npub == me_b32,
3200 }
3201 })
3202 .collect();
3203
3204 let params = raid::RaidParams {
3205 known_shortcodes: crate::emoji_packs::known_shortcodes(),
3206 ..Default::default()
3207 };
3208 let report = std::sync::Arc::new(raid::assess(&signals, now_secs, ¶ms));
3209 raid_report_cache()
3210 .lock()
3211 .unwrap_or_else(|e| e.into_inner())
3212 .insert(cid_hex.to_string(), (now_secs, report.clone()));
3213 Ok(report)
3214 }
3215
3216 pub fn policy_presets(&self) -> Result<serde_json::Value> {
3218 let presets: Vec<serde_json::Value> = crate::community::policy::presets::all()
3219 .into_iter()
3220 .map(|p| {
3221 serde_json::json!({
3222 "id": p.id,
3223 "name": p.name,
3224 "description": p.description,
3225 "example": p.example,
3226 "caveat": p.caveat,
3227 "dials": p.dials,
3228 "rules": crate::community::policy::presets::describe(&p.policy),
3232 "bytes": serde_json::to_string(&p.policy).unwrap_or_default(),
3233 })
3234 })
3235 .collect();
3236 let rule_kinds: Vec<serde_json::Value> = crate::community::policy::presets::rule_kinds()
3240 .into_iter()
3241 .map(|k| {
3242 serde_json::json!({
3243 "id": k.id,
3244 "label": k.label,
3245 "description": k.description,
3246 "input": k.input,
3247 "input_label": k.input_label,
3248 "input_hint": k.input_hint,
3249 "rule": k.rule,
3250 })
3251 })
3252 .collect();
3253 Ok(serde_json::json!({ "presets": presets, "rule_kinds": rule_kinds }))
3254 }
3255
3256 pub fn preview_community_policy(&self, community_id: &str, bytes: &str) -> Result<serde_json::Value> {
3260 use crate::community::v2::guestbook::GuestbookEntry;
3261 use nostr_sdk::prelude::PublicKey;
3262 let community = Self::load_v2_if_v2(community_id)?
3263 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3264 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3265 let owner = community.owner().map_err(VectorError::Other)?;
3266 let me = state::my_public_key();
3267
3268 use crate::community::roles::Permissions;
3269 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3270 | Permissions::MANAGE_CHANNELS
3271 | Permissions::MANAGE_METADATA
3272 | Permissions::KICK
3273 | Permissions::BAN
3274 | Permissions::MANAGE_MESSAGES;
3275 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3276 let staff: std::collections::HashSet<String> = roster
3277 .grants
3278 .iter()
3279 .filter(|g| {
3280 g.role_ids
3281 .iter()
3282 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0))
3283 })
3284 .map(|g| g.member.clone())
3285 .collect();
3286 let roles_of: std::collections::HashMap<String, Vec<String>> =
3287 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3288
3289 let (events, _cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
3290 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3291 let mut invite_label: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3292 for e in &events {
3293 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3294 let hex = member.to_hex();
3295 let slot = joined_at.entry(hex.clone()).or_insert(*at_ms);
3296 *slot = (*slot).min(*at_ms);
3297 if let Some((_c, label)) = invited_by {
3298 invite_label.entry(hex).or_insert_with(|| label.clone());
3299 }
3300 }
3301 let members: Vec<(PublicKey, Option<u64>, bool, Vec<String>, Option<String>)> =
3302 crate::community::v2::service::stored_memberlist(&community)
3303 .unwrap_or_default()
3304 .into_iter()
3305 .map(|pk| {
3306 let hex = pk.to_hex();
3307 (
3308 pk,
3309 joined_at.get(&hex).copied(),
3310 staff.contains(&hex),
3311 roles_of.get(&hex).cloned().unwrap_or_default(),
3312 invite_label.get(&hex).cloned(),
3313 )
3314 })
3315 .collect();
3316
3317 let now_ms = std::time::SystemTime::now()
3318 .duration_since(std::time::UNIX_EPOCH)
3319 .map(|d| d.as_millis() as u64)
3320 .unwrap_or(0);
3321 let assembled =
3322 crate::community::policy::harness::assemble(&cid_hex, &owner, me.as_ref(), &members, now_ms)
3323 .map_err(VectorError::Other)?;
3324 let preview = crate::community::policy::harness::preview_policy(&assembled, bytes, now_ms);
3325 serde_json::to_value(preview).map_err(|e| VectorError::Other(e.to_string()))
3326 }
3327
3328 pub fn list_community_policies(&self, community_id: &str) -> Result<serde_json::Value> {
3331 let community = Self::load_v2_if_v2(community_id)?
3332 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3333 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3334 let stored = crate::db::community::get_community_policies(&cid_hex).map_err(VectorError::Other)?;
3335 let rows: Vec<serde_json::Value> = stored
3336 .iter()
3337 .map(|p| {
3338 let verdict = serde_json::from_str::<crate::community::policy::document::Policy>(&p.bytes)
3342 .map_err(|e| e.to_string())
3343 .and_then(|doc| doc.validate().map_err(|r| format!("{r:?}")));
3344 serde_json::json!({
3345 "policy_id": p.policy_id,
3346 "hash": p.hash,
3347 "enabled": p.enabled,
3348 "updated_at": p.updated_at,
3349 "bytes": p.bytes,
3350 "valid": verdict.is_ok(),
3351 "error": verdict.err(),
3352 })
3353 })
3354 .collect();
3355 Ok(serde_json::json!({
3356 "community_id": cid_hex,
3357 "policies": rows,
3358 "using_builtin": !stored
3363 .iter()
3364 .any(|p| p.policy_id == crate::community::policy::harness::DEFAULTS_POLICY_ID),
3365 }))
3366 }
3367
3368 pub fn set_community_policy(&self, community_id: &str, policy_id: &str, bytes: &str, enabled: bool) -> Result<serde_json::Value> {
3371 let community = Self::load_v2_if_v2(community_id)?
3372 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3373 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3374 if bytes.len() > crate::community::policy::types::caps::MAX_POLICY_BYTES {
3375 return Err(VectorError::Other("policy exceeds the maximum document size".into()));
3376 }
3377 let doc: crate::community::policy::document::Policy =
3378 serde_json::from_str(bytes).map_err(|e| VectorError::Other(format!("policy is not valid JSON: {e}")))?;
3379 doc.validate().map_err(|r| VectorError::Other(format!("policy rejected: {r:?}")))?;
3380
3381 let hash = crate::community::policy::harness::hash_policy_bytes(bytes.as_bytes());
3382 let hash_hex = crate::simd::hex::bytes_to_hex_32(&hash.0);
3383 let now_secs = std::time::SystemTime::now()
3384 .duration_since(std::time::UNIX_EPOCH)
3385 .map(|d| d.as_secs())
3386 .unwrap_or(0);
3387 crate::db::community::set_community_policy(&cid_hex, policy_id, bytes, &hash_hex, enabled, now_secs)
3388 .map_err(VectorError::Other)?;
3389 Self::invalidate_raid_report(&cid_hex);
3392 Ok(serde_json::json!({ "policy_id": policy_id, "hash": hash_hex, "enabled": enabled }))
3393 }
3394
3395 pub fn delete_community_policy(&self, community_id: &str, policy_id: &str) -> Result<()> {
3396 let community = Self::load_v2_if_v2(community_id)?
3397 .ok_or_else(|| VectorError::Other("policies require a Concord v2 community".into()))?;
3398 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3399 crate::db::community::delete_community_policy(&cid_hex, policy_id).map_err(VectorError::Other)?;
3400 Self::invalidate_raid_report(&cid_hex);
3401 Ok(())
3402 }
3403
3404 pub fn builtin_policy_json(&self) -> Result<String> {
3407 serde_json::to_string_pretty(&crate::community::policy::harness::default_policy())
3408 .map_err(|e| VectorError::Other(e.to_string()))
3409 }
3410
3411 fn policy_console_report(cid_hex: &str, community: &crate::community::v2::community::CommunityV2) -> Result<std::sync::Arc<serde_json::Value>> {
3419 use crate::community::v2::guestbook::GuestbookEntry;
3420 use nostr_sdk::prelude::PublicKey;
3421 let now_secs = std::time::SystemTime::now()
3422 .duration_since(std::time::UNIX_EPOCH)
3423 .map(|d| d.as_secs())
3424 .unwrap_or(0);
3425 {
3426 let cache = policy_report_cache();
3427 let guard = cache.lock().unwrap_or_else(|e| e.into_inner());
3428 if let Some((at, cached)) = guard.get(cid_hex) {
3429 if now_secs.saturating_sub(*at) < RAID_REPORT_TTL_SECS {
3430 return Ok(cached.clone());
3431 }
3432 }
3433 }
3434
3435 let owner = community.owner().map_err(VectorError::Other)?;
3436 let me = state::my_public_key();
3437
3438 use crate::community::roles::Permissions;
3442 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3443 | Permissions::MANAGE_CHANNELS
3444 | Permissions::MANAGE_METADATA
3445 | Permissions::KICK
3446 | Permissions::BAN
3447 | Permissions::MANAGE_MESSAGES;
3448 let roster = crate::db::community::get_community_roles(cid_hex).unwrap_or_default();
3449 let staff: std::collections::HashSet<String> = roster
3450 .grants
3451 .iter()
3452 .filter(|g| {
3453 g.role_ids
3454 .iter()
3455 .any(|rid| roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0))
3456 })
3457 .map(|g| g.member.clone())
3458 .collect();
3459 let roles_of: std::collections::HashMap<String, Vec<String>> =
3460 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3461
3462 let (events, _cursor) = crate::db::community::get_guestbook(cid_hex).unwrap_or_default();
3463 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3464 let mut invite_label: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3465 for e in &events {
3466 let GuestbookEntry::Join { member, invited_by, at_ms } = &e.entry else { continue };
3467 let hex = member.to_hex();
3468 let slot = joined_at.entry(hex.clone()).or_insert(*at_ms);
3470 *slot = (*slot).min(*at_ms);
3471 if let Some((_creator, label)) = invited_by {
3472 invite_label.entry(hex).or_insert_with(|| label.clone());
3473 }
3474 }
3475
3476 let members: Vec<(PublicKey, Option<u64>, bool, Vec<String>, Option<String>)> =
3477 crate::community::v2::service::stored_memberlist(community)
3478 .unwrap_or_default()
3479 .into_iter()
3480 .map(|pk| {
3481 let hex = pk.to_hex();
3482 (
3483 pk,
3484 joined_at.get(&hex).copied(),
3485 staff.contains(&hex),
3486 roles_of.get(&hex).cloned().unwrap_or_default(),
3487 invite_label.get(&hex).cloned(),
3488 )
3489 })
3490 .collect();
3491
3492 let now_ms = now_secs.saturating_mul(1000);
3493 let assembled =
3494 crate::community::policy::harness::assemble(cid_hex, &owner, me.as_ref(), &members, now_ms)
3495 .map_err(VectorError::Other)?;
3496 let report = std::sync::Arc::new(
3497 crate::community::policy::harness::evaluate_for_console(cid_hex, &assembled, now_ms)
3498 .map_err(VectorError::Other)?,
3499 );
3500 policy_report_cache()
3501 .lock()
3502 .unwrap_or_else(|e| e.into_inner())
3503 .insert(cid_hex.to_string(), (now_secs, report.clone()));
3504 Ok(report)
3505 }
3506
3507 pub fn policy_side_by_side(&self, community_id: &str) -> Result<serde_json::Value> {
3511 use crate::community::v2::guestbook::GuestbookEntry;
3512 use nostr_sdk::prelude::PublicKey;
3513 let community = Self::load_v2_if_v2(community_id)?
3514 .ok_or_else(|| VectorError::Other("moderation tools require a Concord v2 community".into()))?;
3515 let cid_hex = crate::simd::hex::bytes_to_hex_32(&community.id().0);
3516 let owner = community.owner().map_err(VectorError::Other)?;
3517
3518 use crate::community::roles::Permissions;
3521 const MOD_MASK: u64 = Permissions::MANAGE_ROLES
3522 | Permissions::MANAGE_CHANNELS
3523 | Permissions::MANAGE_METADATA
3524 | Permissions::KICK
3525 | Permissions::BAN
3526 | Permissions::MANAGE_MESSAGES;
3527 let roster = crate::db::community::get_community_roles(&cid_hex).unwrap_or_default();
3528 let staff: std::collections::HashSet<String> = roster
3529 .grants
3530 .iter()
3531 .filter(|g| {
3532 g.role_ids.iter().any(|rid| {
3533 roster.roles.iter().any(|r| &r.role_id == rid && (r.permissions.0 & MOD_MASK) != 0)
3534 })
3535 })
3536 .map(|g| g.member.clone())
3537 .collect();
3538 let roles_of: std::collections::HashMap<String, Vec<String>> =
3539 roster.grants.iter().map(|g| (g.member.clone(), g.role_ids.clone())).collect();
3540
3541 let (events, _cursor) = crate::db::community::get_guestbook(&cid_hex).unwrap_or_default();
3542 let mut joined_at: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
3543 for e in &events {
3544 let GuestbookEntry::Join { member, at_ms, .. } = &e.entry else { continue };
3545 let hex = member.to_hex();
3546 let slot = joined_at.entry(hex).or_insert(*at_ms);
3547 *slot = (*slot).min(*at_ms);
3548 }
3549
3550 let members: Vec<(PublicKey, Option<u64>, bool, Vec<String>)> =
3551 crate::community::v2::service::stored_memberlist(&community)
3552 .unwrap_or_default()
3553 .into_iter()
3554 .map(|pk| {
3555 let hex = pk.to_hex();
3556 (pk, joined_at.get(&hex).copied(), staff.contains(&hex), roles_of.get(&hex).cloned().unwrap_or_default())
3557 })
3558 .collect();
3559
3560 let assessments: Vec<(String, String)> = Self::raid_report(&cid_hex)
3562 .map(|r| {
3563 r.members
3564 .iter()
3565 .map(|m| (m.npub.clone(), format!("{:?}", m.verdict).to_lowercase()))
3566 .collect()
3567 })
3568 .unwrap_or_default();
3569
3570 let now_ms = std::time::SystemTime::now()
3571 .duration_since(std::time::UNIX_EPOCH)
3572 .map(|d| d.as_millis() as u64)
3573 .unwrap_or(0);
3574 let diff = crate::community::policy::harness::run_side_by_side(
3575 &cid_hex, &owner, &members, &assessments, now_ms,
3576 )
3577 .map_err(VectorError::Other)?;
3578 serde_json::to_value(diff).map_err(|e| VectorError::Other(e.to_string()))
3579 }
3580
3581 pub fn invalidate_raid_report(community_id: &str) {
3585 policy_report_cache().lock().unwrap_or_else(|e| e.into_inner()).remove(community_id);
3586 raid_report_cache().lock().unwrap_or_else(|e| e.into_inner()).remove(community_id);
3587 }
3588
3589 pub async fn refound_community(&self, community_id: &str, retain: &[&str]) -> Result<()> {
3597 let community = Self::load_v2_if_v2(community_id)?
3598 .ok_or_else(|| VectorError::Other("re-founding requires a Concord v2 community".into()))?;
3599 let removed = Self::members_to_remove(&community, retain)?;
3600 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3601 crate::community::v2::service::refound_community(&transport, &community, &removed)
3602 .await
3603 .map_err(VectorError::Other)?;
3604 Self::invalidate_raid_report(community_id);
3606 Ok(())
3607 }
3608
3609 fn members_to_remove(
3627 community: &crate::community::v2::community::CommunityV2,
3628 retain: &[&str],
3629 ) -> Result<Vec<nostr_sdk::prelude::PublicKey>> {
3630 use nostr_sdk::prelude::PublicKey;
3631 let keep: std::collections::HashSet<PublicKey> =
3632 retain.iter().filter_map(|n| PublicKey::parse(n).ok()).collect();
3633 if keep.is_empty() {
3634 return Ok(Vec::new());
3635 }
3636 let me = state::my_public_key();
3637 let owner = community.owner().ok();
3638 Ok(crate::community::v2::service::stored_memberlist(community)
3639 .map_err(VectorError::Other)?
3640 .into_iter()
3641 .filter(|pk| !keep.contains(pk))
3642 .filter(|pk| Some(*pk) != me && Some(*pk) != owner)
3643 .collect())
3644 }
3645
3646 pub async fn purge_and_refound(
3662 &self,
3663 community_id: &str,
3664 retain: &[&str],
3665 on_progress: &(dyn Fn(usize, usize) + Sync),
3666 ) -> Result<serde_json::Value> {
3667 let community = Self::load_v2_if_v2(community_id)?
3668 .ok_or_else(|| VectorError::Other("re-founding requires a Concord v2 community".into()))?;
3669 let removed = Self::members_to_remove(&community, retain)?;
3670 let keep: Vec<nostr_sdk::prelude::PublicKey> = retain
3675 .iter()
3676 .filter_map(|n| nostr_sdk::prelude::PublicKey::parse(n).ok())
3677 .collect();
3678 log_info!("[Moderation] purge: {} to remove, {} retained", removed.len(), keep.len());
3679 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3680 if keep.is_empty() {
3691 crate::community::v2::service::refound_community(&transport, &community, &removed)
3692 .await
3693 .map_err(VectorError::Other)?;
3694 } else {
3695 crate::community::v2::service::refound_community_retaining(&transport, &community, &removed, &keep)
3696 .await
3697 .map_err(VectorError::Other)?;
3698 }
3699 let fresh = Self::load_v2_if_v2(community_id)?
3703 .ok_or_else(|| VectorError::Other("community gone during the purge".into()))?;
3704 let sweep = crate::community::v2::service::kick_members(&transport, &fresh, &removed, on_progress)
3705 .await
3706 .map_err(VectorError::Other)?;
3707 let _ = crate::community::v2::service::sync_guestbook(&transport, &fresh).await;
3710 log_info!(
3711 "[Moderation] purge: rotated to epoch {}, kicked {}/{} ({} refused, {} failed)",
3712 fresh.root_epoch.0,
3713 sweep.kicked.len(),
3714 removed.len(),
3715 sweep.refused.len(),
3716 sweep.failed.len()
3717 );
3718 Ok(serde_json::json!({
3719 "kicked": sweep.kicked.len(),
3720 "refused": sweep.refused.len(),
3721 "failed": sweep.failed.len(),
3722 "rotated": true,
3723 }))
3724 }
3725
3726 pub async fn kick_community_members(
3729 &self,
3730 community_id: &str,
3731 npubs: &[&str],
3732 on_progress: &(dyn Fn(usize, usize) + Sync),
3733 ) -> Result<serde_json::Value> {
3734 let community = Self::load_v2_if_v2(community_id)?
3735 .ok_or_else(|| VectorError::Other("kicking requires a Concord v2 community".into()))?;
3736 use nostr_sdk::prelude::PublicKey;
3737 let targets: Vec<PublicKey> = npubs.iter().filter_map(|n| PublicKey::parse(n).ok()).collect();
3738 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
3739 let sweep = crate::community::v2::service::kick_members(&transport, &community, &targets, on_progress)
3740 .await
3741 .map_err(VectorError::Other)?;
3742 let _ = crate::community::v2::service::sync_guestbook(&transport, &community).await;
3743 log_info!(
3744 "[Moderation] kick sweep: {}/{} kicked ({} refused, {} failed)",
3745 sweep.kicked.len(),
3746 targets.len(),
3747 sweep.refused.len(),
3748 sweep.failed.len()
3749 );
3750 Self::invalidate_raid_report(community_id);
3751 Ok(serde_json::json!({
3752 "kicked": sweep.kicked.len(),
3753 "refused": sweep.refused.iter().map(|(_, why)| why.clone()).collect::<Vec<_>>(),
3754 "failed": sweep.failed.len(),
3755 }))
3756 }
3757
3758 pub async fn revoke_all_public_invites(&self, community_id: &str) -> Result<(usize, usize)> {
3763 let tokens: Vec<String> = crate::db::community::list_public_invites(community_id)
3764 .map_err(VectorError::Other)?
3765 .into_iter()
3766 .map(|i| i.token)
3767 .collect();
3768 let mut revoked = 0usize;
3769 let mut failed = 0usize;
3770 for token in &tokens {
3771 match self.revoke_public_invite(community_id, token).await {
3772 Ok(()) => revoked += 1,
3773 Err(_) => failed += 1,
3774 }
3775 }
3776 Ok((revoked, failed))
3777 }
3778
3779 pub async fn v2_inline_follow(id: &crate::community::CommunityId) -> Vec<String> {
3787 crate::db::scoped(async move {
3788 use crate::community::transport::LiveTransport;
3789 let session = crate::db::current_session();
3790 let lock = crate::community::v2::realtime::follow_lock(id);
3795 let _guard = lock.lock().await;
3796 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
3797 let mut warnings: Vec<String> = Vec::new();
3798 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else {
3799 warnings.push("v2 community not found".to_string());
3800 return warnings;
3801 };
3802 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
3803 match crate::community::v2::service::follow_rekeys(&transport, &community, &session).await {
3804 Ok(f) if f.dissolved => return warnings,
3806 Ok(f) if f.self_removed => {
3807 let _ = crate::db::community::delete_community_retain_keys(&cid_hex);
3815 return warnings;
3816 }
3817 Ok(_) => {}
3818 Err(e) => warnings.push(format!("v2 rekey follow failed: {e}")),
3819 }
3820 if let Ok(Some(fresh)) = crate::db::community::load_community_v2(id) {
3821 match crate::community::v2::service::follow_control(&transport, &fresh).await {
3822 Ok(control) if control.moved() => {
3831 if let Ok(Some(latest)) = crate::db::community::load_community_v2(id) {
3832 if let Err(e) = crate::community::v2::service::follow_rekeys(&transport, &latest, &session).await {
3833 warnings.push(format!("v2 rekey follow failed: {e}"));
3834 }
3835 }
3836 }
3837 Ok(_) => {}
3838 Err(e) => warnings.push(format!("v2 control follow failed: {e}")),
3839 }
3840 }
3841 if let Some(me) = crate::my_public_key() {
3846 if crate::db::community::is_author_banned(&cid_hex, &me) {
3847 let _ = crate::db::community::delete_community_retain_keys(&cid_hex);
3848 }
3849 }
3850 warnings
3851 })
3852 .await
3853 }
3854
3855 pub(crate) async fn v2_backfill_channel(
3867 id: &crate::community::CommunityId,
3868 channel_id: &str,
3869 limit: usize,
3870 max_pages: usize,
3871 since: Option<u64>,
3872 until: Option<u64>,
3873 evidence: crate::community::transport::Evidence,
3874 transport_secs: u64,
3875 ) -> usize {
3876 Self::v2_backfill_channel_counted(id, channel_id, limit, max_pages, since, until, evidence, transport_secs)
3877 .await
3878 .new_messages
3879 }
3880
3881 pub(crate) async fn v2_backfill_channel_counted(
3886 id: &crate::community::CommunityId,
3887 channel_id: &str,
3888 limit: usize,
3889 max_pages: usize,
3890 since: Option<u64>,
3891 until: Option<u64>,
3892 evidence: crate::community::transport::Evidence,
3893 transport_secs: u64,
3894 ) -> BackfillCount {
3895 let session = crate::db::current_session();
3898 let Some(my_pk) = state::my_public_key() else { return BackfillCount::default() };
3899 if crate::db::community::get_community_dissolved(&crate::simd::hex::bytes_to_hex_32(&id.0)).unwrap_or(false) {
3903 return BackfillCount::default();
3904 }
3905 let Ok(Some(community)) = crate::db::community::load_community_v2(id) else { return BackfillCount::default() };
3906 let ch = crate::community::ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
3907 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(transport_secs));
3908 let Ok(page) = crate::community::v2::service::fetch_channel_history(
3909 &transport,
3910 &community,
3911 &ch,
3912 limit.max(50),
3913 max_pages,
3914 since,
3915 until,
3916 evidence,
3917 |page| {
3922 let mut saw_message = false;
3923 for f in page {
3924 if matches!(&f.event, crate::community::v2::chat::ChatEvent::Message { .. }) {
3925 saw_message = true;
3926 if !crate::db::events::event_exists(&f.event.opened().rumor_id.to_hex()).unwrap_or(false) {
3927 return true;
3928 }
3929 }
3930 }
3931 !saw_message
3932 },
3933 )
3934 .await
3935 else {
3936 return BackfillCount::default();
3937 };
3938 let fetched = page.len();
3939 let new_messages = Self::v2_ingest_chat_page(channel_id, my_pk, session, page).await;
3940 BackfillCount { fetched, new_messages }
3941 }
3942
3943 pub(crate) async fn v2_ingest_chat_page(
3947 channel_id: &str,
3948 my_pk: nostr_sdk::prelude::PublicKey,
3949 session: std::sync::Arc<crate::db::Session>,
3950 page: Vec<crate::community::v2::service::FetchedEvent>,
3951 ) -> usize {
3952 crate::db::scoped(async move {
3953 use crate::community::v2::inbound::{apply_chat_to_state, ChatPersist};
3954 let mut new = 0usize;
3955 let mut outcomes: Vec<ChatPersist> = Vec::with_capacity(page.len());
3957 for f in &page {
3958 if let crate::community::v2::chat::ChatEvent::Webxdc { opened } = &f.event {
3963 if opened.author != my_pk {
3964 if let Some((topic, addr)) = crate::webxdc::parse_peer_signal(&opened.rumor.content) {
3965 let Ok(npub) = ToBech32::to_bech32(&opened.author);
3966 crate::community::service::persist_webxdc_signal(
3967 channel_id,
3968 &npub,
3969 &topic,
3970 addr.as_deref(),
3971 &opened.rumor_id.to_hex(),
3972 opened.at_ms / 1000,
3973 )
3974 .await;
3975 }
3976 }
3977 continue;
3978 }
3979 let outcome = {
3980 let mut st = state::STATE.lock().await;
3981 apply_chat_to_state(&mut st, &f.event, channel_id, &my_pk)
3982 };
3983 if let Some(outcome) = outcome {
3984 if matches!(outcome, ChatPersist::New(_)) {
3985 new += 1;
3986 }
3987 outcomes.push(outcome);
3988 }
3989 }
3990 let mut pending: Vec<&crate::types::Message> = Vec::new();
3994 for outcome in &outcomes {
3995 if !session.is_live() {
3996 pending.clear();
3997 break;
3998 }
3999 match outcome {
4000 ChatPersist::New(m) => pending.push(m),
4001 ChatPersist::Updated { message, edit_event } => match edit_event {
4002 Some(ev) => {
4003 let mut ev = (**ev).clone();
4004 if let Ok(cid) = crate::db::id_cache::get_or_create_chat_id(channel_id) {
4007 ev.chat_id = cid;
4008 }
4009 let _ = crate::db::events::save_event(&ev).await;
4010 }
4011 None => pending.push(message),
4012 },
4013 ChatPersist::Removed(target_id) => {
4014 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4015 let _ = crate::db::events::delete_event(target_id).await;
4016 }
4017 ChatPersist::ReactionRemoved { reaction_id, message } => {
4018 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4019 let _ = crate::db::events::delete_event(reaction_id).await;
4020 pending.push(message);
4021 }
4022 }
4023 }
4024 crate::db::events::flush_message_batch(channel_id, &mut pending, &session).await;
4025 drop(pending);
4026 {
4030 let quoted: Vec<&mut crate::types::Message> = outcomes
4031 .iter_mut()
4032 .filter_map(|o| match o {
4033 ChatPersist::New(m)
4034 | ChatPersist::Updated { message: m, .. }
4035 | ChatPersist::ReactionRemoved { message: m, .. } => Some(m),
4036 ChatPersist::Removed(_) => None,
4037 })
4038 .collect();
4039 let _ = crate::db::events::populate_reply_contexts(quoted).await;
4040 }
4041 for outcome in &outcomes {
4047 match outcome {
4048 ChatPersist::New(msg) => crate::traits::emit_event(
4049 "message_new",
4050 &serde_json::json!({ "message": msg, "chat_id": channel_id }),
4051 ),
4052 ChatPersist::Updated { message, .. }
4053 | ChatPersist::ReactionRemoved { message, .. } => {
4054 let mut message = message.clone();
4055 let target_id = message.id.clone();
4056 crate::traits::emit_message_update(channel_id, &target_id, &mut message).await;
4057 }
4058 ChatPersist::Removed(target_id) => crate::traits::emit_event(
4059 "message_removed",
4060 &serde_json::json!({ "id": target_id, "chat_id": channel_id, "reason": "deleted" }),
4061 ),
4062 }
4063 }
4064 new
4065 })
4066 .await
4067 }
4068
4069 fn load_v2_if_v2(community_id: &str) -> Result<Option<crate::community::v2::community::CommunityV2>> {
4073 if community_id.len() != 64 {
4074 return Ok(None);
4075 }
4076 let cid = crate::community::CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4077 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4078 Some(crate::community::ConcordProtocol::V2) => crate::db::community::load_community_v2(&cid).map_err(VectorError::Other),
4079 _ => Ok(None),
4080 }
4081 }
4082
4083 fn load_community_hex(community_id: &str) -> Result<crate::community::Community> {
4088 use crate::community::CommunityId;
4089 if community_id.len() != 64 {
4090 return Err(VectorError::Other("malformed community id".into()));
4091 }
4092 crate::db::community::load_community(&CommunityId(crate::simd::hex::hex_to_bytes_32(community_id)))
4093 .map_err(VectorError::Other)?
4094 .ok_or_else(|| VectorError::Other("community not found".into()))
4095 }
4096
4097 fn admin_role_id_of(community_id: &str) -> Result<String> {
4098 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4099 roles.roles.iter()
4102 .find(|r| matches!(r.scope, crate::community::roles::RoleScope::Server)
4103 && r.permissions.contains(crate::community::roles::Permissions::ADMIN_FOUNDING_MASK))
4104 .map(|r| r.role_id.clone())
4105 .ok_or_else(|| VectorError::Other("admin role not found (roster not synced?)".into()))
4106 }
4107
4108 pub fn community_capabilities(&self, community_id: &str) -> Result<serde_json::Value> {
4112 use crate::community::service;
4113 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4114 use crate::community::roles::Permissions;
4115 let me = state::my_public_key().ok_or_else(|| VectorError::Other("Not logged in".into()))?.to_hex();
4116 let owner_hex = v2.owner().map_err(VectorError::Other)?.to_hex();
4117 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4118 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4121 if banned.contains(&me) && me != owner_hex {
4122 return Ok(serde_json::json!({
4123 "manage_metadata": false, "manage_channels": false, "create_invite": false, "kick": false,
4124 "ban": false, "manage_messages": false, "manage_roles": false, "manage_admin_role": false,
4125 "pin_messages": false, "control_write": false,
4126 }));
4127 }
4128 let control_write = crate::community::v2::control::ControlPlane::of(&v2).can_write();
4134 let has = |p: u64| roster.is_authorized(&me, Some(&owner_hex), p);
4135 let has_ctl = |p: u64| control_write && has(p);
4136 return Ok(serde_json::json!({
4137 "manage_metadata": has_ctl(Permissions::MANAGE_METADATA), "manage_channels": has_ctl(Permissions::MANAGE_CHANNELS),
4138 "create_invite": has_ctl(Permissions::CREATE_INVITE), "kick": has(Permissions::KICK), "ban": has_ctl(Permissions::BAN),
4139 "manage_messages": has(Permissions::MANAGE_MESSAGES), "manage_roles": has_ctl(Permissions::MANAGE_ROLES),
4140 "manage_admin_role": me == owner_hex && control_write,
4142 "pin_messages": has_ctl(Permissions::PIN_MESSAGES),
4143 "control_write": control_write,
4144 }));
4145 }
4146 let community = Self::load_community_hex(community_id)?;
4147 let caps = service::caller_capabilities(&community);
4148 let manage_admin_role = Self::admin_role_id_of(community_id).ok()
4149 .map(|rid| service::caller_can_manage_role_id(&community, &rid))
4150 .unwrap_or(false);
4151 Ok(serde_json::json!({
4152 "manage_metadata": caps.manage_metadata, "manage_channels": caps.manage_channels,
4153 "create_invite": caps.create_invite, "kick": caps.kick, "ban": caps.ban,
4154 "manage_messages": caps.manage_messages, "manage_roles": caps.manage_roles,
4155 "manage_admin_role": manage_admin_role,
4156 "pin_messages": false,
4158 }))
4159 }
4160
4161 pub async fn pin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
4164 use crate::community::{transport::LiveTransport, ChannelId};
4165 let v2 = Self::load_v2_if_v2(community_id)?
4166 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4167 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4168 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4169 crate::community::v2::service::pin_message(&transport, &v2, &ch, message_id)
4170 .await
4171 .map_err(VectorError::Other)
4172 }
4173
4174 pub async fn unpin_community_message(&self, community_id: &str, channel_id: &str, message_id: &str) -> Result<()> {
4176 use crate::community::{transport::LiveTransport, ChannelId};
4177 let v2 = Self::load_v2_if_v2(community_id)?
4178 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4179 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4180 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4181 crate::community::v2::service::unpin_message(&transport, &v2, &ch, message_id)
4182 .await
4183 .map_err(VectorError::Other)
4184 }
4185
4186 pub async fn fetch_pinned_attachment(
4194 &self,
4195 community_id: &str,
4196 channel_id: &str,
4197 rumor_id: &str,
4198 ) -> Result<serde_json::Value> {
4199 crate::db::scoped(async move {
4200 use crate::community::ChannelId;
4201 let v2 = Self::load_v2_if_v2(community_id)?
4202 .ok_or_else(|| VectorError::Other("pins are only available in Concord v2 communities".into()))?;
4203 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4204 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
4205 let pin = pins
4206 .pins
4207 .iter()
4208 .find(|p| p.rumor_id == rumor_id)
4209 .ok_or_else(|| VectorError::Other("that message is not pinned".into()))?;
4210 let dir = crate::db::get_download_dir();
4211 let tag = pin
4212 .tags
4213 .iter()
4214 .find(|t| t.first().map(String::as_str) == Some("imeta"))
4215 .map(|t| nostr_sdk::prelude::Tag::custom("imeta", t[1..].to_vec()))
4216 .ok_or_else(|| VectorError::Other("this pin carries no attachment".into()))?;
4217 let attachment = crate::community::attachments::attachment_from_imeta(&tag, &dir)
4218 .ok_or_else(|| VectorError::Other("this pin's attachment metadata is malformed".into()))?;
4219
4220 let respond = |path: &std::path::Path| {
4221 serde_json::json!({
4222 "path": path.to_string_lossy(),
4223 "name": attachment.name.to_string(),
4224 "extension": attachment.extension.to_string(),
4225 })
4226 };
4227
4228 let expected = attachment.original_hash.as_deref();
4232 let path = std::path::PathBuf::from(&*attachment.path);
4233 if let Ok(bytes) = std::fs::read(&path) {
4234 match expected {
4235 Some(want) if crate::crypto::sha256_hex(&bytes) == want => return Ok(respond(&path)),
4236 None => return Ok(respond(&path)),
4237 _ => {} }
4239 }
4240
4241 let author_npub = nostr_sdk::prelude::PublicKey::from_hex(&pin.author)
4242 .ok()
4243 .and_then(|pk| nostr_sdk::prelude::ToBech32::to_bech32(&pk).ok());
4244 let bytes = self.download_attachment_from(&attachment, author_npub.as_deref()).await?;
4245 if let Some(want) = expected {
4246 if crate::crypto::sha256_hex(&bytes) != want {
4247 return Err(VectorError::Other("downloaded bytes do not match the pinned content hash".into()));
4248 }
4249 }
4250 std::fs::write(&path, &bytes).map_err(|e| VectorError::Other(format!("could not cache the attachment: {e}")))?;
4251 Ok(respond(&path))
4252 })
4253 .await
4254 }
4255
4256 pub fn get_channel_pins(&self, community_id: &str, channel_id: &str) -> Result<serde_json::Value> {
4260 use crate::community::ChannelId;
4261 let Some(v2) = Self::load_v2_if_v2(community_id)? else {
4262 return Ok(serde_json::json!({ "pins": [], "sealed": false, "version": 0 }));
4265 };
4266 let ch = ChannelId(crate::simd::hex::hex_to_bytes_32(channel_id));
4267 let pins = crate::community::v2::service::read_channel_pins(&v2, &ch).map_err(VectorError::Other)?;
4268 serde_json::to_value(&pins).map_err(|e| VectorError::Other(e.to_string()))
4269 }
4270
4271 pub fn community_role_graph(&self, community_id: &str) -> Result<serde_json::Value> {
4281 use nostr_sdk::prelude::{PublicKey, ToBech32};
4282 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4283 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4285 let roles: Vec<serde_json::Value> = roster
4286 .roles
4287 .iter()
4288 .map(|r| {
4289 serde_json::json!({
4290 "role_id": r.role_id,
4291 "name": r.name,
4292 "position": r.position,
4293 "color": r.color,
4294 })
4295 })
4296 .collect();
4297 let grants: Vec<serde_json::Value> = roster
4298 .grants
4299 .iter()
4300 .filter(|g| !banned.contains(&g.member))
4301 .filter_map(|g| {
4302 let npub = PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok())?;
4303 Some(serde_json::json!({ "npub": npub, "role_ids": g.role_ids }))
4304 })
4305 .collect();
4306 Ok(serde_json::json!({ "roles": roles, "grants": grants }))
4307 }
4308
4309 pub fn member_has_permission(&self, community_id: &str, npub: &str, permission: u64) -> Result<bool> {
4325 use nostr_sdk::prelude::PublicKey;
4326 let who = PublicKey::parse(npub).map_err(|_| VectorError::Other(format!("invalid npub: {npub}")))?.to_hex();
4327 let owner_hex = match Self::load_v2_if_v2(community_id)? {
4328 Some(v2) => v2.owner().map_err(VectorError::Other)?.to_hex(),
4329 None => Self::load_community_hex(community_id)?
4330 .owner_attestation
4331 .as_ref()
4332 .and_then(|att| crate::community::owner::verify_owner_attestation(att, community_id))
4333 .map(|pk| pk.to_hex())
4334 .unwrap_or_default(),
4335 };
4336 if who != owner_hex && crate::db::community::get_community_banlist(community_id).unwrap_or_default().contains(&who) {
4337 return Ok(false);
4338 }
4339 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4340 Ok(roster.is_authorized(&who, Some(&owner_hex), permission))
4341 }
4342
4343 pub fn community_roles(&self, community_id: &str) -> Result<serde_json::Value> {
4344 use nostr_sdk::prelude::{PublicKey, ToBech32};
4345 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4346 let owner = v2.owner().map_err(VectorError::Other)?;
4347 let roster = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4348 let banned = crate::db::community::get_community_banlist(community_id).unwrap_or_default();
4350 let admins: Vec<String> = roster.grants.iter()
4351 .filter(|g| roster.is_admin(&g.member) && !banned.contains(&g.member))
4352 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
4353 .collect();
4354 return Ok(serde_json::json!({ "owner": owner.to_bech32().ok(), "admins": admins }));
4355 }
4356 let community = Self::load_community_hex(community_id)?;
4357 let owner = community.owner_attestation.as_ref()
4358 .and_then(|att| crate::community::owner::verify_owner_attestation(att, &community.id.to_hex()))
4359 .and_then(|pk| ToBech32::to_bech32(&pk).ok());
4360 let roles = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4361 let admins: Vec<String> = roles.grants.iter().filter(|g| roles.is_admin(&g.member))
4362 .filter_map(|g| PublicKey::from_hex(&g.member).ok().and_then(|pk| pk.to_bech32().ok()))
4363 .collect();
4364 Ok(serde_json::json!({ "owner": owner, "admins": admins }))
4365 }
4366
4367 async fn converge_v2_authority(
4376 transport: &crate::community::transport::LiveTransport,
4377 community_id: &str,
4378 ) {
4379 crate::db::scoped(async move {
4380 if let Ok(Some(fresh)) = Self::load_v2_if_v2(community_id) {
4383 let _ = crate::community::v2::service::follow_control(transport, &fresh).await;
4384 if let Ok(added) = crate::community::v2::service::sync_guestbook(transport, &fresh).await {
4389 if !added.is_empty() {
4390 traits::emit_event_json(
4391 "community_refreshed",
4392 serde_json::json!({ "community_id": community_id }),
4393 );
4394 }
4395 }
4396 }
4397 })
4398 .await
4399 }
4400
4401 pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()> {
4403 use crate::community::{service, transport::LiveTransport};
4404 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4405 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4406 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4407 crate::community::v2::service::grant_admin(&transport, &v2, &member)
4408 .await
4409 .map_err(VectorError::Other)?;
4410 Self::converge_v2_authority(&transport, community_id).await;
4411 return Ok(());
4412 }
4413 let community = Self::load_community_hex(community_id)?;
4414 let role_id = Self::admin_role_id_of(community_id)?;
4415 service::grant_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
4416 }
4417
4418 pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()> {
4420 use crate::community::{service, transport::LiveTransport};
4421 let member = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4422 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4423 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4424 crate::community::v2::service::revoke_admin(&transport, &v2, &member)
4425 .await
4426 .map_err(VectorError::Other)?;
4427 Self::converge_v2_authority(&transport, community_id).await;
4428 return Ok(());
4429 }
4430 let community = Self::load_community_hex(community_id)?;
4431 let role_id = Self::admin_role_id_of(community_id)?;
4432 service::revoke_role(&transport, &community, member, &role_id).await.map_err(VectorError::Other)
4433 }
4434
4435 pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()> {
4437 crate::db::scoped(async move {
4438 use crate::community::{service, transport::LiveTransport};
4439 let pk = nostr_sdk::prelude::PublicKey::parse(npub).map_err(|_| VectorError::Other("invalid npub".into()))?;
4440 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4441 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4442 crate::community::v2::service::kick_member(&transport, &v2, &pk)
4443 .await
4444 .map_err(VectorError::Other)?;
4445 if let Ok(fresh) = crate::community::v2::service::sync_guestbook(&transport, &v2).await {
4450 if !fresh.is_empty() {
4451 emit_event("community_refreshed", &serde_json::json!({ "community_id": community_id }));
4452 }
4453 }
4454 Self::converge_v2_authority(&transport, community_id).await;
4455 return Ok(());
4456 }
4457 let community = Self::load_community_hex(community_id)?;
4458 let channel = community.channels.first().ok_or_else(|| VectorError::Other("community has no channel".into()))?;
4459 service::publish_kick(&transport, &community, channel, &pk.to_hex()).await.map(|_| ()).map_err(VectorError::Other)
4460 })
4461 .await
4462 }
4463
4464 pub async fn set_member_banned(&self, community_id: &str, npub: &str, banned: bool) -> Result<()> {
4470 self.set_members_banned(community_id, &[npub], banned).await
4471 }
4472
4473 pub async fn set_members_banned(&self, community_id: &str, npubs: &[&str], banned: bool) -> Result<()> {
4483 use crate::community::{service, transport::LiveTransport, CommunityId};
4484 if npubs.is_empty() {
4485 return Ok(());
4486 }
4487 let mut pks: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(npubs.len());
4489 for n in npubs {
4490 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4491 if !pks.contains(&pk) {
4492 pks.push(pk);
4493 }
4494 }
4495 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4496 if community_id.len() == 64 {
4500 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4501 if let Some(crate::community::ConcordProtocol::V2) = crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4505 return crate::community::v2::service::set_members_banned(&transport, &cid, &pks, banned)
4506 .await
4507 .map_err(VectorError::Other);
4508 }
4509 }
4510 let hexes: Vec<String> = pks.iter().map(|p| p.to_hex()).collect();
4512 let mut list = crate::db::community::get_community_banlist(community_id).map_err(VectorError::Other)?;
4513 list.retain(|h| !hexes.contains(h));
4514 if banned {
4515 list.extend(hexes);
4516 }
4517 let community = Self::load_community_hex(community_id)?;
4518 service::publish_banlist(&transport, &community, &list).await.map_err(VectorError::Other)?;
4519 Self::invalidate_raid_report(community_id);
4522 Ok(())
4523 }
4524
4525 pub async fn rescue_stranded_members(&self, community_id: &str, npubs: &[&str]) -> Result<serde_json::Value> {
4536 use nostr_sdk::prelude::PublicKey;
4537 let community = Self::load_v2_if_v2(community_id)?
4538 .ok_or_else(|| VectorError::Other("rescuing requires a Concord v2 community".into()))?;
4539 let mut targets: Vec<PublicKey> = Vec::with_capacity(npubs.len());
4540 for n in npubs {
4541 let pk = PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4542 if !targets.contains(&pk) {
4543 targets.push(pk);
4544 }
4545 }
4546 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(30));
4547 let report = crate::community::v2::service::rescue_stranded_members(&transport, &community, &targets)
4548 .await
4549 .map_err(VectorError::Other)?;
4550 serde_json::to_value(report).map_err(|e| VectorError::Other(e.to_string()))
4551 }
4552
4553 pub async fn repair_community_roster(&self, community_id: &str) -> Result<serde_json::Value> {
4564 use crate::community::CommunityId;
4565 if community_id.len() != 64 {
4566 return Err(VectorError::Other("malformed community id".into()));
4567 }
4568 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4569 if crate::db::community::load_community_v2(&cid).map_err(VectorError::Other)?.is_none() {
4570 return Err(VectorError::Other("not a held Concord v2 community".into()));
4571 }
4572 let before = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4573 let before_at = crate::db::community::get_community_roles_at(community_id).map_err(VectorError::Other)?;
4574 crate::db::community::set_community_roles(community_id, &before, 0).map_err(VectorError::Other)?;
4575 let transport = crate::community::transport::LiveTransport::with_timeout(std::time::Duration::from_secs(20));
4577 let community = crate::db::community::load_community_v2(&cid)
4578 .map_err(VectorError::Other)?
4579 .ok_or_else(|| VectorError::Other("community vanished mid-repair".into()))?;
4580 let follow = crate::community::v2::service::follow_control(&transport, &community).await;
4581 let after = crate::db::community::get_community_roles(community_id).map_err(VectorError::Other)?;
4582 Ok(serde_json::json!({
4583 "provenance_before": before_at,
4584 "provenance_after": crate::db::community::get_community_roles_at(community_id).unwrap_or(0),
4585 "roles_before": before.roles.len(),
4586 "grants_before": before.grants.len(),
4587 "roles_after": after.roles.len(),
4588 "grants_after": after.grants.len(),
4589 "fold_error": follow.as_ref().err().cloned(),
4590 "repaired": after.grants.len() > before.grants.len(),
4591 }))
4592 }
4593
4594 pub async fn explain_epoch_state(&self, community_id: &str) -> Result<serde_json::Value> {
4604 use crate::community::{transport::LiveTransport, CommunityId};
4605 if community_id.len() != 64 {
4606 return Err(VectorError::Other("malformed community id".into()));
4607 }
4608 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4609 let community = crate::db::community::load_community_v2(&cid)
4610 .map_err(VectorError::Other)?
4611 .ok_or_else(|| VectorError::Other("not a held Concord v2 community".into()))?;
4612 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(15));
4613 crate::community::v2::service::explain_epoch_state(&transport, &community)
4614 .await
4615 .map_err(VectorError::Other)
4616 }
4617
4618 pub async fn contain_raid(
4631 &self,
4632 community_id: &str,
4633 npubs: &[&str],
4634 window_start_secs: u64,
4635 cut_also: &[&str],
4636 ) -> Result<crate::community::v2::service::ContainmentReport> {
4637 use crate::community::{transport::LiveTransport, CommunityId};
4638 if community_id.len() != 64 {
4639 return Err(VectorError::Other("malformed community id".into()));
4640 }
4641 let mut pks: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(npubs.len());
4642 for n in npubs {
4643 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4644 if !pks.contains(&pk) {
4645 pks.push(pk);
4646 }
4647 }
4648 let mut extra: Vec<nostr_sdk::prelude::PublicKey> = Vec::with_capacity(cut_also.len());
4649 for n in cut_also {
4650 let pk = nostr_sdk::prelude::PublicKey::parse(n).map_err(|_| VectorError::Other(format!("invalid npub: {n}")))?;
4651 if !extra.contains(&pk) {
4652 extra.push(pk);
4653 }
4654 }
4655 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4656 match crate::db::community::community_protocol(&cid).map_err(VectorError::Other)? {
4657 Some(crate::community::ConcordProtocol::V2) => {}
4658 _ => return Err(VectorError::Other("raid containment requires a Concord v2 community".into())),
4661 }
4662 let session = crate::db::current_session();
4665 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(20));
4666 let report = crate::community::v2::service::contain_raid(&transport, &cid, &pks, window_start_secs, &extra)
4667 .await
4668 .map_err(VectorError::Other)?;
4669 if !session.is_live() {
4670 return Err(VectorError::Other("account changed during raid containment".into()));
4671 }
4672 Self::invalidate_raid_report(community_id);
4673 Ok(report)
4674 }
4675
4676 pub async fn dissolve_community(&self, community_id: &str) -> Result<()> {
4680 use crate::community::{service, transport::LiveTransport, CommunityId};
4681 if community_id.len() != 64 {
4682 return Err(VectorError::Other("malformed community id".into()));
4683 }
4684 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4685 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4686 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
4689 let community = crate::db::community::load_community_v2(&cid)
4690 .map_err(VectorError::Other)?
4691 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
4692 return crate::community::v2::service::dissolve_community(&transport, &community)
4693 .await
4694 .map_err(VectorError::Other);
4695 }
4696 let community = Self::load_community_hex(community_id)?;
4697 service::dissolve_community(&transport, &community).await.map_err(VectorError::Other)
4698 }
4699
4700 pub async fn edit_community_metadata(&self, community_id: &str, name: Option<&str>, description: Option<&str>) -> Result<()> {
4703 use crate::community::{service, transport::LiveTransport, CommunityId};
4704 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4705 if community_id.len() == 64 {
4710 let cid = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4711 if let Some(Some(crate::community::ConcordProtocol::V2)) = crate::db::community::community_protocol(&cid).ok() {
4712 let community = crate::db::community::load_community_v2(&cid)
4713 .map_err(VectorError::Other)?
4714 .ok_or_else(|| VectorError::Other("v2 community not found".into()))?;
4715 let mut meta = community.metadata();
4716 if let Some(n) = name {
4717 meta.name = n.to_string();
4718 }
4719 if let Some(d) = description {
4720 meta.description = if d.is_empty() { None } else { Some(d.to_string()) };
4721 }
4722 return crate::community::v2::service::edit_community_metadata(&transport, &community, &meta)
4723 .await
4724 .map_err(VectorError::Other);
4725 }
4726 }
4727 let mut community = Self::load_community_hex(community_id)?;
4728 if let Some(n) = name { community.name = n.to_string(); }
4729 if let Some(d) = description { community.description = if d.is_empty() { None } else { Some(d.to_string()) }; }
4730 service::republish_community_metadata(&transport, &community).await.map_err(VectorError::Other)
4731 }
4732
4733
4734
4735 pub async fn leave_community(&self, community_id: &str) -> Result<()> {
4738 crate::db::scoped(async move {
4739 use crate::community::{transport::LiveTransport, CommunityId};
4740 if community_id.len() != 64 {
4741 return Err(VectorError::Other("malformed community id".into()));
4742 }
4743 let id = CommunityId(crate::simd::hex::hex_to_bytes_32(community_id));
4744 if let Some(v2) = Self::load_v2_if_v2(community_id)? {
4746 let channel_ids: Vec<String> =
4747 v2.channels.iter().map(|ch| crate::simd::hex::bytes_to_hex_32(&ch.id.0)).collect();
4748 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4749 crate::community::v2::service::leave_community(&transport, &v2)
4750 .await
4751 .map_err(VectorError::Other)?;
4752 let mut st = state::STATE.lock().await;
4753 st.chats.retain(|c| !channel_ids.contains(&c.id));
4754 return Ok(());
4755 }
4756 let community = crate::db::community::load_community(&id).map_err(VectorError::Other)?;
4757 let channel_ids: Vec<String> = community
4758 .as_ref()
4759 .map(|c| c.channels.iter().map(|ch| ch.id.to_hex()).collect())
4760 .unwrap_or_default();
4761 if let Some(ref c) = community {
4763 if let Some(primary) = c.channels.first() {
4764 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
4765 let _ = crate::community::service::publish_presence(&transport, c, primary, false, None).await;
4766 }
4767 }
4768 crate::db::community::delete_community_retain_keys(community_id).map_err(VectorError::Other)?;
4770 {
4771 let mut st = state::STATE.lock().await;
4772 st.chats.retain(|c| !channel_ids.contains(&c.id));
4773 }
4774 Ok(())
4775 })
4776 .await
4777 }
4778
4779 fn ensure_v1_writable(community: &crate::community::Community) -> Result<()> {
4787 if crate::db::community::get_community_dissolved(&community.id.to_hex()).unwrap_or(false) {
4788 return Err(VectorError::Other("this community has been dissolved".into()));
4789 }
4790 Ok(())
4791 }
4792
4793 fn resolve_channel(
4794 &self,
4795 channel_id: &str,
4796 ) -> Result<(crate::community::Community, crate::community::Channel)> {
4797 use crate::community::CommunityId;
4798 let community_id = crate::db::community::community_id_for_channel(channel_id)
4799 .map_err(VectorError::Other)?
4800 .ok_or_else(|| VectorError::Other("Unknown Community channel".into()))?;
4801 if community_id.len() != 64 {
4802 return Err(VectorError::Other("malformed community id".into()));
4803 }
4804 let community = crate::db::community::load_community(&CommunityId(
4805 crate::simd::hex::hex_to_bytes_32(&community_id),
4806 ))
4807 .map_err(VectorError::Other)?
4808 .ok_or_else(|| VectorError::Other("Community not found".into()))?;
4809 let channel = community
4810 .channels
4811 .iter()
4812 .find(|c| c.id.to_hex() == channel_id)
4813 .cloned()
4814 .ok_or_else(|| VectorError::Other("Channel not found in Community".into()))?;
4815 Ok((community, channel))
4816 }
4817
4818
4819 pub async fn sync_dms(
4836 &self,
4837 since_days: Option<u64>,
4838 handler: &dyn InboundEventHandler,
4839 ) -> Result<(u32, u32)> {
4840 crate::db::scoped(async move {
4841 use futures_util::StreamExt;
4842 use nostr_sdk::prelude::*;
4843
4844 let client = state::nostr_client()
4845 .ok_or(VectorError::Other("Not connected".into()))?;
4846 let my_pk = state::my_public_key()
4847 .ok_or(VectorError::Other("Not logged in".into()))?;
4848
4849 let (items, filter) = if let Some(days) = since_days {
4854 let since_ts = Timestamp::now().as_secs().saturating_sub(days * 24 * 3600);
4855 let items = db::wrappers::load_negentropy_items_since(since_ts)
4856 .unwrap_or_default();
4857 let filter = Filter::new()
4858 .pubkey(my_pk)
4859 .kind(Kind::GiftWrap)
4860 .since(Timestamp::from_secs(since_ts));
4861 (items, filter)
4862 } else {
4863 let items = db::wrappers::load_negentropy_items().unwrap_or_default();
4864 let filter = Filter::new()
4865 .pubkey(my_pk)
4866 .kind(Kind::GiftWrap);
4867 (items, filter)
4868 };
4869
4870 log_info!("[SyncDMs] {} negentropy items, since_days={:?}", items.len(), since_days);
4871
4872 let sync_opts = nostr_sdk::prelude::SyncOptions::new()
4874 .direction(nostr_sdk::prelude::SyncDirection::Down)
4875 .initial_timeout(std::time::Duration::from_secs(10))
4876 .dry_run();
4877
4878 let relay_map = client.relays().await;
4882 let (all_relays, no_neg_relays): (Vec<(RelayUrl, Relay)>, Vec<(RelayUrl, Relay)>) =
4883 relay_map.iter()
4884 .map(|(url, relay)| (url.clone(), relay.clone()))
4885 .partition(|(url, _)| negentropy::neg_supported_cached(url.as_str()) != Some(false));
4886 drop(relay_map);
4887 let skipped_no_neg: Vec<String> = no_neg_relays.iter().map(|(u, _)| u.to_string()).collect();
4888 if !skipped_no_neg.is_empty() {
4889 log_info!("[SyncDMs] {} relay(s) on REQ path (no NIP-77)", skipped_no_neg.len());
4890 }
4891
4892 let neg_budget = relay_request_timeout(std::time::Duration::from_secs(10));
4896 let neg_outer = neg_budget + std::time::Duration::from_secs(5);
4897 let connect_allowance = relay_request_timeout(std::time::Duration::from_secs(3))
4898 .min(neg_outer);
4899 let mut relay_futs = futures_util::stream::FuturesUnordered::new();
4900 for (url, relay) in &all_relays {
4901 let url = url.clone();
4902 let relay = relay.clone();
4903 let f = filter.clone();
4904 let i = items.clone();
4905 let o = sync_opts.clone();
4906 relay_futs.push(async move {
4907 if !negentropy::wait_connected(&relay, connect_allowance).await {
4908 return (url, None, false);
4909 }
4910 let result = tokio::time::timeout(
4913 neg_outer,
4914 relay.sync(f).items(i).opts(o),
4915 ).await;
4916 let connected = relay.status() == RelayStatus::Connected;
4917 (url, Some(result), connected)
4918 });
4919 }
4920
4921 let cap_session = crate::db::current_session();
4923 let mut all_missing: std::collections::HashSet<EventId> = std::collections::HashSet::new();
4924 while let Some((url, result, connected)) = relay_futs.next().await {
4925 let Some(result) = result else {
4926 log_warn!("[SyncDMs] {} skipped: not connected", url);
4927 continue;
4928 };
4929 match result {
4930 Ok(Ok(recon)) => {
4931 let count = recon.remote.len();
4932 all_missing.extend(recon.remote);
4933 log_info!("[SyncDMs] {} reconciled: {} missing", url, count);
4934 negentropy::record_neg_support(url.as_str(), true);
4935 }
4936 Ok(Err(e)) => {
4937 log_warn!("[SyncDMs] {} failed: {}", url, e);
4938 if cap_session.is_live()
4939 && negentropy::classify_neg_sync_error(&e.to_string(), connected) == Some(false)
4940 {
4941 log_info!("[SyncDMs] {} marked no-NIP-77 for 24h", url);
4942 negentropy::record_neg_support(url.as_str(), false);
4943 }
4944 }
4945 Err(_) => log_warn!("[SyncDMs] {} timed out ({:?})", url, neg_outer),
4946 }
4947 }
4948
4949 let mut total_events = 0u32;
4950 let mut new_messages = 0u32;
4951
4952 if !skipped_no_neg.is_empty() {
4957 let req_filter = filter.clone().limit(500);
4958 match client
4959 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
4960 skipped_no_neg.iter().cloned().map(|u| (u, vec![req_filter.clone()])),
4961 ))
4962 .timeout(std::time::Duration::from_secs(20))
4963 .await
4964 {
4965 Ok(stream) => {
4966 let mut seen: std::collections::HashSet<[u8; 32]> = std::collections::HashSet::new();
4967 tokio::pin!(stream);
4968 while let Some((_relay, res)) = stream.next().await {
4969 let Ok(event) = res else { continue };
4970 if !seen.insert(event.id.to_bytes()) { continue; }
4974 total_events += 1;
4975 let prepared = event_handler::prepare_event(event, &client, my_pk).await;
4976 if event_handler::commit_prepared_event(prepared, false, handler).await {
4977 new_messages += 1;
4978 }
4979 }
4980 }
4981 Err(e) => log_warn!("[SyncDMs] REQ pass failed: {}", e),
4982 }
4983 }
4984
4985 if all_missing.is_empty() {
4986 log_info!("[SyncDMs] No missing events");
4987 return Ok((total_events, new_messages));
4988 }
4989
4990 log_info!("[SyncDMs] Fetching {} missing events", all_missing.len());
4992 let ids: Vec<EventId> = all_missing.into_iter().collect();
4993 let relay_strs: Vec<String> = client.relays().await.keys()
4994 .map(|u| u.to_string()).collect();
4995
4996 const BATCH_SIZE: usize = 500;
4997
4998 for batch in ids.chunks(BATCH_SIZE) {
4999 let f = Filter::new().ids(batch.to_vec()).kind(Kind::GiftWrap).pubkey(my_pk);
5002 match client
5003 .stream_events(nostr_sdk::prelude::ReqTarget::manual(
5004 relay_strs.iter().cloned().map(|u| (u, vec![f.clone()])),
5005 ))
5006 .timeout(std::time::Duration::from_secs(30))
5007 .await
5008 {
5009 Ok(stream) => {
5010 let client_clone = client.clone();
5011 let prepared_stream = stream
5012 .filter_map(|(_relay, res)| async move { res.ok() })
5013 .map(move |event| {
5014 let c = client_clone.clone();
5015 db::spawn_bound(async move {
5016 event_handler::prepare_event(event, &c, my_pk).await
5017 })
5018 })
5019 .buffer_unordered(8);
5020 tokio::pin!(prepared_stream);
5021
5022 while let Some(result) = prepared_stream.next().await {
5023 total_events += 1;
5024 if let Ok(prepared) = result {
5025 if event_handler::commit_prepared_event(prepared, false, handler).await {
5026 new_messages += 1;
5027 }
5028 }
5029 }
5030 }
5031 Err(e) => log_warn!("[SyncDMs] Batch fetch error: {}", e),
5032 }
5033 }
5034
5035 log_info!("[SyncDMs] Complete: {} events processed, {} new messages", total_events, new_messages);
5036 Ok((total_events, new_messages))
5037 })
5038 .await
5039 }
5040
5041 pub async fn subscribe_dms(&self) -> Result<nostr_sdk::prelude::SubscriptionId> {
5050 use nostr_sdk::prelude::*;
5051 let client = state::nostr_client()
5052 .ok_or(VectorError::Other("Not connected".into()))?;
5053 let my_pk = state::my_public_key()
5054 .ok_or(VectorError::Other("Not logged in".into()))?;
5055
5056 let filter = Filter::new()
5057 .pubkey(my_pk)
5058 .kind(Kind::GiftWrap)
5059 .limit(0);
5060
5061 let output = client.subscribe(filter).await
5062 .map_err(|e| VectorError::Nostr(e.to_string()))?;
5063 Ok(output.value)
5064 }
5065
5066 pub async fn sync_communities(&self) -> Result<Vec<String>> {
5081 let mut all_warnings: Vec<String> = Vec::new();
5082 {
5086 use crate::community::{transport::LiveTransport, v2::service as v2};
5087 let bootstrap: Vec<String> = match crate::state::nostr_client() {
5088 Some(client) => client.relays().await.keys().map(|r| r.to_string()).collect(),
5089 None => Vec::new(),
5090 };
5091 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
5092 if let Ok(outcome) = v2::sync_community_list(&transport, &bootstrap).await {
5093 let joined = outcome.joined;
5096 for c in &joined {
5097 if community::v2::realtime::follow_worker_running() {
5098 community::v2::realtime::enqueue_follow(c.id());
5099 } else {
5100 let cid_hex = crate::simd::hex::bytes_to_hex_32(&c.id().0);
5101 all_warnings.extend(
5102 Self::v2_inline_follow(c.id()).await.into_iter().map(|w| format!("{}: {w}", &cid_hex[..8])),
5103 );
5104 }
5105 }
5106 if !joined.is_empty() {
5107 if let Some(client) = crate::state::nostr_client() {
5108 community::v2::realtime::refresh_subscription(&client).await;
5109 }
5110 }
5111 }
5112 }
5113
5114 let ids = db::community::list_community_ids().map_err(VectorError::from)?;
5115 for id in ids {
5116 if matches!(db::community::community_protocol(&id).ok().flatten(), Some(crate::community::ConcordProtocol::V2)) {
5117 if community::v2::realtime::follow_worker_running() {
5120 community::v2::realtime::enqueue_follow(&id);
5121 } else {
5122 let cid_hex = crate::simd::hex::bytes_to_hex_32(&id.0);
5123 all_warnings.extend(
5124 Self::v2_inline_follow(&id).await.into_iter().map(|w| format!("{}: {w}", &cid_hex[..8])),
5125 );
5126 }
5127 if let Ok(Some(c)) = db::community::load_community_v2(&id) {
5134 for ch in &c.channels {
5135 let hex = crate::simd::hex::bytes_to_hex_32(&ch.id.0);
5136 let _ = Self::v2_backfill_channel(
5137 &id, &hex, 50, 2, None, None,
5138 crate::community::transport::Evidence::Fast, 12,
5139 ).await;
5140 }
5141 }
5142 continue;
5143 }
5144 if let Ok(Some(community)) = db::community::load_community(&id) {
5145 for ch in &community.channels {
5146 let _ = self.sync_community_channel(&ch.id.to_hex(), 50).await;
5147 }
5148 }
5149 }
5150 Ok(all_warnings)
5151 }
5152
5153
5154 pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
5186 use nostr_sdk::prelude::*;
5187
5188 let client = state::nostr_client()
5189 .ok_or(VectorError::Other("Not connected".into()))?;
5190 let my_pk = state::my_public_key()
5191 .ok_or(VectorError::Other("Not logged in".into()))?;
5192
5193 community::v2::streamauth::ensure_responder(&client);
5200
5201 community::v2::realtime::spawn_follow_worker(handler.clone());
5210 let _ = self.sync_communities().await;
5211 let _ = self.sync_dms(None, &NoOpEventHandler).await;
5212
5213 let dm_sub_id = self.subscribe_dms().await?;
5216 community::realtime::refresh_subscription(&client).await;
5217 community::v2::realtime::refresh_subscription(&client).await;
5218
5219 handler.on_subscription_ready(db::community::list_community_ids().map(|v| v.len()).unwrap_or(0));
5225
5226 if let Some(monitor) = client.monitor() {
5233 let mut rx = monitor.subscribe();
5234 db::spawn_bound(async move {
5235 let mut last_resync: Option<std::time::Instant> = None;
5238 while let Ok(notification) = rx.recv().await {
5239 let MonitorNotification::StatusChanged { status, .. } = notification;
5240 if status == RelayStatus::Connected {
5241 if last_resync.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(3)) {
5242 continue;
5243 }
5244 let _ = VectorCore.sync_communities().await;
5245 let _ = VectorCore.sync_dms(None, &NoOpEventHandler).await;
5246 if let Some(c) = state::nostr_client() {
5247 community::realtime::refresh_subscription(&c).await;
5248 community::v2::realtime::refresh_subscription(&c).await;
5249 }
5250 last_resync = Some(std::time::Instant::now());
5251 }
5252 }
5253 });
5254 }
5255
5256 {
5260 let client_health = client.clone();
5261 db::spawn_bound(async move {
5262 tokio::time::sleep(std::time::Duration::from_secs(30)).await; loop {
5264 for (url, relay) in client_health.relays().await {
5265 match relay.status() {
5266 RelayStatus::Connected => {
5267 let probe = tokio::time::timeout(
5268 std::time::Duration::from_secs(10),
5269 client_health
5270 .fetch_events(nostr_sdk::prelude::ReqTarget::single(
5271 url.to_string(),
5272 [Filter::new().kind(Kind::Metadata).limit(1)],
5273 ))
5274 .timeout(std::time::Duration::from_secs(8)),
5275 )
5276 .await;
5277 if !matches!(probe, Ok(Ok(_))) {
5278 let _ = relay.disconnect();
5279 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
5280 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
5281 }
5282 }
5283 RelayStatus::Terminated | RelayStatus::Disconnected => {
5284 let _ = relay.try_connect().timeout(crate::relay_connect_timeout(std::time::Duration::from_secs(10))).await;
5285 }
5286 _ => {}
5287 }
5288 }
5289 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
5290 }
5291 });
5292 }
5293
5294 let client_for_closure = client.clone();
5295
5296 let mut notifications = client.notifications();
5299 while let Some(notification) = notifications.next().await {
5300 let handler = handler.clone();
5301 let c = client_for_closure.clone();
5302 let dm_sid = dm_sub_id.clone();
5303 {
5304 if let nostr_sdk::prelude::ClientNotification::Message { message, .. } = ¬ification {
5308 if let nostr_sdk::prelude::RelayMessage::Ok { event_id, status, .. } = &**message {
5309 sending::note_relay_ok(event_id, *status);
5310 }
5311 }
5312 if let nostr_sdk::prelude::ClientNotification::Event { event, subscription_id, .. } = notification {
5313 if subscription_id == dm_sid {
5314 let prepared = event_handler::prepare_event(*event, &c, my_pk).await;
5316 event_handler::commit_prepared_event(prepared, true, &*handler).await;
5317 } else if community::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
5318 || community::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
5319 {
5320 community::realtime::dispatch_event(*event, handler.clone()).await;
5324 } else if community::v2::realtime::subscription_id().await.as_ref() == Some(&subscription_id)
5325 || community::v2::realtime::poolwide_subscription_id().await.as_ref() == Some(&subscription_id)
5326 {
5327 community::v2::realtime::dispatch_event(*event, handler.clone()).await;
5329 }
5330 }
5331 }
5332 }
5333
5334 Ok(())
5335 }
5336
5337 pub async fn logout(&self) {
5339 if let Some(client) = state::nostr_client() {
5340 let _ = client.disconnect().await;
5341 }
5342 db::close_database();
5343 }
5344
5345 pub async fn swap_session(&self) {
5353 state::clear_message_tombstones();
5355
5356 if let Some(client) = state::take_nostr_client() {
5359 let _ = client.shutdown().await;
5360 }
5361 db::close_database();
5362
5363 state::ENCRYPTION_KEY.clear(&[&state::MY_SECRET_KEY]);
5365 state::MY_SECRET_KEY.clear(&[&state::ENCRYPTION_KEY]);
5366 {
5367 use zeroize::Zeroize;
5368 if let Ok(mut g) = state::MNEMONIC_SEED.lock() {
5369 if let Some(s) = g.as_mut() { s.zeroize(); }
5370 *g = None;
5371 }
5372 if let Ok(mut g) = state::PENDING_NSEC.lock() {
5373 if let Some(s) = g.as_mut() { s.zeroize(); }
5374 *g = None;
5375 }
5376 }
5377
5378 crate::community::realtime::clear().await;
5386 crate::community::v2::realtime::clear().await;
5387 crate::community::transport::clear_plane_pool();
5390 }
5391}
5392
5393#[cfg(all(test, feature = "tor", not(target_arch = "wasm32")))]
5394mod transport_policy_tests {
5395 use std::time::Duration;
5396
5397 #[test]
5400 fn tor_transport_policy() {
5401 let short = Duration::from_secs(5);
5402 let long = Duration::from_secs(300);
5403
5404 crate::tor::set_tor_enabled_pref(false);
5407 assert_eq!(super::tor_proxy_target(), None);
5408 assert_eq!(super::relay_connect_timeout(short), short);
5409 assert_eq!(super::relay_request_timeout(short), short);
5410
5411 crate::tor::set_tor_enabled_pref(true);
5415 assert!(matches!(
5416 crate::tor::transport_state(),
5417 crate::tor::TorTransportState::RequiredButInactive
5418 ));
5419 assert_eq!(
5425 super::tor_proxy_target(),
5426 Some(crate::tor::blackhole_proxy_addr()),
5427 "Tor enabled but inactive must blackhole, never connect direct"
5428 );
5429 assert_eq!(super::relay_connect_timeout(short), super::TOR_RELAY_CONNECT_FLOOR);
5430 assert_eq!(super::relay_request_timeout(short), super::TOR_RELAY_REQUEST_FLOOR);
5431
5432 for tor in [true, false] {
5435 crate::tor::set_tor_enabled_pref(tor);
5436 assert_eq!(super::relay_connect_timeout(long), long, "connect, tor={tor}");
5437 assert_eq!(super::relay_request_timeout(long), long, "request, tor={tor}");
5438 }
5439 }
5440}
5441
5442#[cfg(test)]
5443mod facade_tests {
5444 use super::*;
5445
5446 #[tokio::test]
5449 async fn download_attachment_rejects_private_url() {
5450 let att = crate::types::Attachment {
5451 url: "http://169.254.169.254/latest/meta-data/".to_string(),
5452 ..Default::default()
5453 };
5454 match VectorCore.download_attachment(&att).await {
5455 Err(VectorError::Other(msg)) => {
5456 assert!(msg.contains("Private/internal"), "expected SSRF rejection, got: {msg}")
5457 }
5458 other => panic!("expected SSRF rejection, got {other:?}"),
5459 }
5460 }
5461
5462 #[tokio::test]
5463 async fn download_attachment_rejects_empty_url() {
5464 let att = crate::types::Attachment::default();
5465 assert!(VectorCore.download_attachment(&att).await.is_err());
5466 }
5467
5468 #[tokio::test]
5472 async fn list_communities_and_channel_routing_are_protocol_aware() {
5473 use crate::community::transport::memory::MemoryRelay;
5474 use nostr_sdk::prelude::Keys;
5475
5476 let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
5477 crate::db::close_database();
5478 crate::db::clear_id_caches();
5479 let tmp = tempfile::tempdir().unwrap();
5480 let acct = {
5482 const B: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
5483 let mut s = String::from("npub1");
5484 for i in 0..58 {
5485 s.push(B[(i * 7 + 3) % 32] as char);
5486 }
5487 s
5488 };
5489 std::fs::create_dir_all(tmp.path().join(&acct)).unwrap();
5490 crate::db::set_app_data_dir(crate::db::shared_test_data_dir().to_path_buf());
5491 crate::db::set_current_account(acct.clone()).unwrap();
5492 crate::db::init_database(&acct).unwrap();
5493 let _ = crate::state::take_nostr_client();
5494 let me = Keys::generate();
5495 crate::state::MY_SECRET_KEY.store_from_keys(&me, &[]);
5496 crate::state::set_my_public_key(me.public_key());
5497
5498 let relay = MemoryRelay::new();
5500 let community = crate::community::v2::service::create_community(&relay, "V2 Guild", vec!["wss://r".into()], None)
5501 .await
5502 .unwrap();
5503 let channel_hex = crate::simd::hex::bytes_to_hex_32(&community.channels[0].id.0);
5504
5505 let listed = VectorCore.list_communities().await;
5507 let v2 = listed.iter().find(|c| c["version"] == 2).expect("the v2 community is listed");
5508 assert_eq!(v2["name"], "V2 Guild");
5509 assert_eq!(v2["is_owner"], true);
5510 assert_eq!(v2["channels"][0]["channel_id"], channel_hex);
5511
5512 assert_eq!(
5514 VectorCore.v2_community_for_channel(&channel_hex).unwrap(),
5515 Some(community.identity.community_id),
5516 "a v2 channel is routed to v2"
5517 );
5518 assert_eq!(VectorCore.v2_community_for_channel(&"00".repeat(32)).unwrap(), None);
5520 }
5521
5522 #[test]
5527 fn v2_invite_url_base_derivation_round_trips() {
5528 use crate::community::v2::derive::TOKEN_LEN;
5529 use crate::community::v2::invite::{build_invite_url, parse_invite_link};
5530 use nostr_sdk::prelude::Keys;
5531 let base = crate::community::public_invite::INVITE_URL_BASE.trim_end_matches("/invite");
5532 assert!(!base.ends_with("/invite"), "the bare domain must not carry /invite");
5533 let signer = Keys::generate();
5534 let token = [0x07u8; TOKEN_LEN];
5535 let url = build_invite_url(base, &signer.public_key(), &token, &[]).unwrap();
5536 assert!(url.contains("/invite/"), "a v2 URL carries the naddr path");
5537 assert!(!url.contains("/invite/invite/"), "no doubled /invite from the base");
5538 let parsed = parse_invite_link(&url).unwrap();
5539 assert_eq!(parsed.link_signer, signer.public_key());
5540 assert_eq!(parsed.token, token);
5541 }
5542}
5543
5544#[cfg(test)]
5545mod history_paging_tests {
5546 use super::*;
5547
5548 fn msg(at: u64, id_byte: u8, content: &str) -> Message {
5549 Message {
5550 id: format!("{:02x}", id_byte).repeat(32),
5551 content: content.to_string(),
5552 at,
5553 ..Default::default()
5554 }
5555 }
5556
5557 #[tokio::test]
5561 async fn history_pages_through_a_same_ms_wall_and_a_deleted_cursor() {
5562 let chat_id = "test-history-paging-wall";
5563 {
5564 let mut st = state::STATE.lock().await;
5565 st.ensure_community_chat(chat_id);
5566 for m in [msg(500, 0x01, "old"), msg(900, 0xaa, "wall-a"), msg(900, 0xbb, "wall-b"), msg(900, 0xcc, "wall-c")] {
5568 st.add_message_to_chat(chat_id, &m);
5569 }
5570 }
5571 let core = VectorCore;
5572
5573 let newest = core.get_messages_before(chat_id, None, 2).await;
5575 assert_eq!(newest.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["wall-b", "wall-c"]);
5576
5577 let cursor = (newest[0].at, newest[0].id.as_str().to_string());
5579 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
5580 assert_eq!(page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), ["old", "wall-a"]);
5581
5582 {
5585 let mut st = state::STATE.lock().await;
5586 let chat = st.get_chat_mut(chat_id).unwrap();
5587 chat.messages.remove_by_hex_id(&cursor.1);
5588 }
5589 let page = core.get_messages_before(chat_id, Some((cursor.0, &cursor.1)), 10).await;
5590 assert_eq!(
5591 page.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(),
5592 ["old", "wall-a"],
5593 "a deleted cursor pages identically"
5594 );
5595
5596 assert!(core.get_messages_before("test-history-paging-nochat", None, 5).await.is_empty());
5598 }
5599}
5600
5601#[cfg(test)]
5602mod moderation_cache_audit {
5603 #[test]
5609 fn every_moderation_action_drops_the_memoised_verdict() {
5610 let src = include_str!("lib.rs");
5611 for name in [
5612 "pub async fn refound_community",
5613 "pub async fn kick_community_members",
5614 "pub async fn set_members_banned",
5615 "pub async fn contain_raid",
5616 ] {
5617 let start = src.find(name).unwrap_or_else(|| panic!("{name} not found — rename?"));
5618 let rest = &src[start + name.len()..];
5620 let end = rest.find("\n pub ").unwrap_or(rest.len());
5621 assert!(
5622 rest[..end].contains("invalidate_raid_report"),
5623 "{name} mutates membership but never drops the memoised console verdict"
5624 );
5625 }
5626 }
5627
5628 #[test]
5633 fn an_adopted_change_drops_the_verdict_too() {
5634 let src = include_str!("db/community.rs");
5635 for name in ["pub fn set_community_banlist", "pub fn set_community_roles", "pub fn set_guestbook"] {
5636 let start = src.find(name).unwrap_or_else(|| panic!("{name} not found — rename?"));
5637 let rest = &src[start + name.len()..];
5638 let end = rest.find("\npub fn ").unwrap_or(rest.len());
5639 assert!(
5640 rest[..end].contains("forget_console_verdict"),
5641 "{name} changes who the console reports on but leaves the memo standing"
5642 );
5643 }
5644 }
5645}