Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 234 lines 6.4 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 let resolvedUri = uri 85 if (isShortLink(resolvedUri)) { 86 resolvedUri = await resolveShortLink(resolvedUri) 87 } 88 if (isBskyPostUrl(uri)) { 89 uri = convertBskyAppUrlIfNeeded(uri) 90 const [_0, user, _1, rkey] = uri.split('/').filter(Boolean) 91 const recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey) 92 const post = await getPost({uri: recordUri}) 93 if (post.viewer?.embeddingDisabled) { 94 throw new EmbeddingDisabledError() 95 } 96 return { 97 type: 'record', 98 record: { 99 cid: post.cid, 100 uri: post.uri, 101 }, 102 kind: 'post', 103 view: post, 104 } 105 } 106 if (isBskyCustomFeedUrl(resolvedUri)) { 107 resolvedUri = convertBskyAppUrlIfNeeded(resolvedUri) 108 const [_0, handleOrDid, _1, rkey] = resolvedUri.split('/').filter(Boolean) 109 const did = await fetchDid(handleOrDid) 110 const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey) 111 const res = await agent.app.bsky.feed.getFeedGenerator({feed}) 112 return { 113 type: 'record', 114 record: { 115 uri: res.data.view.uri, 116 cid: res.data.view.cid, 117 }, 118 kind: 'feed', 119 view: res.data.view, 120 } 121 } 122 if (isBskyListUrl(resolvedUri)) { 123 resolvedUri = convertBskyAppUrlIfNeeded(resolvedUri) 124 const [_0, handleOrDid, _1, rkey] = resolvedUri.split('/').filter(Boolean) 125 const did = await fetchDid(handleOrDid) 126 const list = makeRecordUri(did, 'app.bsky.graph.list', rkey) 127 const res = await agent.app.bsky.graph.getList({list}) 128 return { 129 type: 'record', 130 record: { 131 uri: res.data.list.uri, 132 cid: res.data.list.cid, 133 }, 134 kind: 'list', 135 view: res.data.list, 136 } 137 } 138 if (isBskyStartUrl(resolvedUri) || isBskyStarterPackUrl(resolvedUri)) { 139 const parsed = parseStarterPackUri(resolvedUri) 140 if (!parsed) { 141 throw new Error( 142 'Unexpectedly called getStarterPackAsEmbed with a non-starterpack url', 143 ) 144 } 145 const did = await fetchDid(parsed.name) 146 const starterPack = createStarterPackUri({did, rkey: parsed.rkey}) 147 const res = await agent.app.bsky.graph.getStarterPack({starterPack}) 148 return { 149 type: 'record', 150 record: { 151 uri: res.data.starterPack.uri, 152 cid: res.data.starterPack.cid, 153 }, 154 kind: 'starter-pack', 155 view: res.data.starterPack, 156 } 157 } 158 return resolveExternal(agent, resolvedUri) 159 160 // Forked from useGetPost. TODO: move into RQ. 161 async function getPost({uri}: {uri: string}) { 162 const urip = new AtUri(uri) 163 if (!urip.host.startsWith('did:')) { 164 const res = await agent.resolveHandle({ 165 handle: urip.host, 166 }) 167 // @ts-expect-error TODO new-sdk-migration 168 urip.host = res.data.did 169 } 170 const res = await agent.getPosts({ 171 uris: [urip.toString()], 172 }) 173 if (res.success && res.data.posts[0]) { 174 return res.data.posts[0] 175 } 176 throw new Error('getPost: post not found') 177 } 178 179 // Forked from useFetchDid. TODO: move into RQ. 180 async function fetchDid(handleOrDid: string) { 181 let identifier = handleOrDid 182 if (!identifier.startsWith('did:')) { 183 const res = await agent.resolveHandle({handle: identifier}) 184 identifier = res.data.did 185 } 186 return identifier 187 } 188} 189 190export async function resolveGif( 191 agent: BskyAgent, 192 gif: Gif, 193): Promise<ResolvedExternalLink> { 194 const uri = `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}` 195 return { 196 type: 'external', 197 uri, 198 title: gif.content_description, 199 description: createGIFDescription(gif.content_description), 200 thumb: await imageToThumb(gif.media_formats.preview.url), 201 } 202} 203 204async function resolveExternal( 205 agent: BskyAgent, 206 uri: string, 207): Promise<ResolvedExternalLink> { 208 const result = await getLinkMeta(agent, uri) 209 return { 210 type: 'external', 211 uri: result.url, 212 title: result.title ?? '', 213 description: result.description ?? '', 214 thumb: result.image ? await imageToThumb(result.image) : undefined, 215 } 216} 217 218export async function imageToThumb( 219 imageUri: string, 220): Promise<ComposerImage | undefined> { 221 try { 222 const img = await downloadAndResize({ 223 uri: imageUri, 224 width: POST_IMG_MAX.width, 225 height: POST_IMG_MAX.height, 226 mode: 'contain', 227 maxSize: POST_IMG_MAX.size, 228 timeout: 15e3, 229 }) 230 if (img) { 231 return await createComposerImage(img) 232 } 233 } catch {} 234}