
Can we agree to call Jev a "Decision Model"? I feel like they buried this in the explainations and docs. Language models output language. Decision models output decisions.


a post on X drawn from its facts, no widget script: media, quotes, link cards
npx shadcn add @ag/tweetnpx shadcn add @ag/tweet --dry-runYour installed source stays unchanged until you update it. How updates work
Image: held until loaded, then faded in, hidden on error so the card's own tone shows. Installing tweet now brings image.two side by side in X's 16:9 frame, the text at reading size
the quoted post drawn small inside, its X article as web-preview's x card

Some people on Reddit are speculating that this account is fake but OpenAI confirmed to me that Joe does work there in the capacity he claims
Took a minute to write a few words about security & safety as someone who lived through it all at OpenAI. I hope my thoughts help someone out there.
Its not just the f*cking sandboxthe richest mp4 under the embed's budget, its poster until played; a verified badge, a mention

A thread of early experiments with Claude Sonnet 5.5. A fall foliage simulator by @_re_pete, made with Sonnet 5 vs Sonnet 5.5.
Every post is a pure function of its facts. Fetching them is tweet-data's job, from the endpoint X's own embed reads, once, at authoring time: no widget script and no request when the page renders.
import { Sample } from "@/app/samples"
import type { Tweet as TweetFacts } from "@/registry/base-nova/lib/tweet-data"
import { Tweet } from "@/registry/base-nova/ui/tweet"
// #region posts
/** Posts as data, pasted from `mise tweet <url>`. Nothing here fetched. */
const PHOTOS: TweetFacts = {
id: "2101560333441610133",
url: "https://x.com/Mappletons/status/2101560333441610133",
author: {
name: "Maggie Appleton",
handle: "@Mappletons",
avatar:
"https://pbs.twimg.com/profile_images/1635665509512355840/-jFo36KR_200x200.jpg",
},
date: "2026-09-20T06:33:23.000Z",
body: [
{
text: 'Can we agree to call Jev a "Decision Model"?\nI feel like they buried this in the explainations and docs.\nLanguage models output language. Decision models output decisions.',
},
],
media: [
{
kind: "photo",
src: "https://pbs.twimg.com/media/HSo9u6-XUAA8PhA.jpg",
width: 1638,
height: 638,
},
{
kind: "photo",
src: "https://pbs.twimg.com/media/HSo9z9FW8AAj0zC.jpg",
width: 1632,
height: 428,
},
],
likes: 304,
replies: 48,
}
const QUOTE: TweetFacts = {
id: "2104646956551422036",
url: "https://x.com/rocketalignment/status/2104646956551422036",
author: {
name: "🚀 Rocket",
handle: "@rocketalignment",
avatar:
"https://pbs.twimg.com/profile_images/1980865852460158976/RoeuxAGX_200x200.jpg",
},
date: "2026-09-28T18:58:31.000Z",
body: [
{
text: "Some people on Reddit are speculating that this account is fake but OpenAI confirmed to me that Joe does work there in the capacity he claims",
},
],
media: [],
quote: {
id: "2104335929293127851",
url: "https://x.com/joedaroo/status/2104335929293127851",
author: {
name: "Joe",
handle: "@joedaroo",
avatar:
"https://pbs.twimg.com/profile_images/2097167182526611457/UJBigEfF_200x200.jpg",
verified: "blue",
},
date: "2026-09-27T22:22:36.000Z",
body: [
{
text: "Took a minute to write a few words about security & safety as someone who lived through it all at OpenAI. I hope my thoughts help someone out there.",
},
],
media: [],
card: {
url: "https://x.com/i/article/2104258872957636608",
site: "X",
title: "Its not just the f*cking sandbox",
description:
"A lot of the perspective on all the AI incidents has been shared from the outside in, and little has been said from the inside looking out, through the lens of a security person living through it.",
image: "https://pbs.twimg.com/media/HTQTvrnbYAAVg_-.jpg",
},
likes: 2849,
replies: 440,
},
likes: 57,
replies: 3,
}
const VIDEO: TweetFacts = {
id: "2104674987164782598",
url: "https://x.com/claudeai/status/2104674987164782598",
author: {
name: "Claude",
handle: "@claudeai",
avatar:
"https://pbs.twimg.com/profile_images/1950950107937185792/QOfEjFoJ_200x200.jpg",
verified: "blue",
},
date: "2026-09-28T20:49:54.000Z",
body: [
{
text: "A thread of early experiments with Claude Sonnet 5.5.\n\nA fall foliage simulator by ",
},
{ text: "@_re_pete", href: "https://x.com/_re_pete" },
{ text: ", made with Sonnet 5 vs Sonnet 5.5." },
],
media: [
{
kind: "video",
src: "https://video.twimg.com/amplify_video/2104672427187793920/vid/avc1/720x720/di5rQh2OGhgj0guK.mp4",
poster:
"https://pbs.twimg.com/amplify_video_thumb/2104672427187793920/img/in4Hl4l6NzD9hpWU.jpg",
width: 1080,
height: 1080,
},
],
likes: 7888,
replies: 255,
}
// #endregion
export default function Demo() {
return (
<div className="space-y-16">
<Sample
name="photos"
glance="large"
with="posts"
title="01 · a post with pictures"
description="two side by side in X's 16:9 frame, the text at reading size"
>
<Tweet tweet={PHOTOS} className="mx-auto max-w-md" />
</Sample>
<Sample
name="quote"
title="02 · a quote"
description="the quoted post drawn small inside, its X article as web-preview's x card"
>
<Tweet tweet={QUOTE} className="mx-auto max-w-xl" />
</Sample>
<Sample
name="video"
title="03 · a video"
description="the richest mp4 under the embed's budget, its poster until played; a verified badge, a mention"
>
<Tweet tweet={VIDEO} className="mx-auto max-w-xl" />
</Sample>
<p className="max-w-prose text-foreground/60 text-sm">
Every post is a pure function of its facts. Fetching them is{" "}
<code>tweet-data</code>'s job, from the endpoint X's own embed
reads, once, at authoring time: no widget script and no request when the
page renders.
</p>
</div>
)
}// A POST ON X, drawn from its facts (`tweet-data`): no widget script, no iframe, no
// request at render. The client's geometry on the page's own surfaces: the avatar and
// the names on one row with X's mark in the corner, the text at reading size with its
// links in X's blue, the media grid X lays out for one to four pictures, a quoted post
// as a smaller post inside, a link card through `web-preview`'s x style, the date and
// the counts at the foot.
//
// The whole post opens the post on X (`card-link`): click anywhere but a link, a
// control or a selection. Inside it the avatar and the names open the profile, the
// date and the mark open the post (the keyboard's way there), a quoted post opens
// itself, every link in the text is a link. Decorative chrome is aria-hidden. A server
// component; only the card's click handler runs in the browser.
import { Heart, MessageCircle } from "lucide-react"
import type * as React from "react"
import { cn } from "@/lib/utils"
import type {
TweetAuthor,
Tweet as TweetFacts,
TweetMedia,
TweetRun,
} from "@/registry/base-nova/lib/tweet-data"
import { CardLink } from "@/registry/base-nova/ui/card-link"
import { Image } from "@/registry/base-nova/ui/image"
import { Panel } from "@/registry/base-nova/ui/panel"
import { Video } from "@/registry/base-nova/ui/video"
import { WebPreview } from "@/registry/base-nova/ui/web-preview"
export interface TweetProps {
tweet: TweetFacts
className?: string
}
const LINK = "text-[#1d9bf0] hover:underline"
const MUTED = "text-foreground/55"
const ANCHOR = { target: "_blank", rel: "noopener noreferrer" } as const
const DATE = new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
})
const COUNT = new Intl.NumberFormat("en", { notation: "compact" })
const BADGE: Record<NonNullable<TweetAuthor["verified"]>, string> = {
blue: "#1d9bf0",
business: "#e2b719",
government: "#829aab",
}
function Verified({ kind }: { kind: NonNullable<TweetAuthor["verified"]> }) {
return (
<svg
data-slot="tweet-verified"
viewBox="0 0 22 22"
className="size-[1.1em] shrink-0"
role="img"
aria-label={`${kind === "blue" ? "" : `${kind} `}verified account`}
>
<path
fill={BADGE[kind]}
d="M20.4 11c0-1.3-.8-2.4-2-3 .4-1.3.2-2.6-.7-3.6-.9-.9-2.3-1.1-3.6-.7-.6-1.2-1.7-2-3-2s-2.4.8-3 2c-1.3-.4-2.7-.2-3.6.7-.9.9-1.1 2.3-.7 3.6-1.2.6-2 1.7-2 3s.8 2.4 2 3c-.4 1.3-.2 2.7.7 3.6.9.9 2.3 1.1 3.6.7.6 1.2 1.7 2 3 2s2.4-.8 3-2c1.3.4 2.7.2 3.6-.7.9-.9 1.1-2.3.7-3.6 1.2-.6 2-1.7 2-3z"
/>
<path
fill="#fff"
d="m9.6 14.9-3.3-3.3 1.3-1.3 2 2 4.6-4.9 1.3 1.2-5.9 6.3z"
/>
</svg>
)
}
function XMark() {
return (
<svg viewBox="0 0 24 24" className="size-5" aria-hidden="true">
<path
fill="currentColor"
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
)
}
function Body({ runs, className }: { runs: TweetRun[]; className?: string }) {
return (
<p
data-slot="tweet-body"
className={cn("whitespace-pre-wrap break-words", className)}
>
{runs.map((r, i) =>
r.href ? (
// biome-ignore lint/suspicious/noArrayIndexKey: runs are positional and never reorder
<a key={i} href={r.href} className={LINK} {...ANCHOR}>
{r.text}
</a>
) : (
r.text
),
)}
</p>
)
}
/** One to four pictures as X lays them out: one at its own aspect (never taller than
* 3:4), more in a 16:9 frame, two side by side, three as a tall one and a stack, four
* as a grid. */
const TALLEST = 3 / 4
const FRAME = 16 / 9
const GRID: Record<number, string> = {
2: "grid-cols-2",
3: "grid-cols-2 grid-rows-2 [&>*:first-child]:row-span-2",
4: "grid-cols-2 grid-rows-2",
}
function Media({ media, url }: { media: TweetMedia[]; url: string }) {
if (media.length > 4)
throw new Error(`tweet ${url}: ${media.length} pictures, X shows four`)
const one = media.length === 1 ? (media[0] as TweetMedia) : undefined
const aspect = one ? Math.max(one.width / one.height, TALLEST) : FRAME
return (
<div
data-slot="tweet-media"
className={cn(
"grid aspect-(--ag-tweet-aspect) gap-0.5 clip-rounded rounded-2xl border border-border",
GRID[media.length],
)}
style={{ "--ag-tweet-aspect": String(aspect) } as React.CSSProperties}
>
{media.map((m, i) =>
m.kind === "photo" ? (
<Image
// biome-ignore lint/suspicious/noArrayIndexKey: media are positional
key={i}
src={m.src}
alt={m.alt ?? ""}
width={m.width}
height={m.height}
loading="lazy"
className="size-full min-h-0 object-cover"
/>
) : (
// A video waits for the person and keeps the browser's own bar, as an
// embed should; a gif plays on sight and loops, as X plays one.
<Video
// biome-ignore lint/suspicious/noArrayIndexKey: media are positional
key={i}
src={m.src}
poster={m.poster}
optimizePoster={false}
play={m.kind === "gif" ? "visible" : "manual"}
loop={m.kind === "gif"}
preload="none"
label={m.kind === "gif" ? "gif" : "video"}
className="min-h-0 bg-black"
videoClassName="object-contain"
/>
),
)}
</div>
)
}
function Names({ author, small }: { author: TweetAuthor; small?: boolean }) {
const profile = `https://x.com/${author.handle.slice(1)}`
return (
<a
href={profile}
{...ANCHOR}
data-slot="tweet-author"
className={cn(
"flex min-w-0 gap-x-1 leading-5",
small ? "items-center" : "flex-col",
)}
>
<span className="flex min-w-0 items-center gap-1 font-bold hover:underline">
<span className="truncate">{author.name}</span>
{author.verified && <Verified kind={author.verified} />}
</span>
<span className={cn("truncate", MUTED)}>{author.handle}</span>
</a>
)
}
function Avatar({ author, size }: { author: TweetAuthor; size: string }) {
return (
<a
href={`https://x.com/${author.handle.slice(1)}`}
{...ANCHOR}
// The names beside it are the same link; the keyboard visits it once.
tabIndex={-1}
className="shrink-0"
>
<Image
src={author.avatar}
alt={author.name}
className={cn(
size,
"bg-foreground/8 object-cover",
author.square ? "rounded-[22%]" : "rounded-full",
)}
/>
</a>
)
}
function Quote({ tweet }: { tweet: TweetFacts }) {
return (
// The quoted post opens itself, not the post quoting it: the innermost card wins.
// A post inside the post: a panel inside a frame, so the inset one (Panel).
<Panel
render={<CardLink href={tweet.url} />}
data-slot="tweet-quote"
bodyClassName="gap-2 p-3"
className="relative text-[15px]"
>
<div className="flex items-center gap-1.5">
<Avatar author={tweet.author} size="size-5" />
<Names author={tweet.author} small />
<span className={MUTED} aria-hidden="true">
·
</span>
<a
href={tweet.url}
{...ANCHOR}
className={cn("shrink-0 hover:underline", MUTED)}
>
<time dateTime={tweet.date}>{DATE.format(new Date(tweet.date))}</time>
</a>
</div>
{tweet.body.length > 0 && <Body runs={tweet.body} />}
{tweet.media.length > 0 && <Media media={tweet.media} url={tweet.url} />}
{tweet.card && <WebPreview facts={tweet.card} style="x" />}
</Panel>
)
}
export function Tweet({ tweet, className }: TweetProps) {
return (
// The whole post opens the post on X, as X's own embed does; a keyboard gets
// there through the mark and the date.
<Panel
render={<CardLink href={tweet.url} />}
data-slot="tweet"
bodyClassName="gap-0 p-0"
className={cn("font-sans text-[15px]", className)}
>
<article className="space-y-3 p-4">
<header className="flex items-start gap-3">
<Avatar author={tweet.author} size="size-10" />
<div className="min-w-0 flex-1">
<Names author={tweet.author} />
</div>
<a
href={tweet.url}
{...ANCHOR}
aria-label="View on X"
className="text-foreground/80 hover:text-foreground"
>
<XMark />
</a>
</header>
{tweet.body.length > 0 && (
<Body runs={tweet.body} className="text-[17px] leading-6" />
)}
{tweet.media.length > 0 && (
<Media media={tweet.media} url={tweet.url} />
)}
{tweet.card && <WebPreview facts={tweet.card} style="x" />}
{tweet.quote && <Quote tweet={tweet.quote} />}
<a
href={tweet.url}
{...ANCHOR}
className={cn("block hover:underline", MUTED)}
>
<time dateTime={tweet.date}>{DATE.format(new Date(tweet.date))}</time>
{tweet.edited && " · edited"}
</a>
<footer
data-slot="tweet-counts"
className={cn(
"flex gap-6 border-border border-t pt-3 font-medium",
MUTED,
)}
>
<a
href={tweet.url}
{...ANCHOR}
className="flex items-center gap-1.5 hover:text-[#f91880]"
>
<Heart className="size-[1.15em]" aria-hidden="true" />
{COUNT.format(tweet.likes)}
<span className="sr-only"> likes</span>
</a>
<a
href={tweet.url}
{...ANCHOR}
className="flex items-center gap-1.5 hover:text-[#1d9bf0]"
>
<MessageCircle className="size-[1.15em]" aria-hidden="true" />
{COUNT.format(tweet.replies)}
<span className="sr-only"> replies</span>
</a>
</footer>
</article>
</Panel>
)
}// A POST ON X, as data: the facts `tweet` draws, and the one fetch that reads them.
//
// The source is the public syndication endpoint X's own embed widget reads (the one
// Vercel's react-tweet reads too): no key, no login, one GET per post. It runs at
// AUTHORING time, the web-preview decision: `mise tweet <url>` prints the facts, they
// ship beside the page, and a deploy never depends on X answering. Pictures on
// pbs.twimg.com and video on video.twimg.com are stable paths; keep them or mirror
// them, the facts do not care.
//
// Every shape the drawing does not know (a poll, a deleted post, a card it cannot read)
// is a THROW at fetch time, never a post that silently lost half of itself.
import type { Person } from "@/registry/base-nova/lib/person"
import type { Unfurl } from "@/registry/base-nova/lib/web-preview-facts"
export interface TweetAuthor extends Person {
handle: string
avatar: string
/** The badge beside the name: X's blue, a business's gold, a government's grey. */
verified?: "blue" | "business" | "government"
/** Businesses wear a rounded square instead of a circle. */
square?: boolean
}
/** A stretch of the text: plain, or a link with what X shows for it (a mention, a
* hashtag, a shortened url's display form). */
export interface TweetRun {
text: string
href?: string
}
export type TweetMedia =
| { kind: "photo"; src: string; width: number; height: number; alt?: string }
| {
kind: "video" | "gif"
src: string
poster: string
width: number
height: number
}
export interface Tweet {
id: string
url: string
author: TweetAuthor
/** ISO 8601. */
date: string
body: TweetRun[]
media: TweetMedia[]
/** The post this one quotes, drawn inside it. */
quote?: Tweet
/** The link card under the text (a page's unfurl, or an X article). */
card?: Unfurl
likes: number
replies: number
edited?: boolean
}
const ENTITIES: Record<string, string> = {
amp: "&",
lt: "<",
gt: ">",
quot: '"',
"#39": "'",
}
function decode(s: string): string {
return s.replace(/&(amp|lt|gt|quot|#39);/g, (_, k: string) => {
return ENTITIES[k] as string
})
}
/** The id inside any form of a post's address, or the id itself. */
export function tweetId(urlOrId: string): string {
const id = /^\d+$/.test(urlOrId)
? urlOrId
: /(?:twitter|x)\.com\/[^/]+\/status(?:es)?\/(\d+)/.exec(urlOrId)?.[1]
if (!id) throw new Error(`tweet: not a post address: ${urlOrId}`)
return id
}
// What the syndication endpoint answers with, the parts read here.
interface RawEntity {
indices: [number, number]
}
interface RawTweet {
__typename?: string
id_str: string
text: string
display_text_range: [number, number]
created_at: string
favorite_count?: number
conversation_count?: number
reply_count?: number
isEdited?: boolean
user: {
name: string
screen_name: string
profile_image_url_https: string
profile_image_shape?: string
is_blue_verified?: boolean
verified_type?: string | null
}
entities?: {
urls?: (RawEntity & {
url: string
expanded_url: string
display_url: string
})[]
user_mentions?: (RawEntity & { screen_name: string })[]
hashtags?: (RawEntity & { text: string })[]
symbols?: (RawEntity & { text: string })[]
media?: RawEntity[]
}
mediaDetails?: {
type: "photo" | "video" | "animated_gif"
media_url_https: string
ext_alt_text?: string | null
original_info: { width: number; height: number }
video_info?: {
variants: { content_type: string; bitrate?: number; url: string }[]
}
}[]
quoted_tweet?: RawTweet
card?: {
url: string
binding_values: Record<
string,
{ string_value?: string; image_value?: { url: string } } | undefined
>
} | null
article?: {
rest_id: string
title: string
preview_text?: string
cover_media?: { media_info?: { original_img_url?: string } }
}
poll?: unknown
}
/** The text between `display_text_range` as runs, entities as links. Indices count
* code points of the ESCAPED text (`&` is five), so the text is sliced first and
* each slice decoded after. */
function bodyOf(t: RawTweet): TweetRun[] {
const chars = Array.from(t.text)
const [from, to] = t.display_text_range
const e = t.entities ?? {}
const links: { at: [number, number]; run: TweetRun }[] = [
...(e.urls ?? []).map((u) => ({
at: u.indices,
run: { text: u.display_url, href: u.expanded_url },
})),
...(e.user_mentions ?? []).map((m) => ({
at: m.indices,
run: {
text: `@${m.screen_name}`,
href: `https://x.com/${m.screen_name}`,
},
})),
...(e.hashtags ?? []).map((h) => ({
at: h.indices,
run: {
text: `#${h.text}`,
href: `https://x.com/hashtag/${encodeURIComponent(h.text)}`,
},
})),
...(e.symbols ?? []).map((s) => ({
at: s.indices,
run: {
text: `$${s.text}`,
href: `https://x.com/search?q=%24${encodeURIComponent(s.text)}`,
},
})),
]
.filter((l) => l.at[0] >= from && l.at[1] <= to)
.sort((a, b) => a.at[0] - b.at[0])
const runs: TweetRun[] = []
let at = from
for (const l of links) {
if (l.at[0] < at) throw new Error(`tweet ${t.id_str}: entities overlap`)
if (l.at[0] > at)
runs.push({ text: decode(chars.slice(at, l.at[0]).join("")) })
runs.push(l.run)
at = l.at[1]
}
const tail = decode(chars.slice(at, to).join("")).trimEnd()
if (tail) runs.push({ text: tail })
return runs
}
/** The video to play inline: an embed in prose does not need the 1080p master, so the
* richest mp4 under this budget, else the leanest there is. */
const VIDEO_BUDGET_BPS = 2_000_000
function mediaOf(t: RawTweet): TweetMedia[] {
return (t.mediaDetails ?? []).map((m) => {
const { width, height } = m.original_info
if (m.type === "photo")
return {
kind: "photo",
src: m.media_url_https,
width,
height,
...(m.ext_alt_text ? { alt: m.ext_alt_text } : {}),
}
const mp4 = (m.video_info?.variants ?? [])
.filter((v) => v.content_type === "video/mp4")
.sort((a, b) => (a.bitrate ?? 0) - (b.bitrate ?? 0))
if (!mp4.length) throw new Error(`tweet ${t.id_str}: a video with no mp4`)
const pick =
mp4.filter((v) => (v.bitrate ?? 0) <= VIDEO_BUDGET_BPS).at(-1) ??
(mp4[0] as (typeof mp4)[number])
return {
kind: m.type === "animated_gif" ? "gif" : "video",
src: pick.url,
poster: m.media_url_https,
width,
height,
}
})
}
function cardOf(t: RawTweet): Unfurl | undefined {
if (t.article)
return {
url: `https://x.com/i/article/${t.article.rest_id}`,
site: "X",
title: t.article.title,
...(t.article.preview_text
? { description: t.article.preview_text }
: {}),
...(t.article.cover_media?.media_info?.original_img_url
? { image: t.article.cover_media.media_info.original_img_url }
: {}),
}
if (!t.card) return undefined
const v = t.card.binding_values
const title = v.title?.string_value
if (!title) throw new Error(`tweet ${t.id_str}: a card with no title`)
const expanded = t.entities?.urls?.find((u) => u.url === t.card?.url)
const image =
v.thumbnail_image_large?.image_value?.url ??
v.summary_photo_image_large?.image_value?.url ??
v.photo_image_full_size_large?.image_value?.url
const domain = v.domain?.string_value ?? v.vanity_url?.string_value
return {
url: expanded?.expanded_url ?? t.card.url,
site: domain ?? new URL(expanded?.expanded_url ?? t.card.url).hostname,
title,
...(v.description?.string_value
? { description: v.description.string_value }
: {}),
...(image ? { image } : {}),
}
}
function fromRaw(t: RawTweet): Tweet {
if (t.poll) throw new Error(`tweet ${t.id_str}: polls are not drawn yet`)
const u = t.user
const verified =
u.verified_type === "Business"
? "business"
: u.verified_type === "Government"
? "government"
: u.is_blue_verified
? "blue"
: undefined
const card = cardOf(t)
const quote = t.quoted_tweet ? fromRaw(t.quoted_tweet) : undefined
// X drops the link that ends a post when the card under the text IS that link.
const body = bodyOf(t)
if (card && body.at(-1)?.href === card.url) {
body.pop()
const last = body.at(-1)
if (last && !last.href) {
last.text = last.text.trimEnd()
if (!last.text) body.pop()
}
}
return {
id: t.id_str,
url: `https://x.com/${u.screen_name}/status/${t.id_str}`,
author: {
name: u.name,
handle: `@${u.screen_name}`,
// `_normal` is 48 px; `_200x200` is crisp at the sizes a post draws on a 2x screen.
avatar: u.profile_image_url_https.replace("_normal.", "_200x200."),
...(verified ? { verified } : {}),
...(u.profile_image_shape === "Square" ? { square: true } : {}),
},
date: new Date(t.created_at).toISOString(),
body,
media: mediaOf(t),
...(quote ? { quote } : {}),
...(card ? { card } : {}),
likes: t.favorite_count ?? 0,
replies: t.conversation_count ?? t.reply_count ?? 0,
...(t.isEdited ? { edited: true } : {}),
}
}
/** Fetch a post by its address or id. THROWS on a post that is gone or private (the
* endpoint answers a tombstone or nothing), and on any shape not drawn yet. */
export async function fetchTweet(urlOrId: string): Promise<Tweet> {
const id = tweetId(urlOrId)
// The endpoint wants a token derived from the id; this is the widget's own formula.
const token = ((Number(id) / 1e15) * Math.PI)
.toString(36)
.replace(/(0+|\.)/g, "")
const res = await fetch(
`https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&token=${token}`,
)
if (!res.ok) throw new Error(`tweet ${id}: HTTP ${res.status}`)
const raw = (await res.json()) as RawTweet
if (raw.__typename !== "Tweet")
throw new Error(
`tweet ${id}: ${raw.__typename ?? "no post"} (deleted or private)`,
)
return fromRaw(raw)
}