pub struct SidecarClient { /* private fields */ }Expand description
A client for the atproto OAuth sidecar’s internal API.
This is the live path for every authed repo operation. Rather than the Rust
server holding PDS tokens, it POSTs {did, action, …} to the sidecar’s
/internal/repo endpoint (gated by the shared X-Internal-Secret); the
sidecar restore(did)s the OAuth session — transparent DPoP + token refresh —
and runs the matching XRPC call via @atproto/api. The did (plus the shared
secret) is what authorizes the call; there is no bearer token on the Rust side.
It also fronts /internal/session/:id, the one-shot handoff the Rust callback
uses to turn a session_id (from the sidecar’s browser redirect) into the
{did, handle} it keys its own signed cookie by.
Cheap to clone (shared reqwest::Client + Arc’d config).
Implementations§
Source§impl SidecarClient
impl SidecarClient
Sourcepub fn new(
http: Client,
public_url: impl Into<String>,
internal_url: impl Into<String>,
internal_secret: impl Into<String>,
) -> Self
pub fn new( http: Client, public_url: impl Into<String>, internal_url: impl Into<String>, internal_secret: impl Into<String>, ) -> Self
Build a sidecar client from the shared reqwest::Client and the
resolved public + internal base URLs + internal secret (from
crate::config::SidecarConfig). public_url anchors the browser
/login redirect; internal_url is the loopback base for the /internal/*
API (they collapse to the same value in single-URL local dev).
Sourcepub fn login_url(&self, handle: &str, return_to: Option<&str>) -> String
pub fn login_url(&self, handle: &str, return_to: Option<&str>) -> String
The sidecar’s public /login URL for a handle, round-tripping an opaque
return value through OAuth state (used to bounce the browser back to a
specific place after login). The browser is redirected here.
Sourcepub async fn resolve_session(
&self,
session_id: &str,
) -> Result<Option<SidecarSession>>
pub async fn resolve_session( &self, session_id: &str, ) -> Result<Option<SidecarSession>>
Resolve a one-shot session_id (from the sidecar’s post-OAuth redirect)
to the {did, handle} that logged in. Ok(None) on 404 SessionNotFound.
Sourcepub async fn revoke_session(&self, did: &str) -> Result<RevokeResult>
pub async fn revoke_session(&self, did: &str) -> Result<RevokeResult>
Revoke a DID’s OAuth session at the sidecar: POST /internal/revoke.
This revokes the refresh + access tokens at the PDS and purges the
sidecar’s stored oauth_session + app_session rows for the DID. It is
idempotent — revoking a DID with no live session returns
had_session: false. Called on /logout (so the cookie clear isn’t the
only thing that ends the session) and on /account/delete.
Sourcepub async fn list_records(
&self,
did: &str,
collection: &str,
limit: Option<u32>,
cursor: Option<&str>,
) -> Result<ListRecordsResponse>
pub async fn list_records( &self, did: &str, collection: &str, limit: Option<u32>, cursor: Option<&str>, ) -> Result<ListRecordsResponse>
list — one page of a collection’s records for did.
Sourcepub async fn list_all_records(
&self,
did: &str,
collection: &str,
) -> Result<Vec<RecordEntry>>
pub async fn list_all_records( &self, did: &str, collection: &str, ) -> Result<Vec<RecordEntry>>
Page through all records in a collection for did.
Bounded by MAX_LIST_PAGES and cursor-repetition detection, same as
PdsClient::list_all_records — the sidecar proxies to the account’s
PDS, so the page count is ultimately remote-controlled here too.
Sourcepub async fn delete_record(
&self,
did: &str,
collection: &str,
rkey: &str,
) -> Result<()>
pub async fn delete_record( &self, did: &str, collection: &str, rkey: &str, ) -> Result<()>
delete — delete a record by collection + rkey.
Sourcepub async fn list_subscriptions(
&self,
did: &str,
) -> Result<Vec<(String, Subscription)>>
pub async fn list_subscriptions( &self, did: &str, ) -> Result<Vec<(String, Subscription)>>
List every Subscription record in did’s repo (paged fully).
Sourcepub async fn list_subscriptions_with_cids(
&self,
did: &str,
) -> Result<Vec<(String, Option<String>, Subscription)>>
pub async fn list_subscriptions_with_cids( &self, did: &str, ) -> Result<Vec<(String, Option<String>, Subscription)>>
Every Subscription with the CID it was listed at, unsorted — the
read half of a read-modify-write that puts with swapRecord (#149).
Sourcepub async fn create_subscription(
&self,
did: &str,
sub: &VettedSubscription,
) -> Result<WriteResult>
pub async fn create_subscription( &self, did: &str, sub: &VettedSubscription, ) -> Result<WriteResult>
Create a Subscription record (subscribe to a feed).
Sourcepub async fn delete_subscription(&self, did: &str, rkey: &str) -> Result<()>
pub async fn delete_subscription(&self, did: &str, rkey: &str) -> Result<()>
Delete a Subscription record by rkey (unsubscribe).
Sourcepub async fn list_folders(&self, did: &str) -> Result<Vec<(String, Folder)>>
pub async fn list_folders(&self, did: &str) -> Result<Vec<(String, Folder)>>
List every Folder record in did’s repo.
Sourcepub async fn list_folders_with_cids(
&self,
did: &str,
) -> Result<Vec<(String, Option<String>, Folder)>>
pub async fn list_folders_with_cids( &self, did: &str, ) -> Result<Vec<(String, Option<String>, Folder)>>
Every Folder with the CID it was listed at, unsorted (#268).
Sourcepub async fn list_saved(&self, did: &str) -> Result<Vec<(String, Saved)>>
pub async fn list_saved(&self, did: &str) -> Result<Vec<(String, Saved)>>
List every Saved record in did’s repo.
Sourcepub async fn list_read_states(
&self,
did: &str,
) -> Result<Vec<(String, ReadState)>>
pub async fn list_read_states( &self, did: &str, ) -> Result<Vec<(String, ReadState)>>
List every ReadState cursor in did’s repo (the read side a
login-time read-state merge would consume).
Sourcepub async fn put_read_state(
&self,
did: &str,
rkey: &str,
state: &ReadState,
) -> Result<WriteResult>
pub async fn put_read_state( &self, did: &str, rkey: &str, state: &ReadState, ) -> Result<WriteResult>
Upsert a single ReadState cursor at its feed-derived rkey.
Sourcepub async fn flush_read_states(
&self,
did: &str,
cursors: &[(String, ReadState, bool)],
) -> Result<()>
pub async fn flush_read_states( &self, did: &str, cursors: &[(String, ReadState, bool)], ) -> Result<()>
Batch-flush many dirty ReadState cursors via applyWrites (chunked).
Each (rkey, state, pds_created) becomes a create op at the feed-derived
rkey when the record does NOT yet exist (pds_created == false), and an
update op when it does. This is what makes the FIRST flush of a feed
succeed: applyWrites#update errors on a record that does not pre-exist,
and applyWrites is atomic per-repo, so a single not-yet-created cursor
would otherwise drop the whole DID batch. Both kinds ride the SAME
applyWrites batch so batching is preserved.
Sourcepub async fn add_subscription(
&self,
did: &str,
sub: &VettedSubscription,
) -> Result<String>
pub async fn add_subscription( &self, did: &str, sub: &VettedSubscription, ) -> Result<String>
Add a subscription (subscribe to a feed) — createRecord, server-assigned
tid rkey. Returns the new record’s rkey so the web layer can offer
unsubscribe/rename immediately.
Sourcepub async fn remove_subscription(&self, did: &str, rkey: &str) -> Result<()>
pub async fn remove_subscription(&self, did: &str, rkey: &str) -> Result<()>
Remove a subscription (unsubscribe) by rkey — deleteRecord. Alias of
delete_subscription in the reader vocabulary.
Sourcepub async fn update_subscription(
&self,
did: &str,
rkey: &str,
sub: &VettedSubscription,
swap_record: Option<&str>,
) -> Result<WriteResult>
pub async fn update_subscription( &self, did: &str, rkey: &str, sub: &VettedSubscription, swap_record: Option<&str>, ) -> Result<WriteResult>
Update / rename a subscription in place at a known rkey — putRecord.
The whole record is replaced (retitle, move to a folder, change the fetch hint …). Upsert semantics: it also creates the record if the rkey is somehow absent, so it is safe as a general “write this exact record”.
Sourcepub async fn list_subscriptions_sorted(
&self,
did: &str,
) -> Result<Vec<(String, Subscription)>>
pub async fn list_subscriptions_sorted( &self, did: &str, ) -> Result<Vec<(String, Subscription)>>
List every subscription, sorted deterministically — by display title (case-insensitive), then feed URL, then rkey as the final tiebreaker — so the rendered feed list is stable across reads regardless of PDS return order. Untitled feeds sort by their URL.
Sourcepub async fn add_subscriptions_bulk(
&self,
did: &str,
subs: &[VettedSubscription],
) -> Result<Vec<String>>
pub async fn add_subscriptions_bulk( &self, did: &str, subs: &[VettedSubscription], ) -> Result<Vec<String>>
Batch-add many subscriptions via applyWrites (chunked) — the OPML-import path.
Each feed becomes one create op. Client-side monotonic tid
rkeys are assigned so the batch is deterministic and the imported feeds
keep OPML order (server-assigned tids would also be monotonic, but pinning
them here makes the whole import reproducible and testable offline).
Returns the assigned rkeys in input order.
More than APPLY_WRITES_MAX_OPS feeds is more than one call, so the
import can part-land: on an error, ApplyWritesIncomplete::of gives
landed, and the first landed of subs are in the repo.
Sourcepub async fn add_folder(&self, did: &str, folder: &Folder) -> Result<String>
pub async fn add_folder(&self, did: &str, folder: &Folder) -> Result<String>
Add a folder — createRecord, server-assigned tid rkey. Returns the
new folder’s rkey (subscriptions reference it by its at:// URI).
Sourcepub async fn remove_folder(&self, did: &str, rkey: &str) -> Result<()>
pub async fn remove_folder(&self, did: &str, rkey: &str) -> Result<()>
Remove a folder by rkey — deleteRecord. (Subscriptions referencing it
are left untouched; a dangling folder ref reads as “unfiled”.)
Sourcepub async fn rename_folder(
&self,
did: &str,
rkey: &str,
folder: &Folder,
swap_record: Option<&str>,
) -> Result<WriteResult>
pub async fn rename_folder( &self, did: &str, rkey: &str, folder: &Folder, swap_record: Option<&str>, ) -> Result<WriteResult>
Rename / update a folder in place at a known rkey — putRecord
(rename, or change its position sort hint).
Replaces the WHOLE record, so folder must be the record as read with
only the intended change. swap_record is the CID it was read at: the
PDS refuses the write with InvalidSwap if the record has moved since
(#268). None writes unconditionally.
Sourcepub async fn list_folders_sorted(
&self,
did: &str,
) -> Result<Vec<(String, Folder)>>
pub async fn list_folders_sorted( &self, did: &str, ) -> Result<Vec<(String, Folder)>>
List every folder, sorted deterministically — by position (the
lexicon’s sort hint; unset sorts last), then name (case-insensitive),
then rkey — so the sidebar order is stable.
Sourcepub async fn add_saved(&self, did: &str, saved: &VettedSaved) -> Result<String>
pub async fn add_saved(&self, did: &str, saved: &VettedSaved) -> Result<String>
Add a saved (starred / save-for-later) entry — createRecord,
server-assigned tid rkey. Returns the new record’s rkey.
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for SidecarClient
impl !UnwindSafe for SidecarClient
impl Freeze for SidecarClient
impl Send for SidecarClient
impl Sync for SidecarClient
impl Unpin for SidecarClient
impl UnsafeUnpin for SidecarClient
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,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
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