mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import { 2 AppBskyFeedDefs, 3 AppBskyGraphDefs, 4 ComAtprotoRepoStrongRef, 5} from '@atproto/api' 6import {AtUri} from '@atproto/api' 7import {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 {ComposerImage} from '#/state/gallery' 26import {createComposerImage} from '#/state/gallery' 27import {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 urip.host = res.data.did 167 } 168 const res = await agent.getPosts({ 169 uris: [urip.toString()], 170 }) 171 if (res.success && res.data.posts[0]) { 172 return res.data.posts[0] 173 } 174 throw new Error('getPost: post not found') 175 } 176 177 // Forked from useFetchDid. TODO: move into RQ. 178 async function fetchDid(handleOrDid: string) { 179 let identifier = handleOrDid 180 if (!identifier.startsWith('did:')) { 181 const res = await agent.resolveHandle({handle: identifier}) 182 identifier = res.data.did 183 } 184 return identifier 185 } 186} 187 188export async function resolveGif( 189 agent: BskyAgent, 190 gif: Gif, 191): Promise<ResolvedExternalLink> { 192 const uri = `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}` 193 return { 194 type: 'external', 195 uri, 196 title: gif.content_description, 197 description: createGIFDescription(gif.content_description), 198 thumb: await imageToThumb(gif.media_formats.preview.url), 199 } 200} 201 202async function resolveExternal( 203 agent: BskyAgent, 204 uri: string, 205): Promise<ResolvedExternalLink> { 206 const result = await getLinkMeta(agent, uri) 207 return { 208 type: 'external', 209 uri: result.url, 210 title: result.title ?? '', 211 description: result.description ?? '', 212 thumb: result.image ? await imageToThumb(result.image) : undefined, 213 } 214} 215 216async function imageToThumb( 217 imageUri: string, 218): Promise<ComposerImage | undefined> { 219 try { 220 const img = await downloadAndResize({ 221 uri: imageUri, 222 width: POST_IMG_MAX.width, 223 height: POST_IMG_MAX.height, 224 mode: 'contain', 225 maxSize: POST_IMG_MAX.size, 226 timeout: 15e3, 227 }) 228 if (img) { 229 return await createComposerImage(img) 230 } 231 } catch {} 232}