Skip to main content

VectorCore

Struct VectorCore 

Source
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

Source

pub fn init(config: CoreConfig) -> Result<Self>

Initialize Vector Core with the given configuration.

Source

pub fn accounts(&self) -> Result<Vec<String>>

Get all available accounts.

Source

pub async fn login( &self, key: &str, password: Option<&str>, ) -> Result<LoginResult>

Login with an nsec key or mnemonic seed phrase.

Source

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.

Source

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).

Source

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).

Source

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.

Source

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.

Source

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.

Source

pub async fn send_file( &self, to_npub: &str, file_path: &str, ) -> Result<SendResult>

Send a NIP-17 gift-wrapped file attachment DM.

Source

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.

Source

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.

Source

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.

Source

pub async fn delete_dm(&self, message_id: &str) -> Result<DeleteOutcome>

Delete a DM you sent (NIP-09 over the retained gift-wrap keys).

Source

pub async fn get_chats(&self) -> Vec<SerializableChat>

Get chats from the in-memory state.

Source

pub async fn get_messages( &self, chat_id: &str, limit: usize, offset: usize, ) -> Vec<Message>

Get messages for a chat (paginated).

Source

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.

Source

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.

Source

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.

Source

pub async fn get_profile(&self, npub: &str) -> Option<SlimProfile>

Get a profile by npub.

Source

pub async fn load_profile(&self, npub: &str) -> bool

Fetch a profile’s metadata and status from relays.

Source

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.

Source

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.

Source

pub async fn update_status(&self, status: &str) -> bool

Update the current user’s status and broadcast to relays.

Source

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.

Source

pub async fn block_user(&self, npub: &str) -> bool

Block a user by npub.

Source

pub async fn unblock_user(&self, npub: &str) -> bool

Unblock a user by npub.

Source

pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool

Set a nickname for a profile.

Source

pub async fn get_blocked_users(&self) -> Vec<SlimProfile>

Get all blocked profiles.

Source

pub fn queue_profile_sync(&self, npub: &str, priority: SyncPriority)

Queue a profile for background sync.

Source

pub fn my_npub(&self) -> Option<String>

Get the current user’s npub.

Source

pub async fn list_communities(&self) -> Vec<Value>

List every Community held locally (owned or joined), each with its channels.

Source

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.

Source

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

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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”).

Source

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.

Source

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.

Source

pub fn channel_access( &self, community_id: &str, channel_id: &str, ) -> Result<Value>

Source

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

Source

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.

Source

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).

Source

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).

Source

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).

Source

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.

Source

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.

Source

pub async fn send_community_typing(&self, channel_id: &str) -> Result<()>

Send an ephemeral typing indicator to a Community channel.

Source

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).

Source

pub async fn edit_community_message( &self, channel_id: &str, message_id: &str, new_content: &str, ) -> Result<()>

Edit one of your own Community messages.

Source

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.

Source

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.

Source

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.

Source

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.”

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn policy_presets(&self) -> Result<Value>

The preset library: what a designer offers instead of a blank document.

Source

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.

Source

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.

Source

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.

Source

pub fn delete_community_policy( &self, community_id: &str, policy_id: &str, ) -> Result<()>

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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 }.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub async fn set_member_roles( &self, community_id: &str, npub: &str, role_ids: Vec<String>, ) -> Result<()>

Set exactly which Roles a member holds.

Source

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.

Source

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.

Source

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.

Source

pub async fn revoke_admin(&self, community_id: &str, npub: &str) -> Result<()>

Revoke a member’s @admin role.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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);
Source

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.

Source

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.

Source

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?;
Source

pub async fn listen_with( &self, handler: Arc<dyn InboundEventHandler>, catch_up: CatchUp, ) -> Result<()>

listen, choosing who fetches what was published while offline.

Source

pub async fn logout(&self)

Disconnect and clean up.

Source

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

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for VectorCore

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> AsOut<T> for T
where T: Copy,

Source§

fn as_out(&mut self) -> Out<'_, T>

Returns an out reference to self.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> MaybeSend for T
where T: Send + ?Sized,

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more