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