/
get-tweet.ts
66 lines (56 loc) · 1.67 KB
/
get-tweet.ts
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
import type { Tweet } from './types/index.js'
const SYNDICATION_URL = 'https://cdn.syndication.twimg.com'
export class TwitterApiError extends Error {
status: number
data: any
constructor({
message,
status,
data,
}: {
message: string
status: number
data: any
}) {
super(message)
this.name = 'TwitterApiError'
this.status = status
this.data = data
}
}
const TWEET_ID = /^[0-9]+$/
export async function getTweet(id: string): Promise<Tweet | undefined> {
if (id.length > 40 || !TWEET_ID.test(id)) {
throw new Error(`Invalid tweet id: ${id}`)
}
const url = new URL(`${SYNDICATION_URL}/tweet-result`)
url.searchParams.set('id', id)
url.searchParams.set('lang', 'en')
url.searchParams.set(
'features',
[
'tfw_timeline_list:',
'tfw_follower_count_sunset:true',
'tfw_tweet_edit_backend:on',
'tfw_refsrc_session:on',
'tfw_show_business_verified_badge:on',
'tfw_duplicate_scribes_to_settings:on',
'tfw_show_blue_verified_badge:on',
'tfw_legacy_timeline_sunset:true',
'tfw_show_gov_verified_badge:on',
'tfw_show_business_affiliate_badge:on',
'tfw_tweet_edit_frontend:on',
].join(';')
)
// The default `cache: 'force-cache'` can return 200 when there's an error
const res = await fetch(url.toString(), { cache: 'no-store' })
const isJson = res.headers.get('content-type')?.includes('application/json')
const data = isJson ? await res.json() : undefined
if (res.ok) return data
if (res.status === 404) return
throw new TwitterApiError({
message: typeof data.error === 'string' ? data.error : 'Bad request.',
status: res.status,
data,
})
}