Something went wrong. Try again.
Mirror of https://github.com/improsocial/improAn extensible Bluesky client for web
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455import { parseUri } from "/js/dataHelpers.js";import { RefreshTokenError, auth } from "/js/auth.js";import { TokenRefreshError as OauthRefreshTokenError } from "/js/oauth.js";import { batch, buildQueryString, getCurrentTimestamp } from "/js/utils.js";import { linkToLogin } from "/js/navigation.js";import { PUBLIC_SERVICE_ENDPOINT_URL, BSKY_APPVIEW_SERVICE_DID, BSKY_CHAT_SERVICE_DID, BSKY_LABELER_DID, VIDEO_SERVICE_URL, VIDEO_SERVICE_DID,} from "/js/config.js";
// Matches the header format in @atproto/apifunction buildAcceptLabelersHeader(labelers) { return labelers .map((did) => (did === BSKY_LABELER_DID ? `${did};redact` : did)) .join(", ");}
export class ApiError extends Error { constructor(res) { const message = `${res.status} ${res.statusText}`; super(message); this.status = res.status; this.statusText = res.statusText; this.data = res.data; this.headers = res.headers; this.url = res.url; }}
class PublicSession { constructor() { this.serviceEndpoint = PUBLIC_SERVICE_ENDPOINT_URL; } async fetch(url, options) { return fetch(url, options); } get did() { throw new Error("Public session does not have a DID"); }}
export class Api { constructor( session, { bskyAppViewServiceDid = BSKY_APPVIEW_SERVICE_DID, chatAppViewServiceDid = BSKY_CHAT_SERVICE_DID, } = {}, ) { this.isAuthenticated = !!session; this.session = session ?? new PublicSession(); this.bskyAppViewServiceDid = bskyAppViewServiceDid; this.chatAppViewServiceDid = chatAppViewServiceDid; } async request(path, options = {}) { const { body, query, method, headers = {}, parseJson = true, stringifyBody = true, ...restOptions } = options; let queryString = ""; if (query) { queryString = "?" + buildQueryString(query); } let res = null; try { const fetchOptions = { ...restOptions, method: method ?? "GET", headers: { "Content-Type": "application/json", ...headers, }, }; if (body) { if (stringifyBody) { fetchOptions.body = JSON.stringify(body); } else { fetchOptions.body = body; } } res = await this.session.fetch( `${this.session.serviceEndpoint}/xrpc/${path}${queryString}`, fetchOptions, ); } catch (error) { // Handle token refresh error if ( error instanceof RefreshTokenError || error instanceof OauthRefreshTokenError ) { console.error("Token refresh error", error); const did = this.isAuthenticated ? (this.session.did ?? null) : null; await auth.logout(did); window.location.href = linkToLogin(); await new Promise(() => {}); } throw error; } let data = null; if (parseJson) { // If body was already consumed by the oauth library, use that data = res.data ?? (await res.json()); } res.data = data; if (!res.ok) { throw new ApiError(res); } return res; }
async createLikeRecord(post) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.feed.like", record: { createdAt: getCurrentTimestamp(), subject: { uri: post.uri, cid: post.cid }, }, }, }); return res.data; }
async deleteLikeRecord(post) { const like = post.viewer.like; const rkey = like.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.feed.like", rkey, }, }); return res.data; }
async createRepostRecord(post) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.feed.repost", record: { createdAt: getCurrentTimestamp(), subject: { uri: post.uri, cid: post.cid }, }, }, }); return res.data; }
async deleteRepostRecord(post) { const repost = post.viewer.repost; const rkey = repost.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.feed.repost", rkey, }, }); return res.data; }
async createBookmark(post) { const res = await this.request("app.bsky.bookmark.createBookmark", { method: "POST", body: { uri: post.uri, cid: post.cid, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res.data; }
async deleteBookmark(post) { const res = await this.request("app.bsky.bookmark.deleteBookmark", { method: "POST", body: { uri: post.uri, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res.data; }
async getDrafts({ cursor = "", limit } = {}) { const query = {}; if (cursor) { query.cursor = cursor; } if (limit) { query.limit = limit; } const res = await this.request("app.bsky.draft.getDrafts", { query, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async createDraft(draft) { const res = await this.request("app.bsky.draft.createDraft", { method: "POST", body: { draft }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async updateDraft(id, draft) { const res = await this.request("app.bsky.draft.updateDraft", { method: "POST", body: { draft: { id, draft } }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res.data; }
async deleteDraft(id) { const res = await this.request("app.bsky.draft.deleteDraft", { method: "POST", body: { id }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res.data; }
async createFollowRecord(profile) { const res = await this.request(`com.atproto.repo.createRecord`, { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.follow", record: { createdAt: getCurrentTimestamp(), subject: profile.did, }, }, }); return res.data; }
async deleteFollowRecord(profile) { const follow = profile.viewer.following; const rkey = follow.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.follow", rkey, }, }); return res.data; }
async createListItemRecord(listUri, subjectDid) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.listitem", record: { createdAt: getCurrentTimestamp(), subject: subjectDid, list: listUri, }, }, }); return res.data; }
async deleteListItemRecord(listItemUri) { const rkey = listItemUri.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.listitem", rkey, }, }); return res.data; }
async getListItems({ limit = 100, cursor = "" } = {}) { const query = { repo: this.session.did, collection: "app.bsky.graph.listitem", limit, }; if (cursor) { query.cursor = cursor; } const res = await this.request("com.atproto.repo.listRecords", { query }); return res.data; }
async getPostThread(postUri, { labelers = [], depth = 6 } = {}) { const res = await this.request(`app.bsky.feed.getPostThread`, { query: { uri: postUri, depth, parentHeight: 1000, // max height, just so we don't set the wrong reply root by accident. This should be really rare - the default is 80. }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.thread; }
async getPostThreadOther(postUri, { labelers = [] } = {}) { const res = await this.request(`app.bsky.unspecced.getPostThreadOtherV2`, { query: { anchor: postUri }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), }, }); return res.data.thread; }
async getFeed(feedURI, { limit = 31, cursor = "", labelers = [] } = {}) { const res = await this.request(`app.bsky.feed.getFeed`, { query: { feed: feedURI, limit, cursor, }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getFeedGenerator(feedURI) { const res = await this.request(`app.bsky.feed.getFeedGenerator`, { query: { feed: feedURI }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.view; // note- returning the view object. }
async getFeedGenerators(feedURIs) { const res = await this.request(`app.bsky.feed.getFeedGenerators`, { query: { feeds: feedURIs }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.feeds; }
async getStarterPack(starterPackURI) { const res = await this.request(`app.bsky.graph.getStarterPack`, { query: { starterPack: starterPackURI }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.starterPack; }
async getList(listURI, { limit = 1, cursor = "" } = {}) { const query = { list: listURI, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request(`app.bsky.graph.getList`, { query, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getListFeed(listURI, { limit = 31, cursor = "", labelers = [] } = {}) { const query = { list: listURI, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request(`app.bsky.feed.getListFeed`, { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getActorFeeds(did, { limit = 50, cursor = "" } = {}) { const query = { actor: did, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request(`app.bsky.feed.getActorFeeds`, { query, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getListsWithMembership(actor, { limit = 50, cursor = "" } = {}) { const query = { actor, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getListsWithMembership", { query, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getActorLists(did, { limit = 50, cursor = "" } = {}) { const query = { actor: did, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request(`app.bsky.graph.getLists`, { query, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async searchFeedGenerators(query, { limit = 15, cursor = "" } = {}) { const queryParams = { limit, query }; if (cursor) { queryParams.cursor = cursor; } const res = await this.request( `app.bsky.unspecced.getPopularFeedGenerators`, { query: queryParams, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }, ); return res.data; }
async getFollowingFeed({ limit = 31, cursor = "", labelers = [] } = {}) { const res = await this.request(`app.bsky.feed.getTimeline`, { query: { limit, cursor }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
// Returns the posts in request order. The result may still be a subset // because the app view omits deleted and blocked posts async getPosts(postURIs, { labelers = [] } = {}) { const batches = batch(postURIs, 25); let posts = []; for (const batch of batches) { const res = await this.request(`app.bsky.feed.getPosts`, { query: { uris: batch }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); posts.push(...res.data.posts); } const orderByUri = new Map(postURIs.map((uri, index) => [uri, index])); posts.sort( (a, b) => (orderByUri.get(a.uri) ?? Infinity) - (orderByUri.get(b.uri) ?? Infinity), ); return posts; }
async getPost(postUri, { labelers = [] } = {}) { const posts = await this.getPosts([postUri], { labelers }); if (posts.length === 0) { throw new Error(`Post not found: ${postUri}`); } return posts[0]; }
async getRecord(uri) { const { repo, rkey, collection } = parseUri(uri); const res = await this.request(`com.atproto.repo.getRecord`, { query: { repo, collection, rkey, }, }); return res.data; }
async getRepost(repostUri) { return this.getRecord(repostUri); }
async getReposts(repostUris) { const reposts = []; // Batch to avoid rate limiting const batches = batch(repostUris, 5); for (const batch of batches) { const results = await Promise.allSettled( batch.map((repostUri) => this.getRepost(repostUri)), ); // Only keep successful responses. This is similar to getPosts() const successfulResponses = results .filter((result) => result.status === "fulfilled") .map((result) => result.value); reposts.push(...successfulResponses); } return reposts; }
async getProfile(did, { labelers = [] } = {}) { const res = await this.request(`app.bsky.actor.getProfile`, { query: { actor: did }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getProfiles(dids, { labelers = [] } = {}) { const results = await Promise.all( batch(dids, 25).map((chunk) => this.request(`app.bsky.actor.getProfiles`, { query: { actors: chunk }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }), ), ); return results.flatMap((res) => res.data.profiles); }
async searchProfiles(query, { limit = 10, cursor = "", labelers = [] } = {}) { const queryParams = { q: query, limit }; if (cursor) { queryParams.cursor = cursor; } const res = await this.request(`app.bsky.actor.searchActors`, { query: queryParams, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async searchProfilesTypeahead(query, { limit = 12, labelers = [] } = {}) { const res = await this.request(`app.bsky.actor.searchActorsTypeahead`, { query: { q: query, limit }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async searchPosts( query, { limit = 25, sort = "top", cursor = "", labelers = [] } = {}, ) { const queryParams = { q: query, limit, sort }; if (cursor) { queryParams.cursor = cursor; } const res = await this.request(`app.bsky.feed.searchPosts`, { query: queryParams, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async sendInteractions(interactions, feedProxyUrl) { // Interactions are only useful to the feed generator that served the // posts, so callers must route to one. if (!feedProxyUrl) { throw new Error("sendInteractions requires a feedProxyUrl"); } await this.request(`app.bsky.feed.sendInteractions`, { method: "POST", body: { interactions }, headers: { "atproto-proxy": feedProxyUrl, }, parseJson: false, // third-party feed might not return JSON }); }
async getAuthorFeed( did, { limit = 31, cursor = "", filter = "posts_and_author_threads", includePins = false, labelers = [], } = {}, ) { const res = await this.request(`app.bsky.feed.getAuthorFeed`, { query: { actor: did, limit, cursor, filter, includePins }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getActorLikes(did, { limit = 31, cursor = "", labelers = [] } = {}) { const query = { actor: did, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request(`app.bsky.feed.getActorLikes`, { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getPreferences() { const res = await this.request(`app.bsky.actor.getPreferences`); // note - no atproto-proxy for this endpoint return res.data.preferences; }
async updatePreferences(preferencesObj) { const res = await this.request(`app.bsky.actor.putPreferences`, { method: "POST", body: { preferences: preferencesObj }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res; }
async getLabelers(labelerDids) { const res = await this.request(`app.bsky.labeler.getServices`, { query: { dids: labelerDids, detailed: true }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.views; }
async getLabeler(labelerDid) { const labelers = await this.getLabelers([labelerDid]); return labelers[0]; }
async getSession() { const res = await this.request("com.atproto.server.getSession", { method: "GET", }); return res.data; }
async getNumNotifications() { const res = await this.request("app.bsky.notification.getUnreadCount", { headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data.count; }
async getNotifications({ cursor, limit = 31, reasons, labelers = [] } = {}) { const query = { cursor: cursor ?? "", limit }; if (reasons?.length) { query.reasons = reasons; } const res = await this.request("app.bsky.notification.listNotifications", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async markNotificationsAsRead() { await this.request("app.bsky.notification.updateSeen", { method: "POST", body: { seenAt: getCurrentTimestamp() }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); }
async listConvos({ cursor, limit = 30, readState, status, labelers = [], } = {}) { const query = { limit }; if (cursor) { query.cursor = cursor; } if (readState) { query.readState = readState; } if (status) { query.status = status; } const res = await this.request("chat.bsky.convo.listConvos", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getConvo(convoId, { labelers = [] } = {}) { const res = await this.request("chat.bsky.convo.getConvo", { query: { convoId }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getConvoMembers(convoId, { cursor, limit = 50, labelers = [] } = {}) { const query = { convoId, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("chat.bsky.convo.getConvoMembers", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getMessages(convoId, { cursor, limit = 50, labelers = [] } = {}) { const query = { convoId, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("chat.bsky.convo.getMessages", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async sendMessage(convoId, { text, facets, replyTo, embed }) { const message = { text, facets }; if (replyTo) { message.replyTo = replyTo; } if (embed) { message.embed = embed; } const res = await this.request("chat.bsky.convo.sendMessage", { method: "POST", body: { convoId, message, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async acceptConvo(convoId) { const res = await this.request("chat.bsky.convo.acceptConvo", { method: "POST", body: { convoId, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async leaveConvo(convoId) { const res = await this.request("chat.bsky.convo.leaveConvo", { method: "POST", body: { convoId, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getConvoAvailability(memberDids, { labelers = [] } = {}) { const res = await this.request("chat.bsky.convo.getConvoAvailability", { query: { members: memberDids }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getConvoForMembers(memberDids, { labelers = [] } = {}) { const res = await this.request("chat.bsky.convo.getConvoForMembers", { query: { members: memberDids }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getChatLogs({ cursor, labelers = [] }) { const res = await this.request("chat.bsky.convo.getLog", { query: { cursor }, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async markConvoAsRead(convoId) { await this.request("chat.bsky.convo.updateRead", { method: "POST", body: { convoId, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); }
async getChatUnreadCounts({ includeGroupChats = true } = {}) { const res = await this.request("chat.bsky.convo.getUnreadCounts", { query: { includeGroupChats }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async addMessageReaction(convoId, messageId, emoji) { const res = await this.request("chat.bsky.convo.addReaction", { method: "POST", body: { convoId, messageId, value: emoji, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data.message; }
async removeMessageReaction(convoId, messageId, emoji) { const res = await this.request("chat.bsky.convo.removeReaction", { method: "POST", body: { convoId, messageId, value: emoji, }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data.message; }
async getJoinLinkPreviews(codes) { const res = await this.request("chat.bsky.group.getJoinLinkPreviews", { query: { codes }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async requestJoinGroupChat(code) { const res = await this.request("chat.bsky.group.requestJoin", { method: "POST", body: { code }, headers: { "atproto-proxy": this.chatAppViewServiceDid, }, }); return res.data; }
async getLikes(postUri, { limit = 50, cursor, labelers = [] } = {}) { const query = { uri: postUri, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.feed.getLikes", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getQuotes(postUri, { limit = 50, cursor, labelers = [] } = {}) { const query = { uri: postUri, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.feed.getQuotes", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getRepostedBy(postUri, { limit = 50, cursor, labelers = [] } = {}) { const query = { uri: postUri, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.feed.getRepostedBy", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getBookmarks({ limit = 31, cursor, labelers = [] } = {}) { const query = { limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.bookmark.getBookmarks", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getFollowers(actor, { limit = 50, cursor, labelers = [] } = {}) { const query = { actor, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getFollowers", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getKnownFollowers(actor, { limit = 50, cursor, labelers = [] } = {}) { const query = { actor, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getKnownFollowers", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getFollows(actor, { limit = 50, cursor, labelers = [] } = {}) { const query = { actor, limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getFollows", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async putActivitySubscription(did, activitySubscription) { const res = await this.request( "app.bsky.notification.putActivitySubscription", { method: "POST", body: { subject: did, activitySubscription, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, }, ); return res.data; }
async muteActor(did) { const res = await this.request("app.bsky.graph.muteActor", { method: "POST", body: { actor: did, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res; }
async unmuteActor(did) { const res = await this.request("app.bsky.graph.unmuteActor", { method: "POST", body: { actor: did, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res; }
async blockActor(profile) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.block", record: { createdAt: getCurrentTimestamp(), subject: profile.did, }, }, }); return res.data; }
async getBlocks({ limit = 50, cursor, labelers = [] } = {}) { const query = { limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getBlocks", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async getMutes({ limit = 50, cursor, labelers = [] } = {}) { const query = { limit }; if (cursor) { query.cursor = cursor; } const res = await this.request("app.bsky.graph.getMutes", { query, headers: { "atproto-accept-labelers": buildAcceptLabelersHeader(labelers), "atproto-proxy": this.bskyAppViewServiceDid, }, }); return res.data; }
async unblockActor(profile) { const block = profile.viewer.blocking; const rkey = block.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.block", rkey, }, }); return res.data; }
async muteModList(listUri) { const res = await this.request("app.bsky.graph.muteActorList", { method: "POST", body: { list: listUri, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res; }
async unmuteModList(listUri) { const res = await this.request("app.bsky.graph.unmuteActorList", { method: "POST", body: { list: listUri, }, headers: { "atproto-proxy": this.bskyAppViewServiceDid, }, parseJson: false, }); return res; }
async blockModList(listUri) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.listblock", record: { createdAt: getCurrentTimestamp(), subject: listUri, }, }, }); return res.data; }
async unblockModList(blockUri) { const rkey = blockUri.split("/").pop(); const res = await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.listblock", rkey, }, }); return res.data; }
async applyWrites(writes) { const res = await this.request("com.atproto.repo.applyWrites", { method: "POST", body: { repo: this.session.did, writes, validate: true, }, }); return res.data; }
async deletePost(post) { const { rkey } = parseUri(post.uri); await this.request("com.atproto.repo.deleteRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.feed.post", rkey, }, }); }
async uploadBlob(blob) { const res = await this.request("com.atproto.repo.uploadBlob", { method: "POST", headers: { "Content-Type": blob.type, }, body: blob, stringifyBody: false, }); return res.data.blob; }
async getServiceAuthToken({ aud, lxm, exp }) { const res = await this.request("com.atproto.server.getServiceAuth", { query: { aud, lxm, exp: exp ?? Math.floor(Date.now() / 1000) + 60 }, }); return res.data.token; }
async serviceRequest( url, { token, method = "GET", query, body, headers = {} } = {}, ) { let queryString = ""; if (query) { queryString = "?" + buildQueryString(query); } const res = await fetch(`${url}${queryString}`, { method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...headers, }, body, }); const data = await res.json(); res.data = data; if (!res.ok) { throw new ApiError(res); } return res; }
async getVideoUploadLimits() { const token = await this.getServiceAuthToken({ aud: VIDEO_SERVICE_DID, lxm: "app.bsky.video.getUploadLimits", }); const res = await this.serviceRequest( `${VIDEO_SERVICE_URL}/xrpc/app.bsky.video.getUploadLimits`, { token }, ); return res.data; }
async uploadVideoBlob(file) { const pdsHostname = new URL(this.session.serviceEndpoint).hostname; const token = await this.getServiceAuthToken({ aud: `did:web:${pdsHostname}`, lxm: "com.atproto.repo.uploadBlob", exp: Math.floor(Date.now() / 1000) + 60 * 30, }); try { const res = await this.serviceRequest( `${VIDEO_SERVICE_URL}/xrpc/app.bsky.video.uploadVideo`, { token, method: "POST", query: { did: this.session.did, name: file.name }, headers: { "Content-Type": file.type }, body: file, }, ); return res.data; } catch (error) { // If the same video has been uploaded before, the service returns 409 // Treat this as a success if ( error instanceof ApiError && error.data?.error === "already_exists" && error.data.jobId ) { return error.data; } throw error; } }
async getVideoJobStatus(jobId) { const res = await this.serviceRequest( `${VIDEO_SERVICE_URL}/xrpc/app.bsky.video.getJobStatus`, { query: { jobId } }, ); return res.data.jobStatus; }
async getProfileRecord() { const res = await this.request("com.atproto.repo.getRecord", { query: { repo: this.session.did, collection: "app.bsky.actor.profile", rkey: "self", }, }); return res.data; }
async putProfileRecord(record, swapRecord) { const res = await this.request("com.atproto.repo.putRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.actor.profile", rkey: "self", record: { $type: "app.bsky.actor.profile", ...record, }, swapRecord: swapRecord ?? null, }, }); return res.data; }
async createListRecord(record) { const res = await this.request("com.atproto.repo.createRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.list", record: { $type: "app.bsky.graph.list", ...record, }, }, }); return res.data; }
async getListRecord(rkey) { const res = await this.request("com.atproto.repo.getRecord", { query: { repo: this.session.did, collection: "app.bsky.graph.list", rkey, }, }); return res.data; }
async putListRecord(rkey, record, swapRecord) { const res = await this.request("com.atproto.repo.putRecord", { method: "POST", body: { repo: this.session.did, collection: "app.bsky.graph.list", rkey, record: { $type: "app.bsky.graph.list", ...record, }, swapRecord: swapRecord ?? null, }, }); return res.data; }
async createModerationReport({ reasonType, reason, subject, labelerDid }) { const body = { reasonType, subject, }; // Reason is optional if (reason) { body.reason = reason; } const res = await this.request("com.atproto.moderation.createReport", { method: "POST", body, headers: { "atproto-proxy": `${labelerDid}#atproto_labeler`, }, }); return res.data; }}