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}