pub struct Agent { /* private fields */ }Expand description
High-level AT Protocol agent.
Auth state lives in a single RwLock<Option<Session>>. The XRPC client
is never mutated after construction — auth headers are passed per-request.
This avoids token leaks, giant-lock contention, and split-lock atomicity
gaps that arise from storing auth in the client’s default headers.
§Transparent refresh
Every XRPC call goes through xrpc_query_with_refresh /
xrpc_procedure_with_refresh, which detect 401 /
ExpiredToken responses, call Agent::refresh_session, and
retry once. Concurrent refresh attempts are deduplicated via an
async Mutex so N in-flight calls that all see an expired token
issue exactly one /refreshSession request. If the refresh itself
fails, the agent fires AtpSessionEvent::Expired and the
original error propagates.
Implementations§
Source§impl Agent
impl Agent
Sourcepub fn new(service: impl AsRef<str>) -> Result<Self, AgentError>
pub fn new(service: impl AsRef<str>) -> Result<Self, AgentError>
Create a new agent pointing at the given service URL.
Available whenever XrpcClient::new is — native requires
fetch-reqwest; on wasm the default is always the browser
fetch backend.
Sourcepub fn on_session<F>(&self, callback: F)
pub fn on_session<F>(&self, callback: F)
Register a session-event listener.
Returns (), not a handle — listener unregistration isn’t
currently supported (the typical pattern is to register a
single persistence callback that lives for the Agent’s
lifetime). Multiple listeners are fired in registration order.
Sourcepub async fn anon_call_options(&self) -> Option<CallOptions>
pub async fn anon_call_options(&self) -> Option<CallOptions>
Build anonymous CallOptions carrying just the proxy and
labeler config, for methods that don’t need auth.
Exposed in case callers drive XrpcClient::query / ::procedure
directly and want the agent’s proxy / labeler headers folded in.
Sourcepub async fn configure_proxy(&self, target: Option<&str>)
pub async fn configure_proxy(&self, target: Option<&str>)
Configure the service-proxy target (atproto-proxy header) for
every subsequent call. Pass None to clear.
The canonical use case is chat, which runs on a different
service: agent.configure_proxy(Some("did:web:api.bsky.chat#bsky_chat")).
Sourcepub async fn with_proxy(&self, target: &str) -> Self
pub async fn with_proxy(&self, target: &str) -> Self
Return a new Agent configured with the given proxy target.
Shares session state with this agent (cheap clone of internals).
Sourcepub async fn configure_labelers(&self, labelers: &[LabelerOpts])
pub async fn configure_labelers(&self, labelers: &[LabelerOpts])
Configure the set of labelers sent as atproto-accept-labelers.
Passing an empty slice clears the header.
Sourcepub async fn login(
&self,
identifier: &AtIdentifier,
password: &str,
) -> Result<Session, AgentError>
pub async fn login( &self, identifier: &AtIdentifier, password: &str, ) -> Result<Session, AgentError>
Log in with identifier (handle or DID) and password.
Emits AtpSessionEvent::Create on success, or
AtpSessionEvent::CreateFailed if the server rejected the
credentials.
Sourcepub async fn resume_session(&self, session: Session) -> Result<(), AgentError>
pub async fn resume_session(&self, session: Session) -> Result<(), AgentError>
Resume an existing session.
Verifies the session with the server before updating internal state. If verification fails, the agent remains unauthenticated.
Sourcepub async fn refresh_session(&self) -> Result<Session, AgentError>
pub async fn refresh_session(&self) -> Result<Session, AgentError>
Refresh the current session tokens.
Emits AtpSessionEvent::Update on success or
AtpSessionEvent::Expired if the refresh token was
rejected. Uses a per-request header for the refresh call so the
refresh JWT is never exposed as the global auth state. The new
session is committed atomically in a single write lock.
Sourcepub async fn post(
&self,
text: &str,
facets: Option<Vec<Facet>>,
created_at: Option<&str>,
) -> Result<Value, AgentError>
pub async fn post( &self, text: &str, facets: Option<Vec<Facet>>, created_at: Option<&str>, ) -> Result<Value, AgentError>
Create a new post.
If created_at is None, the current time is used.
Sourcepub async fn post_rich(
&self,
rt: &RichText,
created_at: Option<&str>,
) -> Result<Value, AgentError>
pub async fn post_rich( &self, rt: &RichText, created_at: Option<&str>, ) -> Result<Value, AgentError>
Create a post from RichText (includes detected facets).
Sourcepub async fn delete_post(&self, uri: &AtUri) -> Result<(), AgentError>
pub async fn delete_post(&self, uri: &AtUri) -> Result<(), AgentError>
Delete a post by AT-URI.
Sourcepub async fn like(
&self,
uri: &AtUri,
cid: &Cid,
created_at: Option<&str>,
) -> Result<Value, AgentError>
pub async fn like( &self, uri: &AtUri, cid: &Cid, created_at: Option<&str>, ) -> Result<Value, AgentError>
Like a post.
If created_at is None, the current time is used.
Sourcepub async fn delete_like(&self, like_uri: &AtUri) -> Result<(), AgentError>
pub async fn delete_like(&self, like_uri: &AtUri) -> Result<(), AgentError>
Unlike a post by AT-URI of the like record.
Sourcepub async fn repost(
&self,
uri: &AtUri,
cid: &Cid,
created_at: Option<&str>,
) -> Result<Value, AgentError>
pub async fn repost( &self, uri: &AtUri, cid: &Cid, created_at: Option<&str>, ) -> Result<Value, AgentError>
Repost a post.
If created_at is None, the current time is used.
Sourcepub async fn delete_repost(&self, repost_uri: &AtUri) -> Result<(), AgentError>
pub async fn delete_repost(&self, repost_uri: &AtUri) -> Result<(), AgentError>
Delete a repost by AT-URI.
Sourcepub async fn follow(
&self,
subject_did: &Did,
created_at: Option<&str>,
) -> Result<Value, AgentError>
pub async fn follow( &self, subject_did: &Did, created_at: Option<&str>, ) -> Result<Value, AgentError>
Follow a user by DID.
If created_at is None, the current time is used.
Sourcepub async fn delete_follow(&self, follow_uri: &AtUri) -> Result<(), AgentError>
pub async fn delete_follow(&self, follow_uri: &AtUri) -> Result<(), AgentError>
Unfollow by AT-URI of the follow record.
Sourcepub async fn get_profile(
&self,
actor: &AtIdentifier,
) -> Result<Value, AgentError>
pub async fn get_profile( &self, actor: &AtIdentifier, ) -> Result<Value, AgentError>
Get a user’s profile.
Sourcepub async fn get_timeline(
&self,
limit: Option<i64>,
cursor: Option<&str>,
) -> Result<Value, AgentError>
pub async fn get_timeline( &self, limit: Option<i64>, cursor: Option<&str>, ) -> Result<Value, AgentError>
Get the home timeline.
Sourcepub async fn get_post_thread(
&self,
uri: &AtUri,
depth: Option<i64>,
) -> Result<Value, AgentError>
pub async fn get_post_thread( &self, uri: &AtUri, depth: Option<i64>, ) -> Result<Value, AgentError>
Get a post thread.
Sourcepub async fn search_actors(
&self,
query: &str,
limit: Option<i64>,
) -> Result<Value, AgentError>
pub async fn search_actors( &self, query: &str, limit: Option<i64>, ) -> Result<Value, AgentError>
Search actors.
Sourcepub async fn resolve_handle(&self, handle: &Handle) -> Result<Did, AgentError>
pub async fn resolve_handle(&self, handle: &Handle) -> Result<Did, AgentError>
Resolve a handle to a DID.
Sourcepub async fn list_notifications(
&self,
limit: Option<i64>,
cursor: Option<&str>,
) -> Result<Value, AgentError>
pub async fn list_notifications( &self, limit: Option<i64>, cursor: Option<&str>, ) -> Result<Value, AgentError>
Get notifications.
Sourcepub async fn upload_blob(
&self,
data: Vec<u8>,
content_type: &str,
) -> Result<Value, AgentError>
pub async fn upload_blob( &self, data: Vec<u8>, content_type: &str, ) -> Result<Value, AgentError>
Upload a blob (image, video, etc.).
Sourcepub async fn describe_server(&self) -> Result<Value, AgentError>
pub async fn describe_server(&self) -> Result<Value, AgentError>
Describe the server.
Sourcepub async fn logout(&self) -> Result<(), AgentError>
pub async fn logout(&self) -> Result<(), AgentError>
Log out of the current session.
Sends a best-effort deleteSession call using the current
refresh token (TS matches this — deleteSession requires
the refresh JWT, not the access JWT). Clears local session
state whether or not the server call succeeds, so the agent
always ends up unauthenticated.
Sourcepub async fn create_account(
&self,
handle: &Handle,
password: &str,
email: Option<&str>,
extra: Option<Value>,
) -> Result<Session, AgentError>
pub async fn create_account( &self, handle: &Handle, password: &str, email: Option<&str>, extra: Option<Value>, ) -> Result<Session, AgentError>
Create a new account on the current service.
extra is merged into the request body — useful for passing
inviteCode, verificationCode, or custom provider-specific
fields without this method’s signature needing to know every
option the server supports.
On success, the new session is stored and Create is emitted.
Sourcepub async fn upsert_profile<F>(&self, mutate: F) -> Result<Value, AgentError>
pub async fn upsert_profile<F>(&self, mutate: F) -> Result<Value, AgentError>
Create-or-update the signed-in user’s app.bsky.actor.profile
record.
The mutate closure receives the existing profile record (or
serde_json::Value::Null if none exists) and returns the
desired next state. This pattern mirrors TS
AtpAgent.upsertProfile(updateFn).
The write uses putRecord with swapRecord for CAS safety;
if the swap fails we retry up to 5 times with a fresh read.