diff options
Diffstat (limited to 'src/lib/api/index.ts')
-rw-r--r-- | src/lib/api/index.ts | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts new file mode 100644 index 000000000..d800c376c --- /dev/null +++ b/src/lib/api/index.ts @@ -0,0 +1,201 @@ +import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api' +import {AtUri} from '../../third-party/uri' +import {RootStoreModel} from 'state/models/root-store' +import {extractEntities} from 'lib/strings/rich-text-detection' +import {isNetworkError} from 'lib/strings/errors' +import {LinkMeta} from '../link-meta/link-meta' +import {Image} from '../images' +import {RichText} from '../strings/rich-text' + +export interface ExternalEmbedDraft { + uri: string + isLoading: boolean + meta?: LinkMeta + localThumb?: Image +} + +export async function resolveName(store: RootStoreModel, didOrHandle: string) { + if (!didOrHandle) { + throw new Error('Invalid handle: ""') + } + if (didOrHandle.startsWith('did:')) { + return didOrHandle + } + const res = await store.api.com.atproto.handle.resolve({ + handle: didOrHandle, + }) + return res.data.did +} + +export async function post( + store: RootStoreModel, + rawText: string, + replyTo?: string, + extLink?: ExternalEmbedDraft, + images?: string[], + knownHandles?: Set<string>, + onStateChange?: (state: string) => void, +) { + let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined + let reply + const text = new RichText(rawText, undefined, { + cleanNewlines: true, + }).text.trim() + + onStateChange?.('Processing...') + const entities = extractEntities(text, knownHandles) + if (entities) { + for (const ent of entities) { + if (ent.type === 'mention') { + const prof = await store.profiles.getProfile(ent.value) + ent.value = prof.data.did + } + } + } + + if (images?.length) { + embed = { + $type: 'app.bsky.embed.images', + images: [], + } as AppBskyEmbedImages.Main + let i = 1 + for (const image of images) { + onStateChange?.(`Uploading image #${i++}...`) + const res = await store.api.com.atproto.blob.upload( + image, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts + {encoding: 'image/jpeg'}, + ) + embed.images.push({ + image: { + cid: res.data.cid, + mimeType: 'image/jpeg', + }, + alt: '', // TODO supply alt text + }) + } + } + + if (!embed && extLink) { + let thumb + if (extLink.localThumb) { + onStateChange?.('Uploading link thumbnail...') + let encoding + if (extLink.localThumb.path.endsWith('.png')) { + encoding = 'image/png' + } else if ( + extLink.localThumb.path.endsWith('.jpeg') || + extLink.localThumb.path.endsWith('.jpg') + ) { + encoding = 'image/jpeg' + } else { + store.log.warn( + 'Unexpected image format for thumbnail, skipping', + extLink.localThumb.path, + ) + } + if (encoding) { + const thumbUploadRes = await store.api.com.atproto.blob.upload( + extLink.localThumb.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts + {encoding}, + ) + thumb = { + cid: thumbUploadRes.data.cid, + mimeType: encoding, + } + } + } + embed = { + $type: 'app.bsky.embed.external', + external: { + uri: extLink.uri, + title: extLink.meta?.title || '', + description: extLink.meta?.description || '', + thumb, + }, + } as AppBskyEmbedExternal.Main + } + + if (replyTo) { + const replyToUrip = new AtUri(replyTo) + const parentPost = await store.api.app.bsky.feed.post.get({ + user: replyToUrip.host, + rkey: replyToUrip.rkey, + }) + if (parentPost) { + const parentRef = { + uri: parentPost.uri, + cid: parentPost.cid, + } + reply = { + root: parentPost.value.reply?.root || parentRef, + parent: parentRef, + } + } + } + + try { + onStateChange?.('Posting...') + return await store.api.app.bsky.feed.post.create( + {did: store.me.did || ''}, + { + text, + reply, + embed, + entities, + createdAt: new Date().toISOString(), + }, + ) + } catch (e: any) { + console.error(`Failed to create post: ${e.toString()}`) + if (isNetworkError(e)) { + throw new Error( + 'Post failed to upload. Please check your Internet connection and try again.', + ) + } else { + throw e + } + } +} + +export async function repost(store: RootStoreModel, uri: string, cid: string) { + return await store.api.app.bsky.feed.repost.create( + {did: store.me.did || ''}, + { + subject: {uri, cid}, + createdAt: new Date().toISOString(), + }, + ) +} + +export async function unrepost(store: RootStoreModel, repostUri: string) { + const repostUrip = new AtUri(repostUri) + return await store.api.app.bsky.feed.repost.delete({ + did: repostUrip.hostname, + rkey: repostUrip.rkey, + }) +} + +export async function follow( + store: RootStoreModel, + subjectDid: string, + subjectDeclarationCid: string, +) { + return await store.api.app.bsky.graph.follow.create( + {did: store.me.did || ''}, + { + subject: { + did: subjectDid, + declarationCid: subjectDeclarationCid, + }, + createdAt: new Date().toISOString(), + }, + ) +} + +export async function unfollow(store: RootStoreModel, followUri: string) { + const followUrip = new AtUri(followUri) + return await store.api.app.bsky.graph.follow.delete({ + did: followUrip.hostname, + rkey: followUrip.rkey, + }) +} |