about summary refs log tree commit diff
path: root/src/lib/api/resolve.ts
blob: a97a3f31c44329bb83ae30940de574a3c78acc08 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import {ComAtprotoRepoStrongRef} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {BskyAgent} from '@atproto/api'

import {POST_IMG_MAX} from '#/lib/constants'
import {
  getFeedAsEmbed,
  getListAsEmbed,
  getPostAsQuote,
  getStarterPackAsEmbed,
} from '#/lib/link-meta/bsky'
import {getLinkMeta} from '#/lib/link-meta/link-meta'
import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
import {downloadAndResize} from '#/lib/media/manip'
import {
  isBskyCustomFeedUrl,
  isBskyListUrl,
  isBskyPostUrl,
  isBskyStarterPackUrl,
  isBskyStartUrl,
  isShortLink,
} from '#/lib/strings/url-helpers'
import {ComposerImage} from '#/state/gallery'
import {createComposerImage} from '#/state/gallery'
import {Gif} from '#/state/queries/tenor'
import {createGIFDescription} from '../gif-alt-text'

type ResolvedExternalLink = {
  type: 'external'
  uri: string
  title: string
  description: string
  thumb: ComposerImage | undefined
}

type ResolvedRecord = {
  type: 'record'
  record: ComAtprotoRepoStrongRef.Main
}

type ResolvedLink = ResolvedExternalLink | ResolvedRecord

export async function resolveLink(
  agent: BskyAgent,
  uri: string,
): Promise<ResolvedLink> {
  if (isShortLink(uri)) {
    uri = await resolveShortLink(uri)
  }
  if (isBskyPostUrl(uri)) {
    // TODO: Remove this abstraction.
    // TODO: Nice error messages (e.g. EmbeddingDisabledError).
    const result = await getPostAsQuote(getPost, uri)
    return {
      type: 'record',
      record: {
        cid: result.cid,
        uri: result.uri,
      },
    }
  }
  if (isBskyCustomFeedUrl(uri)) {
    // TODO: Remove this abstraction.
    const result = await getFeedAsEmbed(agent, fetchDid, uri)
    return {
      type: 'record',
      record: result.embed!.record, // TODO: Fix types.
    }
  }
  if (isBskyListUrl(uri)) {
    // TODO: Remove this abstraction.
    const result = await getListAsEmbed(agent, fetchDid, uri)
    return {
      type: 'record',
      record: result.embed!.record, // TODO: Fix types.
    }
  }
  if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) {
    // TODO: Remove this abstraction.
    const result = await getStarterPackAsEmbed(agent, fetchDid, uri)
    return {
      type: 'record',
      record: result.embed!.record, // TODO: Fix types.
    }
  }
  return resolveExternal(agent, uri)

  // Forked from useGetPost. TODO: move into RQ.
  async function getPost({uri}: {uri: string}) {
    const urip = new AtUri(uri)
    if (!urip.host.startsWith('did:')) {
      const res = await agent.resolveHandle({
        handle: urip.host,
      })
      urip.host = res.data.did
    }
    const res = await agent.getPosts({
      uris: [urip.toString()],
    })
    if (res.success && res.data.posts[0]) {
      return res.data.posts[0]
    }
    throw new Error('getPost: post not found')
  }

  // Forked from useFetchDid. TODO: move into RQ.
  async function fetchDid(handleOrDid: string) {
    let identifier = handleOrDid
    if (!identifier.startsWith('did:')) {
      const res = await agent.resolveHandle({handle: identifier})
      identifier = res.data.did
    }
    return identifier
  }
}

export async function resolveGif(
  agent: BskyAgent,
  gif: Gif,
): Promise<ResolvedExternalLink> {
  const uri = `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`
  return {
    type: 'external',
    uri,
    title: gif.content_description,
    description: createGIFDescription(gif.content_description),
    thumb: await imageToThumb(gif.media_formats.preview.url),
  }
}

async function resolveExternal(
  agent: BskyAgent,
  uri: string,
): Promise<ResolvedExternalLink> {
  const result = await getLinkMeta(agent, uri)
  return {
    type: 'external',
    uri: result.url,
    title: result.title ?? '',
    description: result.description ?? '',
    thumb: result.image ? await imageToThumb(result.image) : undefined,
  }
}

async function imageToThumb(
  imageUri: string,
): Promise<ComposerImage | undefined> {
  try {
    const img = await downloadAndResize({
      uri: imageUri,
      width: POST_IMG_MAX.width,
      height: POST_IMG_MAX.height,
      mode: 'contain',
      maxSize: POST_IMG_MAX.size,
      timeout: 15e3,
    })
    if (img) {
      return await createComposerImage(img)
    }
  } catch {}
}