pub struct VectorCore;Expand description
The main entry point for Vector Core.
Provides a high-level API for all Vector operations. Internally uses global state (same pattern as the Tauri backend) for compatibility.
use vector_core::{VectorCore, CoreConfig};
use std::path::PathBuf;
let core = VectorCore::init(CoreConfig {
data_dir: PathBuf::from("/tmp/vector-data"),
event_emitter: None,
})?;
// Login with nsec
let result = core.login("nsec1...", None).await?;
println!("Logged in as {}", result.npub);Implementations§
Source§impl VectorCore
impl VectorCore
Sourcepub fn init(config: CoreConfig) -> Result<Self>
pub fn init(config: CoreConfig) -> Result<Self>
Initialize Vector Core with the given configuration.
Sourcepub async fn login(
&self,
key: &str,
password: Option<&str>,
) -> Result<LoginResult>
pub async fn login( &self, key: &str, password: Option<&str>, ) -> Result<LoginResult>
Login with an nsec key or mnemonic seed phrase.
Sourcepub fn generate_nsec(&self) -> Result<String>
pub fn generate_nsec(&self) -> Result<String>
Generate a fresh random account secret key (bech32 nsec). Lets a headless client spin up a
brand-new identity (add_account with no key) without depending on nostr-sdk directly.
Sourcepub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<SendResult>
pub async fn send_dm(&self, to_npub: &str, content: &str) -> Result<SendResult>
Send a NIP-17 gift-wrapped text DM using the full pipeline. Retries a
transient publish miss (headless preset, 3 attempts) so an SDK/CLI bot rides
out a relay blip instead of silently dropping the message on the first miss;
self_send: false keeps it a plain send (no inbox self-copy).
Sourcepub async fn send_dm_reply(
&self,
to_npub: &str,
replied_to: &str,
content: &str,
) -> Result<SendResult>
pub async fn send_dm_reply( &self, to_npub: &str, replied_to: &str, content: &str, ) -> Result<SendResult>
Send a DM as a threaded reply to replied_to (an existing message’s event id).
Sourcepub async fn download_attachment(
&self,
attachment: &Attachment,
) -> Result<Vec<u8>>
pub async fn download_attachment( &self, attachment: &Attachment, ) -> Result<Vec<u8>>
Download a received attachment and decrypt it to plaintext bytes. Fetches the encrypted blob
from its Blossom URL (SSRF/Tor-aware client, size-capped) and AES-decrypts with the
attachment’s embedded key + nonce. Walks the primary URL then any BUD-04 fallback
mirrors (same ciphertext on other hosts) until one serves. Prefer
download_attachment_from when the message author is
known — it adds the BUD-03 hash-swap over the author’s advertised servers.
Sourcepub async fn download_attachment_from(
&self,
attachment: &Attachment,
author_npub: Option<&str>,
) -> Result<Vec<u8>>
pub async fn download_attachment_from( &self, attachment: &Attachment, author_npub: Option<&str>, ) -> Result<Vec<u8>>
download_attachment with the full source walk: primary URL →
embedded fallback mirrors → BUD-03 hash-swap (the same content-address on each of the
author’s kind-10063 servers). author_npub is the message author (your own npub for your
own messages); None skips the hash-swap stage.
Sourcepub async fn download_attachment_within(
&self,
attachment: &Attachment,
author_npub: Option<&str>,
max_bytes: usize,
) -> Result<Vec<u8>, DownloadError>
pub async fn download_attachment_within( &self, attachment: &Attachment, author_npub: Option<&str>, max_bytes: usize, ) -> Result<Vec<u8>, DownloadError>
download_attachment_from, refusing more
than max_bytes of ciphertext: the size an attachment declares is the
sender’s word, so the cap holds on the bytes actually received.
Sourcepub async fn send_file(
&self,
to_npub: &str,
file_path: &str,
) -> Result<SendResult>
pub async fn send_file( &self, to_npub: &str, file_path: &str, ) -> Result<SendResult>
Send a NIP-17 gift-wrapped file attachment DM.
Sourcepub async fn send_reaction(
&self,
to_npub: &str,
reference_id: &str,
emoji: &str,
emoji_url: Option<&str>,
) -> Result<String>
pub async fn send_reaction( &self, to_npub: &str, reference_id: &str, emoji: &str, emoji_url: Option<&str>, ) -> Result<String>
Send a NIP-25 reaction to a DM message. emoji_url carries the NIP-30
image URL when reacting with a custom-pack emoji (content stays
:shortcode:). Returns the reaction’s rumor id. Local echo + persistence
are best-effort — the gift-wrap send is the source of truth.
Sourcepub async fn send_typing(&self, to_npub: &str) -> Result<()>
pub async fn send_typing(&self, to_npub: &str) -> Result<()>
Send an ephemeral typing indicator to a DM recipient. Fire-and-forget with a 30-second NIP-40 expiry so relays purge it quickly.
Sourcepub async fn edit_dm(
&self,
to_npub: &str,
message_id: &str,
new_content: &str,
) -> Result<String>
pub async fn edit_dm( &self, to_npub: &str, message_id: &str, new_content: &str, ) -> Result<String>
Edit a DM you previously sent (kind-16 edit) with an optimistic local echo. Returns the edit event id. Persistence is best-effort and only happens when the chat already exists locally.
Sourcepub async fn delete_dm(&self, message_id: &str) -> Result<DeleteOutcome>
pub async fn delete_dm(&self, message_id: &str) -> Result<DeleteOutcome>
Delete a DM you sent (NIP-09 over the retained gift-wrap keys).
Sourcepub async fn get_chats(&self) -> Vec<SerializableChat>
pub async fn get_chats(&self) -> Vec<SerializableChat>
Get chats from the in-memory state.
Sourcepub async fn get_messages(
&self,
chat_id: &str,
limit: usize,
offset: usize,
) -> Vec<Message>
pub async fn get_messages( &self, chat_id: &str, limit: usize, offset: usize, ) -> Vec<Message>
Get messages for a chat (paginated).
Sourcepub async fn get_message(&self, message_id: &str) -> Option<(String, Message)>
pub async fn get_message(&self, message_id: &str) -> Option<(String, Message)>
One message by id, with the chat it was said in. Reads STATE first, then the store for a row that has paged out of the loaded window.
This is what a moderation citation needs: it names a message id and
nothing else, so the room has to come back with it. None is an
ordinary answer — a citation outlives its message (a delete, a hide, an
expiry, history never synced), and callers must read absence as absence
rather than as a fault.
Sourcepub async fn get_messages_with_attachment(
&self,
content_hash: &str,
) -> Vec<(String, Message)>
pub async fn get_messages_with_attachment( &self, content_hash: &str, ) -> Vec<(String, Message)>
Every message carrying an attachment with this content hash, newest
first. Attachment.id IS that hash, and so is a citation’s
content_hash, so a classifier verdict about a blob resolves straight
back to what carried it.
A list, not one answer: one blob rides many messages — a forward, a re-post, or forty accounts posting the same image, which is exactly the case worth being able to see whole.
Sourcepub async fn get_messages_before(
&self,
chat_id: &str,
before: Option<(u64, &str)>,
limit: usize,
) -> Vec<Message>
pub async fn get_messages_before( &self, chat_id: &str, before: Option<(u64, &str)>, limit: usize, ) -> Vec<Message>
Cursor-paged local read: up to limit messages strictly before the
(at_ms, id) cursor, chronological. None returns the newest limit.
The cursor is compared BY VALUE, never resolved to a stored row, so it keeps working after its message is gone (NIP-09 delete, moderation hide, a self-destruct timer) — a persisted cursor must not wedge the walk. The id tiebreaks inside a same-millisecond burst, giving a total order that can’t skip or re-serve across pages. Local only: sync older history in from relays first.
Sourcepub async fn get_profile(&self, npub: &str) -> Option<SlimProfile>
pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile>
Get a profile by npub.
Sourcepub async fn load_profile(&self, npub: &str) -> bool
pub async fn load_profile(&self, npub: &str) -> bool
Fetch a profile’s metadata and status from relays.
Sourcepub async fn update_profile(
&self,
name: &str,
avatar: &str,
banner: &str,
about: &str,
) -> bool
pub async fn update_profile( &self, name: &str, avatar: &str, banner: &str, about: &str, ) -> bool
Update the current user’s profile metadata and broadcast to relays.
Sourcepub async fn update_bot_profile(
&self,
name: &str,
avatar: &str,
banner: &str,
about: &str,
) -> bool
pub async fn update_bot_profile( &self, name: &str, avatar: &str, banner: &str, about: &str, ) -> bool
Like update_profile but marks the profile as a bot (bot: true in
the metadata). The SDK uses this for every bot; build human clients on update_profile.
Sourcepub async fn update_status(&self, status: &str) -> bool
pub async fn update_status(&self, status: &str) -> bool
Update the current user’s status and broadcast to relays.
Sourcepub async fn upload_public_image(&self, file_path: &str) -> Result<String>
pub async fn upload_public_image(&self, file_path: &str) -> Result<String>
Upload an image file to Blossom unencrypted and return its public URL — for avatars,
banners, and other images other clients must fetch directly. (The opposite of
send_file’s encrypted attachments.) Pass the URL to update_profile.
Sourcepub async fn block_user(&self, npub: &str) -> bool
pub async fn block_user(&self, npub: &str) -> bool
Block a user by npub.
Sourcepub async fn unblock_user(&self, npub: &str) -> bool
pub async fn unblock_user(&self, npub: &str) -> bool
Unblock a user by npub.
Sourcepub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool
pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool
Set a nickname for a profile.
Sourcepub async fn get_blocked_users(&self) -> Vec<SlimProfile>
pub async fn get_blocked_users(&self) -> Vec<SlimProfile>
Get all blocked profiles.
Sourcepub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority)
pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority)
Queue a profile for background sync.
Sourcepub async fn list_communities(&self) -> Vec<Value>
pub async fn list_communities(&self) -> Vec<Value>
List every Community held locally (owned or joined), each with its channels.
Sourcepub async fn create_community_v2(&self, name: &str) -> Result<Value>
pub async fn create_community_v2(&self, name: &str) -> Result<Value>
Create a fresh Concord v2 community owned by the local identity (the
SDK’s default; the GUI’s create_community stays v1 during the migration
window). Mints the self-certifying id + genesis, persists, publishes, and
registers each channel as a chat. Returns a version: 2 JSON summary.
Sourcepub async fn register_v2_chats(
&self,
community: &CommunityV2,
__session: &Arc<Session>,
)
pub async fn register_v2_chats( &self, community: &CommunityV2, __session: &Arc<Session>, )
Register each of a v2 community’s channels as a chat row (so it surfaces in
the chat list / communities()), mirroring the v1 create path. session
is captured by the caller BEFORE its network I/O, so this STATE write is
skipped if the account swapped mid-flight (else we’d write A’s community
into B’s in-memory chats).
Source§impl VectorCore
impl VectorCore
Sourcepub async fn join_community(&self, invite_url: &str) -> Result<Value>
pub async fn join_community(&self, invite_url: &str) -> Result<Value>
Join a Community from a public invite URL (vectorapp.io/invite#...). Fetches the
token-encrypted bundle, persists the member-view Community, and registers its channels
as chats. Returns a JSON summary.
Sourcepub fn list_pending_invites(&self) -> Result<Vec<Value>>
pub fn list_pending_invites(&self) -> Result<Vec<Value>>
List the parked private invites (giftwrapped) awaiting acceptance. Each entry is the community id, its name (from the stored bundle), and the inviter’s npub.
Sourcepub async fn accept_pending_invite(&self, community_id: &str) -> Result<Value>
pub async fn accept_pending_invite(&self, community_id: &str) -> Result<Value>
Accept a PARKED private invite by community id: rebuild the member-view Community from the stored bundle, finalize the join exactly like a public link, then drop the pending row. Mirrors the desktop’s consent-then-join for an invite delivered over a gift wrap.
Sourcepub async fn create_channel(
&self,
community_id: &str,
name: &str,
private: bool,
) -> Result<String>
pub async fn create_channel( &self, community_id: &str, name: &str, private: bool, ) -> Result<String>
Create a channel. A private one mints its own key plus the channel-scoped
access Role that is its access list (CORD-03/04); grant that role with
grant_channel_access to let someone read it.
Returns the new channel id (32-byte hex).
Sourcepub async fn rename_channel(
&self,
community_id: &str,
channel_id: &str,
name: &str,
) -> Result<()>
pub async fn rename_channel( &self, community_id: &str, channel_id: &str, name: &str, ) -> Result<()>
Rename a channel (a versioned edit of the same entity, so its history and id survive). Other folded fields are carried through untouched.
Sourcepub async fn delete_channel(
&self,
community_id: &str,
channel_id: &str,
) -> Result<()>
pub async fn delete_channel( &self, community_id: &str, channel_id: &str, ) -> Result<()>
Tombstone a channel. Deletion is terminal and the id is never reused; history stays readable to anyone who already holds its keys.
Sourcepub async fn grant_channel_access(
&self,
community_id: &str,
channel_id: &str,
npub: &str,
) -> Result<()>
pub async fn grant_channel_access( &self, community_id: &str, channel_id: &str, npub: &str, ) -> Result<()>
Grant npub read access to a private channel: adds the channel’s access
role and vends the key to them (CORD-03 “delivered on grant”).
Sourcepub async fn revoke_channel_access(
&self,
community_id: &str,
channel_id: &str,
npub: &str,
) -> Result<()>
pub async fn revoke_channel_access( &self, community_id: &str, channel_id: &str, npub: &str, ) -> Result<()>
Revoke npub’s read access: drops the access role and rekeys the channel
so the removal actually severs them (CORD-06). They keep whatever history
they already read — a rekey protects the future, never the past.
Sourcepub fn member_can_read_channel(
&self,
community_id: &str,
channel_id: &str,
npub: &str,
) -> Result<bool>
pub fn member_can_read_channel( &self, community_id: &str, channel_id: &str, npub: &str, ) -> Result<bool>
Who may read a private channel: the channel-scoped Roles that are its access list (CORD-04 §2) and the members holding them.
Sync read off the folded roster. members is exactly the access-role
holders; owner is reported alongside because the owner is entitled
whether or not they hold one (a channel’s creator is granted the role, so
an owner-created channel lists them in both).
Whether npub may READ channel_id here.
A PUBLIC channel is readable by the community; a PRIVATE one only by the holders of one of its channel-scoped access roles (CORD-03), plus the owner. Community-level power is NOT the question and must not be substituted for it: BAN says what someone may do to people, never which rooms they may see, and an access role commonly carries no powers at all.
The reason this exists: a moderation report quotes what was said. Sending one built from a private channel to a moderator who holds no key there would launder the content out of the room, using the bot as the bypass — so anything that forwards evidence asks this first.
Fails closed. A banned member reads nothing, and an unknown channel is an error rather than a permissive default.
pub fn channel_access( &self, community_id: &str, channel_id: &str, ) -> Result<Value>
Sourcepub async fn create_public_invite(
&self,
community_id: &str,
expires_at_ms: Option<u64>,
label: Option<String>,
) -> Result<String>
pub async fn create_public_invite( &self, community_id: &str, expires_at_ms: Option<u64>, label: Option<String>, ) -> Result<String>
Mint a public invite link for a Community this identity owns. Returns the shareable URL.
expires_at_ms is an ABSOLUTE unix timestamp in MILLISECONDS (v2’s native unit and the
InviteEntry wire’s); the v1 path takes seconds, so it is converted here rather than at
each call site. label is the attribution bucket shown as “joined via
Sourcepub async fn invite_to_community(
&self,
community_id: &str,
invitee_npub: &str,
) -> Result<Value>
pub async fn invite_to_community( &self, community_id: &str, invitee_npub: &str, ) -> Result<Value>
Send a PRIVATE invite: gift-wrap this Community’s invite bundle directly to an npub over a NIP-17 DM (the same transport as a regular DM). The invitee parks it pending consent (accept_pending_invite). Requires CREATE_INVITE; a banned npub can’t be re-invited. Returns the wrap’s event id + relays.
Sourcepub fn list_public_invites(
&self,
community_id: &str,
) -> Result<Vec<PublicInviteRecord>>
pub fn list_public_invites( &self, community_id: &str, ) -> Result<Vec<PublicInviteRecord>>
The public invite links this account minted for a Community (to list + revoke). Each carries
the hex token (the link secret) needed by Self::revoke_public_invite. A local read for
both protocols — links minted on this device (a v2 mint also syncs the cross-device 13303
record; v2 join_count is not yet tracked and is always 0).
Sourcepub async fn revoke_public_invite(
&self,
community_id: &str,
token: &str,
) -> Result<()>
pub async fn revoke_public_invite( &self, community_id: &str, token: &str, ) -> Result<()>
Revoke a public invite link by its hex token. Retiring the LAST active link flips the Community to Private, which re-founds (rotates the base key + every channel key) to cut link-joined lurkers. Idempotent: a token this account doesn’t hold is a no-op. Needs a local key when the revoke triggers the privatize rekey (a bunker account can’t rotate).
Sourcepub async fn send_community_message(
&self,
channel_id: &str,
content: &str,
replied_to: Option<&str>,
) -> Result<String>
pub async fn send_community_message( &self, channel_id: &str, content: &str, replied_to: Option<&str>, ) -> Result<String>
Post a text message to a Community channel. Returns the message id (the inner id).
Sourcepub async fn send_community_message_expiring(
&self,
channel_id: &str,
content: &str,
replied_to: Option<&str>,
expires_in_secs: Option<u64>,
) -> Result<String>
pub async fn send_community_message_expiring( &self, channel_id: &str, content: &str, replied_to: Option<&str>, expires_in_secs: Option<u64>, ) -> Result<String>
Post a message that deletes itself after expires_in_secs.
A NIP-40 expiration tag on the rumor, which is what relays drop on and
every member’s client purges against — the same mechanism as the
per-channel Self-Destruct Timer, decided per message instead of per
channel. For a bot posting a public moderation notice: the warning is
seen, then it stops being a permanent monument to somebody’s worst day.
v2 only. A v1 community has no expiry, and posting a permanent message where a vanishing one was asked for is the kind of surprise that ends up in a screenshot, so it refuses rather than silently keeping it forever.
Sourcepub async fn send_community_file(
&self,
channel_id: &str,
file_path: &str,
) -> Result<String>
pub async fn send_community_file( &self, channel_id: &str, file_path: &str, ) -> Result<String>
Send a file to a Community channel as an encrypted attachment. Returns the message id.
Mirrors the DM file pipeline (encrypt → Blossom upload → NIP-92 imeta) but publishes
over the community transport.
Sourcepub async fn send_community_typing(&self, channel_id: &str) -> Result<()>
pub async fn send_community_typing(&self, channel_id: &str) -> Result<()>
Send an ephemeral typing indicator to a Community channel.
Sourcepub async fn send_community_reaction(
&self,
channel_id: &str,
message_id: &str,
emoji: &str,
emoji_url: Option<&str>,
) -> Result<()>
pub async fn send_community_reaction( &self, channel_id: &str, message_id: &str, emoji: &str, emoji_url: Option<&str>, ) -> Result<()>
React to a Community message. emoji_url carries the NIP-30 image URL for a custom
:shortcode: reaction (parity with DMs).
Sourcepub async fn edit_community_message(
&self,
channel_id: &str,
message_id: &str,
new_content: &str,
) -> Result<()>
pub async fn edit_community_message( &self, channel_id: &str, message_id: &str, new_content: &str, ) -> Result<()>
Edit one of your own Community messages.
Sourcepub async fn delete_community_message(&self, message_id: &str) -> Result<()>
pub async fn delete_community_message(&self, message_id: &str) -> Result<()>
Delete one of your own Community messages, resolving its channel from local
state (the GUI path). A headless v2 consumer holds no local history — use
Self::delete_community_message_in with the channel id instead.
Sourcepub async fn delete_community_message_in(
&self,
channel_id: &str,
message_id: &str,
) -> Result<()>
pub async fn delete_community_message_in( &self, channel_id: &str, message_id: &str, ) -> Result<()>
Delete one of your own Community messages in channel_id: a NIP-09 relay nuke when the
per-message key is held (v1) or the in-plane kind-5 (v2), plus a cooperative tombstone so
peers hide it, plus best-effort attachment cleanup.
Sourcepub async fn hide_community_message(
&self,
channel_id: &str,
message_id: &str,
) -> Result<()>
pub async fn hide_community_message( &self, channel_id: &str, message_id: &str, ) -> Result<()>
Moderation-hide someone ELSE’s community message under MANAGE_MESSAGES
(CORD-04 §3/§5). Protocol-agnostic: v2 seals the same kind-5 its authors
use, v1 publishes its 3305 tombstone; both re-derive the actor’s authority
from the signed inner against the folded Roster, so this is an authority
claim peers verify, never a local suppression.
Sourcepub async fn sync_community_channel(
&self,
channel_id: &str,
limit: usize,
) -> Result<(usize, Vec<String>)>
pub async fn sync_community_channel( &self, channel_id: &str, limit: usize, ) -> Result<(usize, Vec<String>)>
Catch a Community channel up from relays. v1: fetch + ingest the latest page of messages,
reactions, edits, and deletes, returning how many were brand-new. v2: consensus catch-up
only (rekeys + control refold) — chat history delivers over the live handler bridge, so the
count is always 0. Returns (new_message_count, warnings); warnings are NON-FATAL errors
hit during the sync (catch-up, control fold, read-cut resume) — surfaced rather than
swallowed so a headless caller is never blind to “the sync ran but a re-founding couldn’t
be resumed.”
Sourcepub async fn sync_community_channel_page(
&self,
channel_id: &str,
limit: usize,
before_secs: Option<u64>,
since_secs: Option<u64>,
) -> Result<(BackfillCount, Vec<String>)>
pub async fn sync_community_channel_page( &self, channel_id: &str, limit: usize, before_secs: Option<u64>, since_secs: Option<u64>, ) -> Result<(BackfillCount, Vec<String>)>
One page of a v2 channel catch-up: what the relay served, and how much of it this client had never seen.
Sourcepub async fn sync_community_channel_before(
&self,
channel_id: &str,
limit: usize,
before_secs: Option<u64>,
) -> Result<(usize, Vec<String>)>
pub async fn sync_community_channel_before( &self, channel_id: &str, limit: usize, before_secs: Option<u64>, ) -> Result<(usize, Vec<String>)>
sync_community_channel with a back-paging
cursor: before_secs bounds the fetch to messages at or older than it.
The cursor is what reaches a v2 channel’s PRE-ROTATION history. An uncursored catch-up reads the live epoch only (rotations move the conversation, so that is where it is); each epoch keeps its own plane address, and a bounded fetch spans all of them at once because messages are selected by time, not by epoch.
Sourcepub async fn sync_channel_events(
&self,
channel_id: &str,
max_events: usize,
until_s: Option<u64>,
since_s: Option<u64>,
) -> Result<usize>
pub async fn sync_channel_events( &self, channel_id: &str, max_events: usize, until_s: Option<u64>, since_s: Option<u64>, ) -> Result<usize>
Cursor-paged relay sync: fetch ONE page of up to max_events events from
the channel’s relays (per relay — divergent relays can push the union
higher), ingest it, and return how many MESSAGES were new. A return of 0
means the page held nothing new, not that the channel is empty.
until_s / since_s are unix-seconds bounds (the back-paging cursor and
the time floor). Depth comes from looping, not from a bigger page:
max_events is clamped to 500 (relays clamp theirs anyway) and values
under 50 behave as 50 on v2 (the shared pager’s floor).
Consensus (root rotations, control folds, ban checks) is the FULL sync’s
job — sync_community_channel — so a
hundred-page walk doesn’t re-fold control a hundred times. Run one full
sync (or be listening) before deep walks.
Sourcepub async fn get_chat_commands(&self, chat_id: &str) -> ChatCommandsSnapshot
pub async fn get_chat_commands(&self, chat_id: &str) -> ChatCommandsSnapshot
The composer’s / picker snapshot for chat_id, answered INSTANTLY
from local state: the chat’s bot-flagged members (kind-0 bot: true —
the SDK sets it on every bot it builds) and their last-known manifests
from the persistent store. When the last refresh is older than a minute
(or the bot set changed), ONE background REQ re-fetches every bot’s
manifest together (5s unification window), persists newer editions, and
emits chat_commands_updated — the UI swaps the list in when it lands.
Works for BOTH community protocols (an invocation is plain content; only
the optional routing tag is v2-only) and DMs. The manifest REQ always
includes the discovery indexers beside the chat’s own relays, so an
unreachable or stranger-dropping community relay can’t blind the picker.
Sourcepub async fn get_community_members(&self, community_id: &str) -> Vec<Value>
pub async fn get_community_members(&self, community_id: &str) -> Vec<Value>
Observed members of a Community (best-effort: those who’ve posted or announced a join,
minus anyone who’s left or is banned). v1 entries are {npub, last_active}; a v2 entry
is {npub} (the Complete Memberlist carries no activity time). Best-effort throughout:
a transport failure yields an empty list, never an error.
Sourcepub fn get_community_banned(&self, community_id: &str) -> Vec<String>
pub fn get_community_banned(&self, community_id: &str) -> Vec<String>
A Community’s banlist, as npubs.
The counterpart to Self::set_member_banned, which had none: setting a
ban succeeds identically whether or not one was already in place, so a
caller lifting a ban could not tell whether it changed anything, and
anything reporting on it was guessing.
Read from local state, so it reflects the last banlist edition this client folded rather than a fresh fetch.
Sourcepub fn community_moderation_intel(&self, community_id: &str) -> Result<Value>
pub fn community_moderation_intel(&self, community_id: &str) -> Result<Value>
Everything the moderation panel needs about one Community, read once so the
parts agree with each other: every member scored against the raid evidence
(crate::community::raid), the duplicate-text cohorts behind those verdicts,
the live invite links, and what the caller may actually do about it.
v2 only — the containment this feeds (batch ban, refounding) are v2 verbs.
Sourcepub fn screen_community_message(
&self,
channel_id: &str,
author_npub: &str,
text: &str,
) -> Result<Value>
pub fn screen_community_message( &self, channel_id: &str, author_npub: &str, text: &str, ) -> Result<Value>
Screen ONE message immediately, without waiting for a sweep.
Runs only the stateless rules — words, links, regex, mentions — because those are the only ones a single message can answer. Rate, repetition and cohorts describe a window and stay with the periodic evaluation.
Costs no corpus read and no cache: the whole point is that a word filter should answer in milliseconds, not on the next 90-second tick.
Sourcepub fn check_community_raid(&self, community_id: &str) -> Result<Value>
pub fn check_community_raid(&self, community_id: &str) -> Result<Value>
The raid verdict alone, for the badge on a community header. Same assessment as the panel and served from the same cache, so surfacing the alert costs nothing once and then nothing at all until it expires.
Sourcepub fn policy_presets(&self) -> Result<Value>
pub fn policy_presets(&self) -> Result<Value>
The preset library: what a designer offers instead of a blank document.
Sourcepub fn preview_community_policy(
&self,
community_id: &str,
bytes: &str,
) -> Result<Value>
pub fn preview_community_policy( &self, community_id: &str, bytes: &str, ) -> Result<Value>
What a candidate policy WOULD do, against real history. Stores nothing, publishes nothing, removes nobody — the designer never enables a policy from a form alone, only from a preview that named who it would catch.
Sourcepub fn list_community_policies(&self, community_id: &str) -> Result<Value>
pub fn list_community_policies(&self, community_id: &str) -> Result<Value>
A community’s policies, as stored. Returns the exact bytes, so an editor round-trips what was written rather than a re-serialization of it.
Sourcepub fn set_community_policy(
&self,
community_id: &str,
policy_id: &str,
bytes: &str,
enabled: bool,
) -> Result<Value>
pub fn set_community_policy( &self, community_id: &str, policy_id: &str, bytes: &str, enabled: bool, ) -> Result<Value>
Store a policy, validating FIRST. An invalid policy is a rejected edit, never a stored one that quietly evaluates to nothing.
pub fn delete_community_policy( &self, community_id: &str, policy_id: &str, ) -> Result<()>
Sourcepub fn builtin_policy_json(&self) -> Result<String>
pub fn builtin_policy_json(&self) -> Result<String>
The built-in policy, as editable bytes — the starting point an editor offers instead of a blank document.
Sourcepub fn policy_side_by_side(&self, community_id: &str) -> Result<Value>
pub fn policy_side_by_side(&self, community_id: &str) -> Result<Value>
Run the policy engine beside the shipped assessor and report where they disagree. Diagnostic only: the engine convicts nothing in production until this diff has been read against real data.
Sourcepub fn invalidate_raid_report(community_id: &str)
pub fn invalidate_raid_report(community_id: &str)
Drop a community’s memoised verdict. Every moderation action changes who is a member, so serving the pre-action answer would leave the badge accusing people who are already gone.
Sourcepub async fn refound_community(
&self,
community_id: &str,
retain: &[&str],
) -> Result<()>
pub async fn refound_community( &self, community_id: &str, retain: &[&str], ) -> Result<()>
Rotate the Community’s keys with no banlist edit: mint a new root epoch that
only retain can follow. This is the containment that scales — a wave of
thousands cannot fit in a 500-entry banlist, but it can be locked out of the
next epoch in one publish. retain is a keep-list; everyone else is cut.
Passing an empty retain rotates without removing anyone, which is what you
want when the invite link leaked but the members are all real.
Sourcepub async fn purge_and_refound(
&self,
community_id: &str,
retain: &[&str],
on_progress: &(dyn Fn(usize, usize) + Sync),
) -> Result<Value>
pub async fn purge_and_refound( &self, community_id: &str, retain: &[&str], on_progress: &(dyn Fn(usize, usize) + Sync), ) -> Result<Value>
Rotate the keys around the survivors, then kick the removed off the roster.
The order is load-bearing, and it is ROTATE FIRST. A Guestbook plane is derived per epoch, so kicks published before the rotation land on a plane the rotation immediately buries: every reader then has to receive them in the window before it follows the rekey, and whatever is still in flight is never folded by anyone. Publishing onto the NEW epoch instead puts the departures on the plane every survivor will read from now on, so a client that catches up an hour later still folds them. It also closes the re-admission window — the removed hold no key at the new epoch, so they cannot post their way back into the memberlist between their own kick and the rotation.
The rotation alone would not clear the roster: the guestbook store is keyed by community and appended across epochs, and the memberlist re-admits observed authors from local history regardless of epoch. The kicks are what evict them.
Sourcepub async fn kick_community_members(
&self,
community_id: &str,
npubs: &[&str],
on_progress: &(dyn Fn(usize, usize) + Sync),
) -> Result<Value>
pub async fn kick_community_members( &self, community_id: &str, npubs: &[&str], on_progress: &(dyn Fn(usize, usize) + Sync), ) -> Result<Value>
Kick many members without rotating. Exposed for callers that want the roster cleaned but the keys left alone.
Sourcepub async fn revoke_all_public_invites(
&self,
community_id: &str,
) -> Result<(usize, usize)>
pub async fn revoke_all_public_invites( &self, community_id: &str, ) -> Result<(usize, usize)>
Retire every public invite link this Community holds. A raid arrives through a
link, and revoking them one at a time loses the race — the whole set has to go
in one pass before the rotation, or the next wave walks in through the survivor.
Returns (revoked, failed).
Sourcepub async fn v2_inline_follow(id: &CommunityId) -> Vec<String>
pub async fn v2_inline_follow(id: &CommunityId) -> Vec<String>
One synchronous v2 follow pass — rekeys first (a base adopt moves the control address), then a control refold on the FRESH state, the same order the live follow worker runs. Returns non-fatal warnings. Walk one community’s rekeys + control fold NOW, serialized against the live follow worker by the per-community lock. Public so boot can converge a community the probe saw move BEFORE paging its content — content read at a superseded epoch is wasted work, and may not open at all.
Sourcepub fn community_capabilities(&self, community_id: &str) -> Result<Value>
pub fn community_capabilities(&self, community_id: &str) -> Result<Value>
My effective management capabilities in a community (role engine — owner is just position 0). Use to confirm a promotion/demotion landed. A local read: the roster is folded + persisted by the passive sync (v1) / control follow (v2), never fetched here.
Sourcepub async fn pin_community_message(
&self,
community_id: &str,
channel_id: &str,
message_id: &str,
) -> Result<()>
pub async fn pin_community_message( &self, community_id: &str, channel_id: &str, message_id: &str, ) -> Result<()>
Pin a message in a v2 community channel (CORD-04 §7). The proof is
rebuilt from the message’s recovered wrap; see service::pin_message.
Sourcepub async fn unpin_community_message(
&self,
community_id: &str,
channel_id: &str,
message_id: &str,
) -> Result<()>
pub async fn unpin_community_message( &self, community_id: &str, channel_id: &str, message_id: &str, ) -> Result<()>
Unpin a message: the next Pin List edition without the entry.
Sourcepub async fn fetch_pinned_attachment(
&self,
community_id: &str,
channel_id: &str,
rumor_id: &str,
) -> Result<Value>
pub async fn fetch_pinned_attachment( &self, community_id: &str, channel_id: &str, rumor_id: &str, ) -> Result<Value>
Fetch a pinned attachment FROM THE PIN ALONE (CORD-04 §7’s whole point:
the proof carries the rumor, whose imeta carries the blob URL and the
file’s decryption material — no chat history and no old epoch key
needed). Serves the already-downloaded copy when its content hash
verifies; otherwise runs the full download walk (mirrors + hash-swap),
decrypts, verifies, and caches at the same content-addressed path the
chat pipeline uses. Returns { path, mime, name }.
Sourcepub fn get_channel_pins(
&self,
community_id: &str,
channel_id: &str,
) -> Result<Value>
pub fn get_channel_pins( &self, community_id: &str, channel_id: &str, ) -> Result<Value>
A channel’s verified pins from the locally folded head — local-only read.
sealed: true means the list exists but is unreadable on this device;
render it as unavailable, never as empty.
Sourcepub fn community_role_graph(&self, community_id: &str) -> Result<Value>
pub fn community_role_graph(&self, community_id: &str) -> Result<Value>
The whole role graph: every role with its position and colour, and who holds what.
community_roles above answers only “who is an admin”, which is all the
crown ever needed. A member list that groups people by rank needs the
hierarchy itself, and npubs rather than hex so it can match a roster it
already holds.
Sourcepub fn community_roles_view(&self, community_id: &str) -> Result<Value>
pub fn community_roles_view(&self, community_id: &str) -> Result<Value>
Everything a role editor needs, in one local read: every role in display order (CORD-04 §3: position, then the lower role_id), who holds each, what the caller may touch, and the channels a role can be scoped to. Permissions ride as decimal strings, the wire’s own rule, since a JS number corrupts past 2^53.
Sourcepub async fn create_role(
&self,
community_id: &str,
name: &str,
color: u32,
permissions: &str,
channel_id: Option<&str>,
) -> Result<String>
pub async fn create_role( &self, community_id: &str, name: &str, color: u32, permissions: &str, channel_id: Option<&str>, ) -> Result<String>
Create a Role at the bottom of the list. Returns its id.
Sourcepub async fn edit_role(
&self,
community_id: &str,
role_id: &str,
name: &str,
color: u32,
permissions: &str,
channel_id: Option<&str>,
) -> Result<()>
pub async fn edit_role( &self, community_id: &str, role_id: &str, name: &str, color: u32, permissions: &str, channel_id: Option<&str>, ) -> Result<()>
Edit a Role’s name, colour, permissions and scope.
Sourcepub async fn reorder_roles(
&self,
community_id: &str,
ordered: &[String],
) -> Result<usize>
pub async fn reorder_roles( &self, community_id: &str, ordered: &[String], ) -> Result<usize>
Reorder the roles beneath the caller, top to bottom. Returns how many moved.
Sourcepub async fn set_member_roles(
&self,
community_id: &str,
npub: &str,
role_ids: Vec<String>,
) -> Result<()>
pub async fn set_member_roles( &self, community_id: &str, npub: &str, role_ids: Vec<String>, ) -> Result<()>
Set exactly which Roles a member holds.
Sourcepub fn member_has_permission(
&self,
community_id: &str,
npub: &str,
permission: u64,
) -> Result<bool>
pub fn member_has_permission( &self, community_id: &str, npub: &str, permission: u64, ) -> Result<bool>
Whether npub holds permission in this community, per the folded roster.
The roster IS the ACL (CORD-04), so this is the same question every enforcement path asks — a bot gating a chat command on it refuses exactly what the protocol would, instead of inventing a second permission model that can disagree with the first.
Deliberately NOT [community_capabilities]: that answers “could I publish
this”, which additionally needs the Control Plane write key. Here the
caller is asking someone else for authority while the bot does the
publishing, so the write key is the bot’s problem, not theirs.
Fails CLOSED. A banned member holds nothing; an unreadable or not-yet-folded
roster answers false for everyone but the owner, because has_permission
finds no grant. Authority must never be granted by an absence of data.
Sourcepub fn community_roles(&self, community_id: &str) -> Result<Value>
pub fn community_roles(&self, community_id: &str) -> Result<Value>
The community’s owner npub + the admin npubs (role overview). A local read,
like Self::community_capabilities.
Sourcepub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()>
pub async fn grant_admin(&self, community_id: &str, npub: &str) -> Result<()>
Grant a member the @admin role. Requires MANAGE_ROLES + outranking the role’s position.
Sourcepub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()>
pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()>
Revoke a member’s @admin role.
Sourcepub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()>
pub async fn kick_member(&self, community_id: &str, npub: &str) -> Result<()>
Cooperatively kick a member — they self-remove but can rejoin. Requires KICK + outrank.
Sourcepub async fn set_member_banned(
&self,
community_id: &str,
npub: &str,
banned: bool,
) -> Result<()>
pub async fn set_member_banned( &self, community_id: &str, npub: &str, banned: bool, ) -> Result<()>
Ban (true) or unban (false) a member. Ban is terminal (no rejoin). The read cut:
a private community re-founds (full rotation); a public one rotates just the private
channels the member could read (CORD-05 §5 — no refound, the link refresh would
re-hand them the root). Requires BAN + outrank, checked against the folded roster
BEFORE any publish.
Sourcepub async fn set_members_banned(
&self,
community_id: &str,
npubs: &[&str],
banned: bool,
) -> Result<()>
pub async fn set_members_banned( &self, community_id: &str, npubs: &[&str], banned: bool, ) -> Result<()>
Ban (true) or unban (false) a whole batch as ONE moderation unit: one
Banlist edition, one grant-strip pass, one read cut — a private community
re-founds ONCE with every target removed, however many there are. Additive
on ban, reductive on unban, over the freshest folded list either way.
Serialized per community (with every other ban/unban) so concurrent moderation composes in arrival order instead of the last write erasing its siblings. The wire caps a Banlist at 500 entries — a batch that would exceed it fails before anything publishes.
Sourcepub async fn rescue_stranded_members(
&self,
community_id: &str,
npubs: &[&str],
) -> Result<Value>
pub async fn rescue_stranded_members( &self, community_id: &str, npubs: &[&str], ) -> Result<Value>
Re-vend the epoch chain to members a past rotation forgot.
A client follows rekeys hop by hop and can only ever derive held + 1, so
a member missed at one hop is stranded there permanently — no later
rotation can reach them, and re-inviting does not help because they are
already a member. This publishes the blobs that should have existed, keyed
exactly like the original rotation so they merge into it.
Requires rotation authority (owner or BAN) and the archived roots, which the rotator holds. Publishing nothing else and touching no membership.
Sourcepub async fn repair_community_roster(&self, community_id: &str) -> Result<Value>
pub async fn repair_community_roster(&self, community_id: &str) -> Result<Value>
Repair a v2 community’s roster cache: keep what is stored, but reset its PROVENANCE to zero so the next control fold is allowed to replace it.
The completeness guard retains the cached roster whenever a stored entity is floored but folds no head — protection against a relay quietly stripping admins. If the cache itself is wrong, that same guard pins the wrong data in place: the fold can see the real roster and still be refused. Zeroing the provenance marks the cache a seed — a guess, not evidence — which the fold may overwrite. It publishes nothing and touches no key material; worst case the next fold writes what it can see, which is what a fresh join would get.
Sourcepub async fn explain_epoch_state(&self, community_id: &str) -> Result<Value>
pub async fn explain_epoch_state(&self, community_id: &str) -> Result<Value>
Explain this account’s epoch state for a v2 community: what it holds, what the next-epoch rekey plane offers, and why each rotation there was or was not adopted.
This is the answer to “is this client stranded, and why” — a question that is
otherwise unanswerable, because a stranded client sees only silence and
silence looks exactly like a quiet community. Run it from two clients on one
community to compare their views; the held block is where they diverge
first. Read-only; returns public keys and verdicts, never key material.
Sourcepub async fn contain_raid(
&self,
community_id: &str,
npubs: &[&str],
window_start_secs: u64,
cut_also: &[&str],
) -> Result<ContainmentReport>
pub async fn contain_raid( &self, community_id: &str, npubs: &[&str], window_start_secs: u64, cut_also: &[&str], ) -> Result<ContainmentReport>
Contain a raid (v2 only): ban the named raiders, cut everyone who joined through the door during the raid window, and perform a SEVERING Refounding — the rotation that REVOKES the public invite links instead of carrying them into the new epoch.
This is the raid lane. An ordinary ban keeps its shipped behaviour, where a Public community deliberately does not roll the root (the stable-URL refresh would undo it). Here the link IS the attack surface, so it closes.
window_start_secs is the raid’s opening edge (unix seconds); pass 0 to cut
only the convicted. Returns an honest report — a failed refound is reported,
never dressed up as a closed door.
Sourcepub async fn dissolve_community(&self, community_id: &str) -> Result<()>
pub async fn dissolve_community(&self, community_id: &str) -> Result<()>
Owner dissolution / “Delete Community”: publish the terminal GroupDissolved tombstone (and
retire the owner’s own invite links, no rekey), sealing the community permanently. Owner-only
(re-verified cryptographically in service::dissolve_community); irreversible.
Sourcepub async fn edit_community_metadata(
&self,
community_id: &str,
name: Option<&str>,
description: Option<&str>,
) -> Result<()>
pub async fn edit_community_metadata( &self, community_id: &str, name: Option<&str>, description: Option<&str>, ) -> Result<()>
Edit community metadata (name / description) as an authorized member (MANAGE_METADATA). None leaves
a field unchanged; an empty description clears it.
Sourcepub async fn leave_community(&self, community_id: &str) -> Result<()>
pub async fn leave_community(&self, community_id: &str) -> Result<()>
Leave a Community: announce a best-effort “left” presence (before dropping keys), then drop the held keys + local channel chats. You need a fresh invite to rejoin.
Sourcepub async fn sync_dms(
&self,
since_days: Option<u64>,
handler: &dyn InboundEventHandler,
) -> Result<(u32, u32)>
pub async fn sync_dms( &self, since_days: Option<u64>, handler: &dyn InboundEventHandler, ) -> Result<(u32, u32)>
Sync DM history from relays using NIP-77 negentropy set reconciliation.
Reconciles local wrapper history with relay state, fetches missing events, and processes them through the standard prepare → commit pipeline.
Returns (total_events, new_messages).
let core = vector_core::VectorCore;
// Sync last 7 days of DMs
let (events, new) = core.sync_dms(Some(7), &vector_core::NoOpEventHandler).await?;
println!("Processed {} events, {} new messages", events, new);Sourcepub async fn subscribe_dms(&self) -> Result<SubscriptionId>
pub async fn subscribe_dms(&self) -> Result<SubscriptionId>
Subscribe to incoming DM events (NIP-17 GiftWraps).
Returns the subscription ID for use in a custom notification loop.
For a complete listen-and-process loop, use listen() instead.
Sourcepub async fn sync_communities(&self) -> Result<Vec<String>>
pub async fn sync_communities(&self) -> Result<Vec<String>>
Catch up every locally-held Community: fold control / re-foundings / rekeys / banlist and
fetch recent messages into local state for each channel. State-only (does not replay to an
InboundEventHandler). Called at listen() start and periodically for outage resilience;
also safe to call manually after a known disconnect.
Catch up every locally-held Community. v1 channels are synced inline; a v2
community is ENQUEUED for the follow worker (control/rekey re-fold + adopt),
non-blocking. State-only (no handler replay of history). Called at listen()
start and on reconnect; safe to call manually — the v2 enqueue is a no-op if
no listen() worker is running.
Returns each community’s non-fatal follow warnings (rekey/control catch-up
failures), prefixed with the id. A caller that discards them is blind to
“the sync ran, but this account never adopted the rotation” — which reads
exactly like a healthy quiet sync while the account sits on a dead epoch.
Sourcepub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()>
pub async fn listen(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()>
Start listening for incoming DMs.
Blocks until the client disconnects. Processes GiftWraps (DMs, files) → prepare_event → commit_prepared_event.
use vector_core::*;
use std::sync::Arc;
struct MyBot;
impl InboundEventHandler for MyBot {
fn on_dm_received(&self, chat_id: &str, msg: &Message, _is_new: bool) {
if msg.mine { return; }
let to = chat_id.to_string();
let reply = format!("Echo: {}", msg.content);
tokio::spawn(async move {
let _ = VectorCore.send_dm(&to, &reply).await;
});
}
}
let core = VectorCore::init(CoreConfig {
data_dir: "/tmp/bot-data".into(),
event_emitter: None,
})?;
core.login("nsec1...", None).await?;
core.listen(Arc::new(MyBot)).await?;Sourcepub async fn listen_with(
&self,
handler: Arc<dyn InboundEventHandler>,
catch_up: CatchUp,
) -> Result<()>
pub async fn listen_with( &self, handler: Arc<dyn InboundEventHandler>, catch_up: CatchUp, ) -> Result<()>
listen, choosing who fetches what was published while offline.
Sourcepub async fn swap_session(&self)
pub async fn swap_session(&self)
Tear down the current session for an in-process account swap — the account-agnostic core of
the app’s reset_session(). Advances the session generation FIRST so any background task
holding a std::sync::Arc<crate::db::Session> short-circuits before it can touch the next account’s storage; shuts
the client down (which ends any listen() notification loop bound to it, so the old account’s
events can’t land in the new account’s DB); closes the DB pool; and clears the key vaults plus
all in-memory per-account state. Follow with login() to bind the next account, then re-attach
listen(). (The app’s reset_session() additionally clears Tauri-only caches it owns.)
Trait Implementations§
Source§impl Clone for VectorCore
impl Clone for VectorCore
impl Copy for VectorCore
Auto Trait Implementations§
impl Freeze for VectorCore
impl RefUnwindSafe for VectorCore
impl Send for VectorCore
impl Sync for VectorCore
impl Unpin for VectorCore
impl UnsafeUnpin for VectorCore
impl UnwindSafe for VectorCore
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more