diff options
author | Hailey <me@haileyok.com> | 2024-08-12 14:00:15 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-12 14:00:15 -0700 |
commit | 7df2327424e948e54b9731e5ab651e889f38a772 (patch) | |
tree | cd6513394de29696124374d1a72bd4cd78cbc1a7 /src/lib/api | |
parent | ae883e2df7bc53baca215fba527fe113e71cb5c2 (diff) | |
download | voidsky-7df2327424e948e54b9731e5ab651e889f38a772.tar.zst |
Upgrade API, implement XRPC rework (#4857)
Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
Diffstat (limited to 'src/lib/api')
-rw-r--r-- | src/lib/api/api-polyfill.ts | 85 | ||||
-rw-r--r-- | src/lib/api/api-polyfill.web.ts | 3 | ||||
-rw-r--r-- | src/lib/api/feed/custom.ts | 25 | ||||
-rw-r--r-- | src/lib/api/index.ts | 39 | ||||
-rw-r--r-- | src/lib/api/upload-blob.ts | 82 | ||||
-rw-r--r-- | src/lib/api/upload-blob.web.ts | 26 |
6 files changed, 124 insertions, 136 deletions
diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts deleted file mode 100644 index e3aec7631..000000000 --- a/src/lib/api/api-polyfill.ts +++ /dev/null @@ -1,85 +0,0 @@ -import RNFS from 'react-native-fs' -import {BskyAgent, jsonToLex, stringifyLex} from '@atproto/api' - -const GET_TIMEOUT = 15e3 // 15s -const POST_TIMEOUT = 60e3 // 60s - -export function doPolyfill() { - BskyAgent.configure({fetch: fetchHandler}) -} - -interface FetchHandlerResponse { - status: number - headers: Record<string, string> - body: any -} - -async function fetchHandler( - reqUri: string, - reqMethod: string, - reqHeaders: Record<string, string>, - reqBody: any, -): Promise<FetchHandlerResponse> { - const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] - if (reqMimeType && reqMimeType.startsWith('application/json')) { - reqBody = stringifyLex(reqBody) - } else if ( - typeof reqBody === 'string' && - (reqBody.startsWith('/') || reqBody.startsWith('file:')) - ) { - if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) { - // HACK - // React native has a bug that inflates the size of jpegs on upload - // we get around that by renaming the file ext to .bin - // see https://github.com/facebook/react-native/issues/27099 - // -prf - const newPath = reqBody.replace(/\.jpe?g$/, '.bin') - await RNFS.moveFile(reqBody, newPath) - reqBody = newPath - } - // NOTE - // React native treats bodies with {uri: string} as file uploads to pull from cache - // -prf - reqBody = {uri: reqBody} - } - - const controller = new AbortController() - const to = setTimeout( - () => controller.abort(), - reqMethod === 'post' ? POST_TIMEOUT : GET_TIMEOUT, - ) - - const res = await fetch(reqUri, { - method: reqMethod, - headers: reqHeaders, - body: reqBody, - signal: controller.signal, - }) - - const resStatus = res.status - const resHeaders: Record<string, string> = {} - res.headers.forEach((value: string, key: string) => { - resHeaders[key] = value - }) - const resMimeType = resHeaders['Content-Type'] || resHeaders['content-type'] - let resBody - if (resMimeType) { - if (resMimeType.startsWith('application/json')) { - resBody = jsonToLex(await res.json()) - } else if (resMimeType.startsWith('text/')) { - resBody = await res.text() - } else if (resMimeType === 'application/vnd.ipld.car') { - resBody = await res.arrayBuffer() - } else { - throw new Error('Non-supported mime type') - } - } - - clearTimeout(to) - - return { - status: resStatus, - headers: resHeaders, - body: resBody, - } -} diff --git a/src/lib/api/api-polyfill.web.ts b/src/lib/api/api-polyfill.web.ts deleted file mode 100644 index 1ad22b3d0..000000000 --- a/src/lib/api/api-polyfill.web.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function doPolyfill() { - // no polyfill is needed on web -} diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index eb54dd29c..6db96a8d6 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,7 +1,6 @@ import { AppBskyFeedDefs, AppBskyFeedGetFeed as GetCustomFeed, - AtpAgent, BskyAgent, } from '@atproto/api' @@ -51,7 +50,7 @@ export class CustomFeedAPI implements FeedAPI { const agent = this.agent const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) - const res = agent.session + const res = agent.did ? await this.agent.app.bsky.feed.getFeed( { ...this.params, @@ -106,34 +105,32 @@ async function loggedOutFetch({ let contentLangs = getContentLanguages().join(',') // manually construct fetch call so we can add the `lang` cache-busting param - let res = await AtpAgent.fetch!( + let res = await fetch( `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}&lang=${contentLangs}`, - 'GET', - {'Accept-Language': contentLangs}, - undefined, + {method: 'GET', headers: {'Accept-Language': contentLangs}}, ) - if (res.body?.feed?.length) { + let data = res.ok ? await res.json() : null + if (data?.feed?.length) { return { success: true, - data: res.body, + data, } } // no data, try again with language headers removed - res = await AtpAgent.fetch!( + res = await fetch( `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}`, - 'GET', - {'Accept-Language': ''}, - undefined, + {method: 'GET', headers: {'Accept-Language': ''}}, ) - if (res.body?.feed?.length) { + data = res.ok ? await res.json() : null + if (data?.feed?.length) { return { success: true, - data: res.body, + data, } } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 12e30bf6c..658ed78de 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -6,7 +6,6 @@ import { AppBskyFeedThreadgate, BskyAgent, ComAtprotoLabelDefs, - ComAtprotoRepoUploadBlob, RichText, } from '@atproto/api' import {AtUri} from '@atproto/api' @@ -15,10 +14,13 @@ import {logger} from '#/logger' import {ThreadgateSetting} from '#/state/queries/threadgate' import {isNetworkError} from 'lib/strings/errors' import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip' -import {isNative, isWeb} from 'platform/detection' +import {isNative} from 'platform/detection' import {ImageModel} from 'state/models/media/image' import {LinkMeta} from '../link-meta/link-meta' import {safeDeleteAsync} from '../media/manip' +import {uploadBlob} from './upload-blob' + +export {uploadBlob} export interface ExternalEmbedDraft { uri: string @@ -28,25 +30,6 @@ export interface ExternalEmbedDraft { localThumb?: ImageModel } -export async function uploadBlob( - agent: BskyAgent, - blob: string, - encoding: string, -): Promise<ComAtprotoRepoUploadBlob.Response> { - if (isWeb) { - // `blob` should be a data uri - return agent.uploadBlob(convertDataURIToUint8Array(blob), { - encoding, - }) - } else { - // `blob` should be a path to a file in the local FS - return agent.uploadBlob( - blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts - {encoding}, - ) - } -} - interface PostOpts { rawText: string replyTo?: string @@ -301,7 +284,7 @@ export async function createThreadgate( const postUrip = new AtUri(postUri) await agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + repo: agent.accountDid, collection: 'app.bsky.feed.threadgate', rkey: postUrip.rkey, record: { @@ -312,15 +295,3 @@ export async function createThreadgate( }, }) } - -// helpers -// = - -function convertDataURIToUint8Array(uri: string): Uint8Array { - var raw = window.atob(uri.substring(uri.indexOf(';base64,') + 8)) - var binary = new Uint8Array(new ArrayBuffer(raw.length)) - for (let i = 0; i < raw.length; i++) { - binary[i] = raw.charCodeAt(i) - } - return binary -} diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts new file mode 100644 index 000000000..0814d5185 --- /dev/null +++ b/src/lib/api/upload-blob.ts @@ -0,0 +1,82 @@ +import RNFS from 'react-native-fs' +import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api' + +/** + * @param encoding Allows overriding the blob's type + */ +export async function uploadBlob( + agent: BskyAgent, + input: string | Blob, + encoding?: string, +): Promise<ComAtprotoRepoUploadBlob.Response> { + if (typeof input === 'string' && input.startsWith('file:')) { + const blob = await asBlob(input) + return agent.uploadBlob(blob, {encoding}) + } + + if (typeof input === 'string' && input.startsWith('/')) { + const blob = await asBlob(`file://${input}`) + return agent.uploadBlob(blob, {encoding}) + } + + if (typeof input === 'string' && input.startsWith('data:')) { + const blob = await fetch(input).then(r => r.blob()) + return agent.uploadBlob(blob, {encoding}) + } + + if (input instanceof Blob) { + return agent.uploadBlob(input, {encoding}) + } + + throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) +} + +async function asBlob(uri: string): Promise<Blob> { + return withSafeFile(uri, async safeUri => { + // Note + // Android does not support `fetch()` on `file://` URIs. for this reason, we + // use XMLHttpRequest instead of simply calling: + + // return fetch(safeUri.replace('file:///', 'file:/')).then(r => r.blob()) + + return await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.onload = () => resolve(xhr.response) + xhr.onerror = () => reject(new Error('Failed to load blob')) + xhr.responseType = 'blob' + xhr.open('GET', safeUri, true) + xhr.send(null) + }) + }) +} + +// HACK +// React native has a bug that inflates the size of jpegs on upload +// we get around that by renaming the file ext to .bin +// see https://github.com/facebook/react-native/issues/27099 +// -prf +async function withSafeFile<T>( + uri: string, + fn: (path: string) => Promise<T>, +): Promise<T> { + if (uri.endsWith('.jpeg') || uri.endsWith('.jpg')) { + // Since we don't "own" the file, we should avoid renaming or modifying it. + // Instead, let's copy it to a temporary file and use that (then remove the + // temporary file). + const newPath = uri.replace(/\.jpe?g$/, '.bin') + try { + await RNFS.copyFile(uri, newPath) + } catch { + // Failed to copy the file, just use the original + return await fn(uri) + } + try { + return await fn(newPath) + } finally { + // Remove the temporary file + await RNFS.unlink(newPath) + } + } else { + return fn(uri) + } +} diff --git a/src/lib/api/upload-blob.web.ts b/src/lib/api/upload-blob.web.ts new file mode 100644 index 000000000..d3c52190c --- /dev/null +++ b/src/lib/api/upload-blob.web.ts @@ -0,0 +1,26 @@ +import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api' + +/** + * @note It is recommended, on web, to use the `file` instance of the file + * selector input element, rather than a `data:` URL, to avoid + * loading the file into memory. `File` extends `Blob` "file" instances can + * be passed directly to this function. + */ +export async function uploadBlob( + agent: BskyAgent, + input: string | Blob, + encoding?: string, +): Promise<ComAtprotoRepoUploadBlob.Response> { + if (typeof input === 'string' && input.startsWith('data:')) { + const blob = await fetch(input).then(r => r.blob()) + return agent.uploadBlob(blob, {encoding}) + } + + if (input instanceof Blob) { + return agent.uploadBlob(input, { + encoding, + }) + } + + throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) +} |