Bluesky app fork with some witchin' additions 馃挮
at b4e1fdfccfda7150f5053dec5a94fe05bb7ed17e 233 lines 6.3 kB view raw
1import { 2 type AppBskyFeedDefs, 3 type AppBskyGraphDefs, 4 type ComAtprotoRepoStrongRef, 5} from '@atproto/api' 6import {AtUri} from '@atproto/api' 7import {type BskyAgent} from '@atproto/api' 8 9import {POST_IMG_MAX} from '#/lib/constants' 10import {getLinkMeta} from '#/lib/link-meta/link-meta' 11import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' 12import {downloadAndResize} from '#/lib/media/manip' 13import { 14 createStarterPackUri, 15 parseStarterPackUri, 16} from '#/lib/strings/starter-pack' 17import { 18 isBskyCustomFeedUrl, 19 isBskyListUrl, 20 isBskyPostUrl, 21 isBskyStarterPackUrl, 22 isBskyStartUrl, 23 isShortLink, 24} from '#/lib/strings/url-helpers' 25import {type ComposerImage} from '#/state/gallery' 26import {createComposerImage} from '#/state/gallery' 27import {type Gif} from '#/state/queries/tenor' 28import {createGIFDescription} from '../gif-alt-text' 29import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' 30 31type ResolvedExternalLink = { 32 type: 'external' 33 uri: string 34 title: string 35 description: string 36 thumb: ComposerImage | undefined 37} 38 39type ResolvedPostRecord = { 40 type: 'record' 41 record: ComAtprotoRepoStrongRef.Main 42 kind: 'post' 43 view: AppBskyFeedDefs.PostView 44} 45 46type ResolvedFeedRecord = { 47 type: 'record' 48 record: ComAtprotoRepoStrongRef.Main 49 kind: 'feed' 50 view: AppBskyFeedDefs.GeneratorView 51} 52 53type ResolvedListRecord = { 54 type: 'record' 55 record: ComAtprotoRepoStrongRef.Main 56 kind: 'list' 57 view: AppBskyGraphDefs.ListView 58} 59 60type ResolvedStarterPackRecord = { 61 type: 'record' 62 record: ComAtprotoRepoStrongRef.Main 63 kind: 'starter-pack' 64 view: AppBskyGraphDefs.StarterPackView 65} 66 67export type ResolvedLink = 68 | ResolvedExternalLink 69 | ResolvedPostRecord 70 | ResolvedFeedRecord 71 | ResolvedListRecord 72 | ResolvedStarterPackRecord 73 74export class EmbeddingDisabledError extends Error { 75 constructor() { 76 super('Embedding is disabled for this record') 77 } 78} 79 80export async function resolveLink( 81 agent: BskyAgent, 82 uri: string, 83): Promise<ResolvedLink> { 84 if (isShortLink(uri)) { 85 uri = await resolveShortLink(uri) 86 } 87 if (isBskyPostUrl(uri)) { 88 uri = convertBskyAppUrlIfNeeded(uri) 89 const [_0, user, _1, rkey] = uri.split('/').filter(Boolean) 90 const recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey) 91 const post = await getPost({uri: recordUri}) 92 if (post.viewer?.embeddingDisabled) { 93 throw new EmbeddingDisabledError() 94 } 95 return { 96 type: 'record', 97 record: { 98 cid: post.cid, 99 uri: post.uri, 100 }, 101 kind: 'post', 102 view: post, 103 } 104 } 105 if (isBskyCustomFeedUrl(uri)) { 106 uri = convertBskyAppUrlIfNeeded(uri) 107 const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) 108 const did = await fetchDid(handleOrDid) 109 const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey) 110 const res = await agent.app.bsky.feed.getFeedGenerator({feed}) 111 return { 112 type: 'record', 113 record: { 114 uri: res.data.view.uri, 115 cid: res.data.view.cid, 116 }, 117 kind: 'feed', 118 view: res.data.view, 119 } 120 } 121 if (isBskyListUrl(uri)) { 122 uri = convertBskyAppUrlIfNeeded(uri) 123 const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) 124 const did = await fetchDid(handleOrDid) 125 const list = makeRecordUri(did, 'app.bsky.graph.list', rkey) 126 const res = await agent.app.bsky.graph.getList({list}) 127 return { 128 type: 'record', 129 record: { 130 uri: res.data.list.uri, 131 cid: res.data.list.cid, 132 }, 133 kind: 'list', 134 view: res.data.list, 135 } 136 } 137 if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { 138 const parsed = parseStarterPackUri(uri) 139 if (!parsed) { 140 throw new Error( 141 'Unexpectedly called getStarterPackAsEmbed with a non-starterpack url', 142 ) 143 } 144 const did = await fetchDid(parsed.name) 145 const starterPack = createStarterPackUri({did, rkey: parsed.rkey}) 146 const res = await agent.app.bsky.graph.getStarterPack({starterPack}) 147 return { 148 type: 'record', 149 record: { 150 uri: res.data.starterPack.uri, 151 cid: res.data.starterPack.cid, 152 }, 153 kind: 'starter-pack', 154 view: res.data.starterPack, 155 } 156 } 157 return resolveExternal(agent, uri) 158 159 // Forked from useGetPost. TODO: move into RQ. 160 async function getPost({uri}: {uri: string}) { 161 const urip = new AtUri(uri) 162 if (!urip.host.startsWith('did:')) { 163 const res = await agent.resolveHandle({ 164 handle: urip.host, 165 }) 166 // @ts-expect-error TODO new-sdk-migration 167 urip.host = res.data.did 168 } 169 const res = await agent.getPosts({ 170 uris: [urip.toString()], 171 }) 172 if (res.success && res.data.posts[0]) { 173 return res.data.posts[0] 174 } 175 throw new Error('getPost: post not found') 176 } 177 178 // Forked from useFetchDid. TODO: move into RQ. 179 async function fetchDid(handleOrDid: string) { 180 let identifier = handleOrDid 181 if (!identifier.startsWith('did:')) { 182 const res = await agent.resolveHandle({handle: identifier}) 183 identifier = res.data.did 184 } 185 return identifier 186 } 187} 188 189export async function resolveGif( 190 agent: BskyAgent, 191 gif: Gif, 192): Promise<ResolvedExternalLink> { 193 const uri = `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}` 194 return { 195 type: 'external', 196 uri, 197 title: gif.content_description, 198 description: createGIFDescription(gif.content_description), 199 thumb: await imageToThumb(gif.media_formats.preview.url), 200 } 201} 202 203async function resolveExternal( 204 agent: BskyAgent, 205 uri: string, 206): Promise<ResolvedExternalLink> { 207 const result = await getLinkMeta(agent, uri) 208 return { 209 type: 'external', 210 uri: result.url, 211 title: result.title ?? '', 212 description: result.description ?? '', 213 thumb: result.image ? await imageToThumb(result.image) : undefined, 214 } 215} 216 217export async function imageToThumb( 218 imageUri: string, 219): Promise<ComposerImage | undefined> { 220 try { 221 const img = await downloadAndResize({ 222 uri: imageUri, 223 width: POST_IMG_MAX.width, 224 height: POST_IMG_MAX.height, 225 mode: 'contain', 226 maxSize: POST_IMG_MAX.size, 227 timeout: 15e3, 228 }) 229 if (img) { 230 return await createComposerImage(img) 231 } 232 } catch {} 233}