diff --git a/bskyogcard/package.json b/bskyogcard/package.json index 176f9d8c49..59d81118c7 100644 --- a/bskyogcard/package.json +++ b/bskyogcard/package.json @@ -4,13 +4,13 @@ "type": "module", "main": "src/index.ts", "scripts": { - "start": "node --loader ts-node/esm ./src/bin.ts", - "dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts", + "start": " NODE_ENV=production node --loader ts-node/esm ./src/bin.ts", + "dev": "LOG_ENABLED=true NODE_ENV=development node --watch-path ./src --loader ts-node/esm ./src/bin.ts | pino-pretty", "build": "tsc && cp -r src/assets dist/", "install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts" }, "dependencies": { - "@atproto/api": "0.12.19-next.0", + "@atproto/api": "^0.13.4", "@atproto/common": "^0.4.0", "@resvg/resvg-js": "^2.6.2", "express": "^4.19.2", @@ -18,10 +18,12 @@ "pino": "^9.2.0", "react": "^18.3.1", "satori": "^0.10.13", - "twemoji": "^14.0.2" + "twemoji": "^14.0.2", + "whatwg-mimetype": "^4.0.0" }, "devDependencies": { "@types/node": "^20.14.3", + "pino-pretty": "^11.2.2", "ts-node": "^10.9.2", "typescript": "^5.4.5" } diff --git a/bskyogcard/src/assets/fonts/100-Inter-Thin.ttf b/bskyogcard/src/assets/fonts/100-Inter-Thin.ttf new file mode 100644 index 0000000000..af78ac31a7 Binary files /dev/null and b/bskyogcard/src/assets/fonts/100-Inter-Thin.ttf differ diff --git a/bskyogcard/src/assets/fonts/200-Inter-ExtraLight.ttf b/bskyogcard/src/assets/fonts/200-Inter-ExtraLight.ttf new file mode 100644 index 0000000000..e98ac27c03 Binary files /dev/null and b/bskyogcard/src/assets/fonts/200-Inter-ExtraLight.ttf differ diff --git a/bskyogcard/src/assets/fonts/300-Inter-Light.ttf b/bskyogcard/src/assets/fonts/300-Inter-Light.ttf new file mode 100644 index 0000000000..31ac7883c4 Binary files /dev/null and b/bskyogcard/src/assets/fonts/300-Inter-Light.ttf differ diff --git a/bskyogcard/src/assets/fonts/400-Inter-Regular.ttf b/bskyogcard/src/assets/fonts/400-Inter-Regular.ttf new file mode 100644 index 0000000000..c544be4784 Binary files /dev/null and b/bskyogcard/src/assets/fonts/400-Inter-Regular.ttf differ diff --git a/bskyogcard/src/assets/fonts/500-Inter-Medium.ttf b/bskyogcard/src/assets/fonts/500-Inter-Medium.ttf new file mode 100644 index 0000000000..9342730853 Binary files /dev/null and b/bskyogcard/src/assets/fonts/500-Inter-Medium.ttf differ diff --git a/bskyogcard/src/assets/fonts/600-Inter-SemiBold.ttf b/bskyogcard/src/assets/fonts/600-Inter-SemiBold.ttf new file mode 100644 index 0000000000..a32e2b8d21 Binary files /dev/null and b/bskyogcard/src/assets/fonts/600-Inter-SemiBold.ttf differ diff --git a/bskyogcard/src/assets/fonts/700-Inter-Bold.ttf b/bskyogcard/src/assets/fonts/700-Inter-Bold.ttf new file mode 100644 index 0000000000..15e908f236 Binary files /dev/null and b/bskyogcard/src/assets/fonts/700-Inter-Bold.ttf differ diff --git a/bskyogcard/src/assets/fonts/800-Inter-ExtraBold.ttf b/bskyogcard/src/assets/fonts/800-Inter-ExtraBold.ttf new file mode 100644 index 0000000000..c1449e8b80 Binary files /dev/null and b/bskyogcard/src/assets/fonts/800-Inter-ExtraBold.ttf differ diff --git a/bskyogcard/src/assets/fonts/900-Inter-Black.ttf b/bskyogcard/src/assets/fonts/900-Inter-Black.ttf new file mode 100644 index 0000000000..034a73ee48 Binary files /dev/null and b/bskyogcard/src/assets/fonts/900-Inter-Black.ttf differ diff --git a/bskyogcard/src/assets/fonts/Inter-Bold.ttf b/bskyogcard/src/assets/fonts/Inter-Bold.ttf deleted file mode 100644 index fe23eeb9c9..0000000000 Binary files a/bskyogcard/src/assets/fonts/Inter-Bold.ttf and /dev/null differ diff --git a/bskyogcard/src/components/Box.tsx b/bskyogcard/src/components/Box.tsx new file mode 100644 index 0000000000..3c6b4480b5 --- /dev/null +++ b/bskyogcard/src/components/Box.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import {atoms as a, style as s} from '../theme/index.js' + +export function Box({ + children, + cx, +}: { + children?: React.ReactNode + cx?: Record[] +}) { + return
{children}
+} diff --git a/bskyogcard/src/components/Grid.tsx b/bskyogcard/src/components/Grid.tsx new file mode 100644 index 0000000000..38e3175ce3 --- /dev/null +++ b/bskyogcard/src/components/Grid.tsx @@ -0,0 +1,55 @@ +import React from 'react' + +import {atoms as a} from '../theme/index.js' +import {Box} from './Box.js' + +export function Row({ + children, + gutter = 0, + cx, +}: { + children: React.ReactNode + gutter?: number + cx?: Record[] +}) { + return ( + + {children} + + ) +} + +export function Column({ + children, + gutter = 0, + cx, + width = 1, +}: { + children: React.ReactNode + gutter?: number + cx?: Record[] + width?: number +}) { + return ( + + {children} + + ) +} diff --git a/bskyogcard/src/components/Image.tsx b/bskyogcard/src/components/Image.tsx new file mode 100644 index 0000000000..b82d77224e --- /dev/null +++ b/bskyogcard/src/components/Image.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import {style as s} from '../theme/index.js' +import {Image as ImageSource} from '../util/resolvePostData.js' + +export type ImageProps = Omit< + React.ImgHTMLAttributes, + 'src' | 'style' +> & { + image: ImageSource + cx?: Record[] +} + +export function Image({image, cx, ...rest}: ImageProps) { + const {image: src, mime = 'image/jpeg'} = image + return ( + + ) +} diff --git a/bskyogcard/src/components/LinkCard.tsx b/bskyogcard/src/components/LinkCard.tsx new file mode 100644 index 0000000000..07a08b72c5 --- /dev/null +++ b/bskyogcard/src/components/LinkCard.tsx @@ -0,0 +1,62 @@ +/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */ +import React from 'react' + +import {atoms as a, theme as t} from '../theme/index.js' +import {Image as ImageSource} from '../util/resolvePostData.js' +import {toShortUrl} from '../util/toShortUrl.js' +import {Box} from './Box.js' +import {Image} from './Image.js' +import {Text} from './Text.js' + +export function LinkCard({ + image, + uri, + title, + description, +}: { + image?: ImageSource + uri?: string + title: string + description: string +}) { + return ( + + {image && ( + + + + )} + + {uri && ( + + {toShortUrl(uri)} + + )} + {title} + {description} + + + ) +} diff --git a/bskyogcard/src/components/Post.tsx b/bskyogcard/src/components/Post.tsx new file mode 100644 index 0000000000..6a019054ea --- /dev/null +++ b/bskyogcard/src/components/Post.tsx @@ -0,0 +1,484 @@ +/* eslint-disable bsky-internal/avoid-unwrapped-text, react-native-a11y/has-valid-accessibility-ignores-invert-colors */ +import React from 'react' +import { + AppBskyEmbedExternal, + AppBskyEmbedImages, + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyGraphDefs, + AppBskyGraphStarterpack, +} from '@atproto/api' + +import {atoms as a, gradient, theme as t} from '../theme/index.js' +import {formatCount} from '../util/formatCount.js' +import {formatDate} from '../util/formatDate.js' +import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js' +import {Image as ImageSource, PostData} from '../util/resolvePostData.js' +import {Box} from './Box.js' +import * as Grid from './Grid.js' +import {CircleInfo} from './icons/CircleInfo.js' +import {Heart} from './icons/Heart.js' +import {Logomark} from './icons/Logomark.js' +import {Logotype} from './icons/Logotype.js' +import {Repost} from './icons/Repost.js' +import {Image} from './Image.js' +import {LinkCard} from './LinkCard.js' +import {RichText} from './RichText.js' +import {Text} from './Text.js' + +export function Post({ + post, + data, +}: { + post: AppBskyFeedDefs.PostView + data: PostData +}) { + if (AppBskyFeedPost.isRecord(post.record)) { + const avatar = data.images.get(post.author.avatar) + const text = post.record.text + const rt = data.texts.get(text) + const hasInteractions = post.likeCount > 0 || post.repostCount > 0 + + return ( + + + + + {avatar && } + + + + {post.author.displayName || post.author.handle} + + + @{post.author.handle} + + + + + {rt && } + + {post.embed && ( + + + + )} + + + + {formatDate(post.record.createdAt)} + + + {!hasInteractions && } + + + {hasInteractions && ( + + + + + + {post.likeCount > 0 && ( + + + + {formatCount(post.likeCount)} + + + )} + {post.repostCount > 0 && ( + + + + {formatCount(post.repostCount)} + + + )} + + + + + + )} + + + ) + } + + return null +} + +export function Logo() { + return ( + + + + + + + ) +} + +export function Embeds({ + embed, + data, + hideNestedEmbeds, +}: { + embed: AppBskyFeedDefs.PostView['embed'] + data: PostData + hideNestedEmbeds?: boolean +}) { + if (AppBskyEmbedExternal.isView(embed)) { + const {title, description, uri, thumb} = embed.external + const image = data.images.get(thumb) + return ( + + ) + } + + if (AppBskyEmbedImages.isView(embed)) { + const {images} = embed + + if (images.length > 0) { + const imgs = images.map(({fullsize}) => data.images.get(fullsize)) + if (imgs.length === 1) { + return ( + + + + ) + } else if (imgs.length === 2) { + return ( + + + + + + + + + ) + } else if (imgs.length === 3) { + return ( + + + + + + + + + + ) + } else { + return ( + + + + + + + + + + + ) + } + } + } + + if (AppBskyEmbedRecord.isView(embed)) { + if (AppBskyFeedDefs.isGeneratorView(embed.record)) { + return + } + if (AppBskyGraphDefs.isListView(embed.record)) { + return + } + if ( + AppBskyGraphDefs.isStarterPackViewBasic(embed.record) && + AppBskyGraphStarterpack.isRecord(embed.record.record) + ) { + const uri = getStarterPackImageUri(embed.record) + const {name, description} = embed.record.record + const image = data.images.get(uri) + return + } + return + } + + if (AppBskyEmbedRecordWithMedia.isView(embed)) { + return ( + + + {!hideNestedEmbeds && } + + ) + } + + return null +} + +export function FeedCard({ + embed, + data, +}: { + embed: AppBskyFeedDefs.GeneratorView + data: PostData +}) { + const {avatar, displayName, likeCount, creator} = embed + const image = data.images.get(avatar) + return ( + + + {image && ( + + )} + + {displayName} + By @{creator.handle} + + + + {likeCount > 1 && ( + + Liked by {formatCount(likeCount)} users + + )} + + ) +} + +export function ListCard({ + embed, + data, +}: { + embed: AppBskyGraphDefs.ListView + data: PostData +}) { + const {avatar, name, creator} = embed + const image = data.images.get(avatar) + return ( + + + {image && ( + + )} + + {name} + By @{creator.handle} + + + + ) +} + +export function SquareImage({image}: {image: ImageSource}) { + return ( + + + + ) +} + +export function QuoteEmbed({ + embed, + data, +}: { + embed: AppBskyEmbedRecord.View + data: PostData +}) { + if ( + AppBskyEmbedRecord.isViewRecord(embed.record) && + AppBskyFeedPost.isRecord(embed.record.value) && + AppBskyFeedPost.validateRecord(embed.record.value).success + ) { + const {author, value: post, embeds} = embed.record + const avatar = data.images.get(author.avatar) + const rt = data.texts.get(post.text) + const notPublic = author.labels.some(l => l.val === `!no-unauthenticated`) + + if (notPublic) { + return ( + + The author of the quoted post has requested their posts not be + displayed on external sites + + ) + } + + return ( + + + + {avatar && } + + + + {author.displayName || author.handle} + + + @{author.handle} + + + + + {rt && ( + + + + )} + + {Boolean(embeds && embeds.length) && ( + + + + )} + + ) + } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { + return Quoted post is blocked + } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { + return ( + + Quoted post not found, it may have been deleted. + + ) + } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) { + return Quoted post detached by author + } + return null +} + +export function NotQuoteEmbed({children}: {children: React.ReactNode}) { + return ( + + + + {children} + + + ) +} diff --git a/bskyogcard/src/components/RichText.tsx b/bskyogcard/src/components/RichText.tsx new file mode 100644 index 0000000000..0c30894894 --- /dev/null +++ b/bskyogcard/src/components/RichText.tsx @@ -0,0 +1,91 @@ +import React from 'react' +import {AppBskyRichtextFacet, RichText as RichTextApi} from '@atproto/api' + +import {atoms as a, style as s, theme as t} from '../theme/index.js' +import {toShortUrl} from '../util/toShortUrl.js' +import {Text} from './Text.js' + +export function RichText({ + value, + disableLinks, + cx, +}: { + value: RichTextApi + disableLinks?: boolean + cx?: Record[] +}) { + const {facets} = value + const baseStyles = [ + a.leading_snug, + { + whiteSpace: 'pre-wrap', + }, + cx ? s(cx) : {}, + ] + const linkStyles = [ + { + color: t.palette.primary_500, + }, + ] + + if (!facets?.length) { + // if (isOnlyEmoji(text)) { + // const fontSize = + // (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier + // return ( + // + // {text} + // + // ) + // } + } + + const els = [] + let key = 0 + // N.B. must access segments via `richText.segments`, not via destructuring + for (const segment of value.segments()) { + const {text, link, mention, tag} = segment + if ( + mention && + AppBskyRichtextFacet.validateMention(mention).success && + !disableLinks + ) { + els.push( + + {text} + , + ) + } else if (link && AppBskyRichtextFacet.validateLink(link).success) { + const url = toShortUrl(text) + if (disableLinks) { + els.push(url) + } else { + els.push( + + {url} + , + ) + } + } else if ( + !disableLinks && + tag && + AppBskyRichtextFacet.validateTag(tag).success + ) { + els.push( + + {segment.text} + , + ) + } else { + els.push(segment.text) + } + key++ + } + + return {els} +} diff --git a/bskyogcard/src/components/StarterPack.tsx b/bskyogcard/src/components/StarterPack.tsx index 29bb8f32ab..92a86070ba 100644 --- a/bskyogcard/src/components/StarterPack.tsx +++ b/bskyogcard/src/components/StarterPack.tsx @@ -106,6 +106,7 @@ export function StarterPack(props: { color: 'white', padding: 60, fontSize: 40, + fontWeight: '700', }}> JOIN THE CONVERSATION @@ -134,6 +135,7 @@ export function StarterPack(props: { fontSize: isLongTitle ? 55 : 65, display: 'flex', textAlign: 'center', + fontWeight: '700', }}> {record?.name || 'Starter Pack'} diff --git a/bskyogcard/src/components/Text.tsx b/bskyogcard/src/components/Text.tsx new file mode 100644 index 0000000000..704f538985 --- /dev/null +++ b/bskyogcard/src/components/Text.tsx @@ -0,0 +1,26 @@ +import React from 'react' + +import {atoms as a, style as s, theme as t} from '../theme/index.js' + +export function Text({ + children, + cx, +}: { + children?: React.ReactNode + cx?: Record[] +}) { + return ( +
+ {children} +
+ ) +} diff --git a/bskyogcard/src/components/icons/CircleInfo.tsx b/bskyogcard/src/components/icons/CircleInfo.tsx new file mode 100644 index 0000000000..9469e83be3 --- /dev/null +++ b/bskyogcard/src/components/icons/CircleInfo.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export function CircleInfo({size, fill}: {size: number; fill?: string}) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/icons/Heart.tsx b/bskyogcard/src/components/icons/Heart.tsx new file mode 100644 index 0000000000..b5a59396bf --- /dev/null +++ b/bskyogcard/src/components/icons/Heart.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export function Heart({size, fill}: {size: number; fill?: string}) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/icons/Logomark.tsx b/bskyogcard/src/components/icons/Logomark.tsx new file mode 100644 index 0000000000..f203225ba8 --- /dev/null +++ b/bskyogcard/src/components/icons/Logomark.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +export function Logomark({size, fill}: {size: number; fill?: string}) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/icons/Logotype.tsx b/bskyogcard/src/components/icons/Logotype.tsx new file mode 100644 index 0000000000..cc4c2cfef6 --- /dev/null +++ b/bskyogcard/src/components/icons/Logotype.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +export function Logotype({size, fill}: {size: number; fill?: string}) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/icons/Repost.tsx b/bskyogcard/src/components/icons/Repost.tsx new file mode 100644 index 0000000000..ef7d8d6af3 --- /dev/null +++ b/bskyogcard/src/components/icons/Repost.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export function Repost({size, fill}: {size: number; fill?: string}) { + return ( + + + + ) +} diff --git a/bskyogcard/src/context.ts b/bskyogcard/src/context.ts index 0c972c94de..5d826f5d8b 100644 --- a/bskyogcard/src/context.ts +++ b/bskyogcard/src/context.ts @@ -31,9 +31,12 @@ export class AppContext { const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts') const fontFiles = readdirSync(fontDirectory) const fonts = fontFiles.map(file => { + const basename = path.basename(file, path.extname(file)) + const [weight, name] = basename.split('-') return { - name: path.basename(file, path.extname(file)), + name, data: readFileSync(path.join(fontDirectory, file)), + weight: parseInt(weight), } }) return new AppContext({ diff --git a/bskyogcard/src/routes/index.ts b/bskyogcard/src/routes/index.ts index 0c40f89d3b..666b14140a 100644 --- a/bskyogcard/src/routes/index.ts +++ b/bskyogcard/src/routes/index.ts @@ -2,6 +2,7 @@ import {Express} from 'express' import {AppContext} from '../context.js' import {default as health} from './health.js' +import {default as post} from './post.js' import {default as starterPack} from './starter-pack.js' export * from './util.js' @@ -9,5 +10,6 @@ export * from './util.js' export default function (ctx: AppContext, app: Express) { app = health(ctx, app) // GET /_health app = starterPack(ctx, app) // GET /start/:actor/:rkey + app = post(ctx, app) // POST /post return app } diff --git a/bskyogcard/src/routes/post.tsx b/bskyogcard/src/routes/post.tsx new file mode 100644 index 0000000000..a7d2b6eea2 --- /dev/null +++ b/bskyogcard/src/routes/post.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api' +import resvg from '@resvg/resvg-js' +import {Express} from 'express' +import satori from 'satori' + +import {Post} from '../components/Post.js' +import {AppContext} from '../context.js' +import {httpLogger} from '../logger.js' +import {loadEmojiAsSvg} from '../util.js' +import {resolvePostData} from '../util/resolvePostData.js' +import {handler, originVerifyMiddleware} from './util.js' + +const WIDTH = 600 + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/:actor/post/:rkey', + originVerifyMiddleware(ctx), + handler(async (req, res) => { + let {actor, rkey} = req.params + let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey) + + let post: AppBskyFeedDefs.PostView + + try { + if (!actor.startsWith('did:')) { + const res = await ctx.appviewAgent.resolveHandle({ + handle: actor, + }) + actor = res.data.did + } + uri = AtUri.make(actor, 'app.bsky.feed.post', rkey) + const {data} = await ctx.appviewAgent.getPosts({ + uris: [uri.toString()], + }) + post = data.posts.at(0) + + const notPublic = post.author.labels.some( + l => l.val === `!no-unauthenticated`, + ) + if (notPublic) { + return res.status(404).end('not found') + } + } catch (err) { + httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post') + return res.status(404).end('not found') + } + + if (!AppBskyFeedPost.isRecord(post.record)) { + return res.status(404).end('not found') + } + + const data = await resolvePostData(post, ctx.appviewAgent) + const svg = await satori(, { + fonts: ctx.fonts, + width: WIDTH, + loadAdditionalAsset: async (code, text) => { + if (code === 'emoji') { + return await loadEmojiAsSvg(text) + } + }, + }) + const output = await resvg.renderAsync(svg, { + fitTo: { + mode: 'width', + value: WIDTH * 2, + }, + logLevel: 'trace', + }) + res.statusCode = 200 + res.setHeader('content-type', 'image/png') + res.setHeader('cdn-tag', [...data.images.keys()].join(',')) + return res.end(output.asPng()) + }), + ) +} diff --git a/bskyogcard/src/theme/atoms.ts b/bskyogcard/src/theme/atoms.ts new file mode 100644 index 0000000000..028e0d6fcd --- /dev/null +++ b/bskyogcard/src/theme/atoms.ts @@ -0,0 +1,566 @@ +import * as tokens from './tokens.js' + +export const atoms = { + block: { + display: 'block', + }, + inline_block: { + display: 'inline-block', + }, + inline: { + display: 'inline', + }, + /* + * Positioning + */ + fixed: { + position: 'absolute', + }, + absolute: { + position: 'absolute', + }, + relative: { + position: 'relative', + }, + inset_0: { + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + + overflow_hidden: { + overflow: 'hidden', + }, + + /* + * Width + */ + w_full: { + width: '100%', + }, + h_full: { + height: '100%', + }, + h_full_vh: { + height: '100vh', + }, + + /* + * Theme-independent bg colors + */ + bg_transparent: { + backgroundColor: 'transparent', + }, + + /* + * Border radius + */ + rounded_2xs: { + borderRadius: tokens.borderRadius._2xs, + }, + rounded_xs: { + borderRadius: tokens.borderRadius.xs, + }, + rounded_sm: { + borderRadius: tokens.borderRadius.sm, + }, + rounded_md: { + borderRadius: tokens.borderRadius.md, + }, + rounded_full: { + borderRadius: tokens.borderRadius.full, + }, + + /* + * Flex + */ + gap_2xs: { + gap: tokens.space._2xs, + }, + gap_xs: { + gap: tokens.space.xs, + }, + gap_sm: { + gap: tokens.space.sm, + }, + gap_md: { + gap: tokens.space.md, + }, + gap_lg: { + gap: tokens.space.lg, + }, + gap_xl: { + gap: tokens.space.xl, + }, + gap_2xl: { + gap: tokens.space._2xl, + }, + gap_3xl: { + gap: tokens.space._3xl, + }, + gap_4xl: { + gap: tokens.space._4xl, + }, + gap_5xl: { + gap: tokens.space._5xl, + }, + flex: { + display: 'flex', + }, + flex_col: { + flexDirection: 'column', + }, + flex_row: { + flexDirection: 'row', + }, + flex_col_reverse: { + flexDirection: 'column-reverse', + }, + flex_row_reverse: { + flexDirection: 'row-reverse', + }, + flex_wrap: { + flexWrap: 'wrap', + }, + flex_0: { + flex: '0 0 auto', + }, + flex_1: { + flex: 1, + }, + flex_grow: { + flexGrow: 1, + }, + flex_shrink: { + flexShrink: 1, + }, + flex_shrink_0: { + flexShrink: 0, + }, + justify_start: { + justifyContent: 'flex-start', + }, + justify_center: { + justifyContent: 'center', + }, + justify_between: { + justifyContent: 'space-between', + }, + justify_end: { + justifyContent: 'flex-end', + }, + align_center: { + alignItems: 'center', + }, + align_start: { + alignItems: 'flex-start', + }, + align_end: { + alignItems: 'flex-end', + }, + align_baseline: { + alignItems: 'baseline', + }, + align_stretch: { + alignItems: 'stretch', + }, + self_auto: { + alignSelf: 'auto', + }, + self_start: { + alignSelf: 'flex-start', + }, + self_end: { + alignSelf: 'flex-end', + }, + self_center: { + alignSelf: 'center', + }, + self_stretch: { + alignSelf: 'stretch', + }, + self_baseline: { + alignSelf: 'baseline', + }, + + /* + * Text + */ + text_left: { + textAlign: 'left', + }, + text_center: { + textAlign: 'center', + }, + text_right: { + textAlign: 'right', + }, + text_2xs: { + fontSize: tokens.fontSize._2xs, + letterSpacing: 0.25, + }, + text_xs: { + fontSize: tokens.fontSize.xs, + letterSpacing: 0.25, + }, + text_sm: { + fontSize: tokens.fontSize.sm, + letterSpacing: 0.25, + }, + text_md: { + fontSize: tokens.fontSize.md, + letterSpacing: 0.25, + }, + text_lg: { + fontSize: tokens.fontSize.lg, + letterSpacing: 0.25, + }, + text_xl: { + fontSize: tokens.fontSize.xl, + letterSpacing: 0.25, + }, + text_2xl: { + fontSize: tokens.fontSize._2xl, + letterSpacing: 0.25, + }, + text_3xl: { + fontSize: tokens.fontSize._3xl, + letterSpacing: 0.25, + }, + text_4xl: { + fontSize: tokens.fontSize._4xl, + letterSpacing: 0.25, + }, + text_5xl: { + fontSize: tokens.fontSize._5xl, + letterSpacing: 0.25, + }, + leading_tight: { + lineHeight: '1.15', + }, + leading_snug: { + lineHeight: '1.3', + }, + leading_normal: { + lineHeight: '1.5', + }, + tracking_normal: { + letterSpacing: 0, + }, + tracking_wide: { + letterSpacing: 0.25, + }, + font_normal: { + fontWeight: tokens.fontWeight.normal, + }, + font_semibold: { + fontWeight: tokens.fontWeight.semibold, + }, + font_bold: { + fontWeight: tokens.fontWeight.bold, + }, + font_heavy: { + fontWeight: tokens.fontWeight.heavy, + }, + italic: { + fontStyle: 'italic', + }, + mono: { + fontFamily: 'monospace', + }, + + /* + * Border + */ + border_0: { + borderWidth: '0px', + }, + border: { + borderWidth: '1px', + borderStyle: 'solid', + }, + border_t: { + borderTopWidth: '1px', + borderStyle: 'solid', + }, + border_b: { + borderBottomWidth: '1px', + borderStyle: 'solid', + }, + border_l: { + borderLeftWidth: '1px', + borderStyle: 'solid', + }, + border_r: { + borderRightWidth: '1px', + borderStyle: 'solid', + }, + + /* + * Padding + */ + p_0: { + padding: 0, + }, + p_2xs: { + padding: tokens.space._2xs, + }, + p_xs: { + padding: tokens.space.xs, + }, + p_sm: { + padding: tokens.space.sm, + }, + p_md: { + padding: tokens.space.md, + }, + p_lg: { + padding: tokens.space.lg, + }, + p_xl: { + padding: tokens.space.xl, + }, + p_2xl: { + padding: tokens.space._2xl, + }, + p_3xl: { + padding: tokens.space._3xl, + }, + p_4xl: { + padding: tokens.space._4xl, + }, + p_5xl: { + padding: tokens.space._5xl, + }, + px_0: { + paddingLeft: 0, + paddingRight: 0, + }, + px_2xs: { + paddingLeft: tokens.space._2xs, + paddingRight: tokens.space._2xs, + }, + px_xs: { + paddingLeft: tokens.space.xs, + paddingRight: tokens.space.xs, + }, + px_sm: { + paddingLeft: tokens.space.sm, + paddingRight: tokens.space.sm, + }, + px_md: { + paddingLeft: tokens.space.md, + paddingRight: tokens.space.md, + }, + px_lg: { + paddingLeft: tokens.space.lg, + paddingRight: tokens.space.lg, + }, + px_xl: { + paddingLeft: tokens.space.xl, + paddingRight: tokens.space.xl, + }, + px_2xl: { + paddingLeft: tokens.space._2xl, + paddingRight: tokens.space._2xl, + }, + px_3xl: { + paddingLeft: tokens.space._3xl, + paddingRight: tokens.space._3xl, + }, + px_4xl: { + paddingLeft: tokens.space._4xl, + paddingRight: tokens.space._4xl, + }, + px_5xl: { + paddingLeft: tokens.space._5xl, + paddingRight: tokens.space._5xl, + }, + py_0: { + paddingTop: 0, + paddingBottom: 0, + }, + py_2xs: { + paddingTop: tokens.space._2xs, + paddingBottom: tokens.space._2xs, + }, + py_xs: { + paddingTop: tokens.space.xs, + paddingBottom: tokens.space.xs, + }, + py_sm: { + paddingTop: tokens.space.sm, + paddingBottom: tokens.space.sm, + }, + py_md: { + paddingTop: tokens.space.md, + paddingBottom: tokens.space.md, + }, + py_lg: { + paddingTop: tokens.space.lg, + paddingBottom: tokens.space.lg, + }, + py_xl: { + paddingTop: tokens.space.xl, + paddingBottom: tokens.space.xl, + }, + py_2xl: { + paddingTop: tokens.space._2xl, + paddingBottom: tokens.space._2xl, + }, + py_3xl: { + paddingTop: tokens.space._3xl, + paddingBottom: tokens.space._3xl, + }, + py_4xl: { + paddingTop: tokens.space._4xl, + paddingBottom: tokens.space._4xl, + }, + py_5xl: { + paddingTop: tokens.space._5xl, + paddingBottom: tokens.space._5xl, + }, + pt_0: { + paddingTop: 0, + }, + pt_2xs: { + paddingTop: tokens.space._2xs, + }, + pt_xs: { + paddingTop: tokens.space.xs, + }, + pt_sm: { + paddingTop: tokens.space.sm, + }, + pt_md: { + paddingTop: tokens.space.md, + }, + pt_lg: { + paddingTop: tokens.space.lg, + }, + pt_xl: { + paddingTop: tokens.space.xl, + }, + pt_2xl: { + paddingTop: tokens.space._2xl, + }, + pt_3xl: { + paddingTop: tokens.space._3xl, + }, + pt_4xl: { + paddingTop: tokens.space._4xl, + }, + pt_5xl: { + paddingTop: tokens.space._5xl, + }, + pb_0: { + paddingBottom: 0, + }, + pb_2xs: { + paddingBottom: tokens.space._2xs, + }, + pb_xs: { + paddingBottom: tokens.space.xs, + }, + pb_sm: { + paddingBottom: tokens.space.sm, + }, + pb_md: { + paddingBottom: tokens.space.md, + }, + pb_lg: { + paddingBottom: tokens.space.lg, + }, + pb_xl: { + paddingBottom: tokens.space.xl, + }, + pb_2xl: { + paddingBottom: tokens.space._2xl, + }, + pb_3xl: { + paddingBottom: tokens.space._3xl, + }, + pb_4xl: { + paddingBottom: tokens.space._4xl, + }, + pb_5xl: { + paddingBottom: tokens.space._5xl, + }, + pl_0: { + paddingLeft: 0, + }, + pl_2xs: { + paddingLeft: tokens.space._2xs, + }, + pl_xs: { + paddingLeft: tokens.space.xs, + }, + pl_sm: { + paddingLeft: tokens.space.sm, + }, + pl_md: { + paddingLeft: tokens.space.md, + }, + pl_lg: { + paddingLeft: tokens.space.lg, + }, + pl_xl: { + paddingLeft: tokens.space.xl, + }, + pl_2xl: { + paddingLeft: tokens.space._2xl, + }, + pl_3xl: { + paddingLeft: tokens.space._3xl, + }, + pl_4xl: { + paddingLeft: tokens.space._4xl, + }, + pl_5xl: { + paddingLeft: tokens.space._5xl, + }, + pr_0: { + paddingRight: 0, + }, + pr_2xs: { + paddingRight: tokens.space._2xs, + }, + pr_xs: { + paddingRight: tokens.space.xs, + }, + pr_sm: { + paddingRight: tokens.space.sm, + }, + pr_md: { + paddingRight: tokens.space.md, + }, + pr_lg: { + paddingRight: tokens.space.lg, + }, + pr_xl: { + paddingRight: tokens.space.xl, + }, + pr_2xl: { + paddingRight: tokens.space._2xl, + }, + pr_3xl: { + paddingRight: tokens.space._3xl, + }, + pr_4xl: { + paddingRight: tokens.space._4xl, + }, + pr_5xl: { + paddingRight: tokens.space._5xl, + }, + + /* + * Text decoration + */ + underline: { + textDecorationLine: 'underline', + }, + strike_through: { + textDecorationLine: 'line-through', + }, +} as const diff --git a/bskyogcard/src/theme/colors.ts b/bskyogcard/src/theme/colors.ts new file mode 100644 index 0000000000..47d6475488 --- /dev/null +++ b/bskyogcard/src/theme/colors.ts @@ -0,0 +1,84 @@ +export const BLUE_HUE = 211 +export const RED_HUE = 346 +export const GREEN_HUE = 152 + +/** + * Smooth progression of lightness "stops" for generating HSL colors. + */ +export const COLOR_STOPS = [ + 0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1, +] + +export function generateScale(start: number, end: number) { + const range = end - start + return COLOR_STOPS.map(stop => { + return start + range * stop + }) +} + +export const defaultScale = generateScale(6, 100) +// dim shifted 6% lighter +export const dimScale = generateScale(12, 100) + +export const colors = { + white: '#FFFFFF', + black: '#000000', + + contrast_0: `hsl(${BLUE_HUE}, 20%, ${defaultScale[14]}%)`, + contrast_25: `hsl(${BLUE_HUE}, 20%, ${defaultScale[13]}%)`, + contrast_50: `hsl(${BLUE_HUE}, 20%, ${defaultScale[12]}%)`, + contrast_100: `hsl(${BLUE_HUE}, 20%, ${defaultScale[11]}%)`, + contrast_200: `hsl(${BLUE_HUE}, 20%, ${defaultScale[10]}%)`, + contrast_300: `hsl(${BLUE_HUE}, 20%, ${defaultScale[9]}%)`, + contrast_400: `hsl(${BLUE_HUE}, 20%, ${defaultScale[8]}%)`, + contrast_500: `hsl(${BLUE_HUE}, 20%, ${defaultScale[7]}%)`, + contrast_600: `hsl(${BLUE_HUE}, 24%, ${defaultScale[6]}%)`, + contrast_700: `hsl(${BLUE_HUE}, 24%, ${defaultScale[5]}%)`, + contrast_800: `hsl(${BLUE_HUE}, 28%, ${defaultScale[4]}%)`, + contrast_900: `hsl(${BLUE_HUE}, 28%, ${defaultScale[3]}%)`, + contrast_950: `hsl(${BLUE_HUE}, 28%, ${defaultScale[2]}%)`, + contrast_975: `hsl(${BLUE_HUE}, 28%, ${defaultScale[1]}%)`, + contrast_1000: `hsl(${BLUE_HUE}, 28%, ${defaultScale[0]}%)`, + + primary_25: `hsl(${BLUE_HUE}, 99%, 97%)`, + primary_50: `hsl(${BLUE_HUE}, 99%, 95%)`, + primary_100: `hsl(${BLUE_HUE}, 99%, 90%)`, + primary_200: `hsl(${BLUE_HUE}, 99%, 80%)`, + primary_300: `hsl(${BLUE_HUE}, 99%, 70%)`, + primary_400: `hsl(${BLUE_HUE}, 99%, 60%)`, + primary_500: `hsl(${BLUE_HUE}, 99%, 53%)`, + primary_600: `hsl(${BLUE_HUE}, 99%, 42%)`, + primary_700: `hsl(${BLUE_HUE}, 99%, 34%)`, + primary_800: `hsl(${BLUE_HUE}, 99%, 26%)`, + primary_900: `hsl(${BLUE_HUE}, 99%, 18%)`, + primary_950: `hsl(${BLUE_HUE}, 99%, 10%)`, + primary_975: `hsl(${BLUE_HUE}, 99%, 7%)`, + + green_25: `hsl(${GREEN_HUE}, 82%, 97%)`, + green_50: `hsl(${GREEN_HUE}, 82%, 95%)`, + green_100: `hsl(${GREEN_HUE}, 82%, 90%)`, + green_200: `hsl(${GREEN_HUE}, 82%, 80%)`, + green_300: `hsl(${GREEN_HUE}, 82%, 70%)`, + green_400: `hsl(${GREEN_HUE}, 82%, 60%)`, + green_500: `hsl(${GREEN_HUE}, 82%, 50%)`, + green_600: `hsl(${GREEN_HUE}, 82%, 42%)`, + green_700: `hsl(${GREEN_HUE}, 82%, 34%)`, + green_800: `hsl(${GREEN_HUE}, 82%, 26%)`, + green_900: `hsl(${GREEN_HUE}, 82%, 18%)`, + green_950: `hsl(${GREEN_HUE}, 82%, 10%)`, + green_975: `hsl(${GREEN_HUE}, 82%, 7%)`, + + red_25: `hsl(${RED_HUE}, 91%, 97%)`, + red_50: `hsl(${RED_HUE}, 91%, 95%)`, + red_100: `hsl(${RED_HUE}, 91%, 90%)`, + red_200: `hsl(${RED_HUE}, 91%, 80%)`, + red_300: `hsl(${RED_HUE}, 91%, 70%)`, + red_400: `hsl(${RED_HUE}, 91%, 60%)`, + red_500: `hsl(${RED_HUE}, 91%, 50%)`, + red_600: `hsl(${RED_HUE}, 91%, 42%)`, + red_700: `hsl(${RED_HUE}, 91%, 34%)`, + red_800: `hsl(${RED_HUE}, 91%, 26%)`, + red_900: `hsl(${RED_HUE}, 91%, 18%)`, + red_950: `hsl(${RED_HUE}, 91%, 10%)`, + red_975: `hsl(${RED_HUE}, 91%, 7%)`, +} as const diff --git a/bskyogcard/src/theme/index.ts b/bskyogcard/src/theme/index.ts new file mode 100644 index 0000000000..5f0b569a23 --- /dev/null +++ b/bskyogcard/src/theme/index.ts @@ -0,0 +1,100 @@ +import {colors} from './colors.js' +import * as tokens from './tokens.js' + +export {atoms} from './atoms.js' +export * as tokens from './tokens.js' + +const fontFamily = 'Inter' + +export const theme = { + palette: colors, + atoms: { + text: { + color: colors.black, + fontFamily, + }, + text_contrast_low: { + color: colors.contrast_500, + fontFamily, + }, + text_contrast_medium: { + color: colors.contrast_700, + fontFamily, + }, + text_contrast_high: { + color: colors.contrast_900, + fontFamily, + }, + text_inverted: { + color: colors.white, + fontFamily, + }, + bg: { + backgroundColor: colors.white, + }, + bg_contrast_25: { + backgroundColor: colors.contrast_25, + }, + bg_contrast_50: { + backgroundColor: colors.contrast_50, + }, + bg_contrast_100: { + backgroundColor: colors.contrast_100, + }, + bg_contrast_200: { + backgroundColor: colors.contrast_200, + }, + bg_contrast_300: { + backgroundColor: colors.contrast_300, + }, + bg_contrast_400: { + backgroundColor: colors.contrast_400, + }, + bg_contrast_500: { + backgroundColor: colors.contrast_500, + }, + bg_contrast_600: { + backgroundColor: colors.contrast_600, + }, + bg_contrast_700: { + backgroundColor: colors.contrast_700, + }, + bg_contrast_800: { + backgroundColor: colors.contrast_800, + }, + bg_contrast_900: { + backgroundColor: colors.contrast_900, + }, + bg_contrast_950: { + backgroundColor: colors.contrast_950, + }, + bg_contrast_975: { + backgroundColor: colors.contrast_975, + }, + bg_primary_500: { + backgroundColor: colors.primary_500, + }, + border_contrast_low: { + borderColor: colors.contrast_100, + }, + border_contrast_medium: { + borderColor: colors.contrast_200, + }, + border_contrast_high: { + borderColor: colors.contrast_300, + }, + }, +} + +export function style(styleObjects: Record[]) { + return Object.assign({}, ...styleObjects) +} + +export function gradient(color: keyof typeof tokens.gradients) { + const {values} = tokens.gradients[color] + return values + .map(([pos, color]) => { + return `${color} ${pos * 100}%` + }) + .join(', ') +} diff --git a/bskyogcard/src/theme/tokens.ts b/bskyogcard/src/theme/tokens.ts new file mode 100644 index 0000000000..9bfaa6cc4b --- /dev/null +++ b/bskyogcard/src/theme/tokens.ts @@ -0,0 +1,104 @@ +export const space = { + _2xs: 2, + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 20, + _2xl: 24, + _3xl: 28, + _4xl: 32, + _5xl: 40, +} as const + +export const fontSize = { + _2xs: 10, + xs: 12, + sm: 14, + md: 16, + lg: 18, + xl: 20, + _2xl: 22, + _3xl: 26, + _4xl: 32, + _5xl: 40, +} as const + +export const lineHeight = { + none: 1, + normal: 1.5, + relaxed: 1.625, +} as const + +export const borderRadius = { + _2xs: 2, + xs: 4, + sm: 8, + md: 12, + full: 999, +} as const + +export const fontWeight = { + normal: '400', + semibold: '500', + bold: '600', + heavy: '700', +} as const + +export const gradients = { + sky: { + values: [ + [0, '#0A7AFF'], + [1, '#59B9FF'], + ], + hover_value: '#0A7AFF', + }, + midnight: { + values: [ + [0, '#022C5E'], + [1, '#4079BC'], + ], + hover_value: '#022C5E', + }, + sunrise: { + values: [ + [0, '#4E90AE'], + [0.4, '#AEA3AB'], + [0.8, '#E6A98F'], + [1, '#F3A84C'], + ], + hover_value: '#AEA3AB', + }, + sunset: { + values: [ + [0, '#6772AF'], + [0.6, '#B88BB6'], + [1, '#FFA6AC'], + ], + hover_value: '#B88BB6', + }, + summer: { + values: [ + [0, '#FF6A56'], + [0.3, '#FF9156'], + [1, '#FFDD87'], + ], + hover_value: '#FF9156', + }, + nordic: { + values: [ + [0, '#083367'], + [1, '#9EE8C1'], + ], + hover_value: '#3A7085', + }, + bonfire: { + values: [ + [0, '#203E4E'], + [0.4, '#755B62'], + [0.8, '#CD7765'], + [1, '#EF956E'], + ], + hover_value: '#755B62', + }, +} as const diff --git a/bskyogcard/src/util/formatCount.ts b/bskyogcard/src/util/formatCount.ts new file mode 100644 index 0000000000..47d6fa4d78 --- /dev/null +++ b/bskyogcard/src/util/formatCount.ts @@ -0,0 +1,10 @@ +export function formatCount(count: number) { + return Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, + // `1,953` shouldn't be rounded up to 2k, it should be truncated. + // @ts-ignore + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode + roundingMode: 'trunc', + }).format(count) +} diff --git a/bskyogcard/src/util/formatDate.ts b/bskyogcard/src/util/formatDate.ts new file mode 100644 index 0000000000..10dcacce5b --- /dev/null +++ b/bskyogcard/src/util/formatDate.ts @@ -0,0 +1,11 @@ +export function formatDate(date: number | string | Date) { + const d = new Date(date) + return `${d.toLocaleDateString('en-us', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} at ${d.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + })}` +} diff --git a/bskyogcard/src/util/getImage.ts b/bskyogcard/src/util/getImage.ts new file mode 100644 index 0000000000..e58bf8e4da --- /dev/null +++ b/bskyogcard/src/util/getImage.ts @@ -0,0 +1,12 @@ +import MIMEType from 'whatwg-mimetype' + +export async function getImage(url: string) { + const response = await fetch(url) + const mimeType = new MIMEType(response.headers.get('content-type') ?? '') + const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded + if (response.status !== 200) return null + return { + mime: mimeType.essence as string | undefined, + image: Buffer.from(arrayBuf), + } +} diff --git a/bskyogcard/src/util/getStarterPackImageUri.ts b/bskyogcard/src/util/getStarterPackImageUri.ts new file mode 100644 index 0000000000..5d44dbb696 --- /dev/null +++ b/bskyogcard/src/util/getStarterPackImageUri.ts @@ -0,0 +1,13 @@ +import {AppBskyGraphDefs, AtUri} from '@atproto/api' + +const DEV = process.env.NODE_ENV === 'development' +const HOST = DEV + ? `http://localhost:3000/start` + : `https://ogcard.cdn.bsky.app/start` + +export function getStarterPackImageUri( + record: AppBskyGraphDefs.StarterPackViewBasic, +) { + const urip = new AtUri(record.uri) + return `${HOST}/${record.creator.did}/${urip.rkey}` +} diff --git a/bskyogcard/src/util/resolvePostData.ts b/bskyogcard/src/util/resolvePostData.ts new file mode 100644 index 0000000000..f8fb3e8175 --- /dev/null +++ b/bskyogcard/src/util/resolvePostData.ts @@ -0,0 +1,322 @@ +import { + AppBskyEmbedExternal, + AppBskyEmbedImages, + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyGraphDefs, + AtpAgent, + RichText, +} from '@atproto/api' + +import {httpLogger} from '../logger.js' +import {getImage} from './getImage.js' +import {getStarterPackImageUri} from './getStarterPackImageUri.js' + +export type Metadata = { + aspectRatio: { + width: number + height: number + } +} + +export type Image = Metadata & { + mime: 'image/jpeg' | 'image/png' | string | undefined + image: Buffer +} + +export type PostData = { + images: Map + texts: Map +} + +function normalizeAspectRatio(aspectRatio?: { + width: number + height: number +}): Metadata['aspectRatio'] { + if (!aspectRatio) + return { + width: 1000, + height: 1000, + } + return aspectRatio +} + +export async function resolvePostData( + post: AppBskyFeedDefs.PostView, + agent: AtpAgent, +): Promise { + const images: Map = new Map() + const texts: Map = new Map() + + // console.log(JSON.stringify(post, null, 2)) + + if (post.author.avatar) { + images.set(post.author.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + + if (AppBskyFeedPost.isRecord(post.record) && post.record.text) { + texts.set(post.record.text, new RichText({text: post.record.text})) + } + + if (post.embed) { + if (AppBskyEmbedImages.isView(post.embed)) { + // get OPs media + for (const image of post.embed.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } + + if (AppBskyEmbedExternal.isView(post.embed)) { + if (post.embed.external.thumb) { + images.set(post.embed.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + + if (AppBskyEmbedRecord.isView(post.embed)) { + if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) { + if (post.embed.record.author.avatar) { + images.set(post.embed.record.author.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + + for (const embed of post.embed.record.embeds) { + if (AppBskyEmbedImages.isView(embed)) { + for (const image of embed.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } + if (AppBskyEmbedExternal.isView(embed)) { + if (embed.external.thumb) { + images.set(embed.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + if (AppBskyEmbedRecordWithMedia.isView(embed)) { + if (AppBskyEmbedImages.isView(embed.media)) { + for (const image of embed.media.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } else if (AppBskyEmbedExternal.isView(embed.media)) { + if (embed.media.external.thumb) { + images.set(embed.media.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + } + + if (AppBskyGraphDefs.isListView(embed.record)) { + if (embed.record.avatar) { + images.set(embed.record.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + } + + if (AppBskyFeedDefs.isGeneratorView(embed.record)) { + if (embed.record.avatar) { + images.set(embed.record.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + } + + if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { + const uri = getStarterPackImageUri(embed.record) + images.set(uri, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + + if ( + AppBskyFeedPost.isRecord(post.embed.record.value) && + post.embed.record.value.text + ) { + texts.set( + post.embed.record.value.text, + new RichText({text: post.embed.record.value.text}), + ) + } + } + + if (AppBskyGraphDefs.isListView(post.embed.record)) { + if (post.embed.record.avatar) { + images.set(post.embed.record.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + } + + if (AppBskyFeedDefs.isGeneratorView(post.embed.record)) { + if (post.embed.record.avatar) { + images.set(post.embed.record.avatar, { + aspectRatio: { + width: 1000, + height: 1000, + }, + }) + } + } + + if (AppBskyGraphDefs.isStarterPackViewBasic(post.embed.record)) { + const uri = getStarterPackImageUri(post.embed.record) + images.set(uri, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + + if (AppBskyEmbedRecordWithMedia.isView(post.embed)) { + // get OPs media + if (AppBskyEmbedImages.isView(post.embed.media)) { + for (const image of post.embed.media.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } + + if (AppBskyEmbedExternal.isView(post.embed.media)) { + if (post.embed.media.external.thumb) { + images.set(post.embed.media.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + + // get media from embedded post + if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) { + for (const embed of post.embed.record.record.embeds) { + if (AppBskyEmbedImages.isView(embed)) { + for (const image of embed.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } + if (AppBskyEmbedExternal.isView(embed)) { + if (embed.external.thumb) { + images.set(embed.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + if (AppBskyEmbedRecordWithMedia.isView(embed)) { + if (AppBskyEmbedImages.isView(embed.media)) { + for (const image of embed.media.images) { + images.set(image.fullsize, { + aspectRatio: normalizeAspectRatio(image.aspectRatio), + }) + } + } else if (AppBskyEmbedExternal.isView(embed.media)) { + if (embed.media.external.thumb) { + images.set(embed.media.external.thumb, { + aspectRatio: { + width: 1200, + height: 630, + }, + }) + } + } + } + } + + if ( + AppBskyFeedPost.isRecord(post.embed.record.record.value) && + post.embed.record.record.value.text + ) { + texts.set( + post.embed.record.record.value.text, + new RichText({text: post.embed.record.record.value.text}), + ) + } + } + } + } + + const deduped = Array.from(images.entries()) + const resolved = await Promise.all( + deduped.map(async ([url, meta]) => { + try { + const image = await getImage(url) + return [ + url, + { + ...meta, + ...image, + }, + ] as const + } catch (err) { + httpLogger.warn({err, uri: url}, 'could not fetch image') + return [ + url, + { + ...meta, + image: null, + }, + ] as const + } + }), + ) + await Promise.all(Array.from(texts.values()).map(r => r.detectFacets(agent))) + const extracted = resolved.filter(([, i]) => i.image !== null) as [ + string, + Image, + ][] + + return { + images: new Map(extracted), + texts, + } +} diff --git a/bskyogcard/src/util/toShortUrl.ts b/bskyogcard/src/util/toShortUrl.ts new file mode 100644 index 0000000000..56971a4a2b --- /dev/null +++ b/bskyogcard/src/util/toShortUrl.ts @@ -0,0 +1,16 @@ +export function toShortUrl(url: string): string { + try { + const urlp = new URL(url) + if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') { + return url + } + const path = + (urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash + if (path.length > 15) { + return urlp.host + path.slice(0, 13) + '...' + } + return urlp.host + path + } catch (e) { + return url + } +} diff --git a/bskyogcard/yarn.lock b/bskyogcard/yarn.lock index 484aee9ee8..08b65797ac 100644 --- a/bskyogcard/yarn.lock +++ b/bskyogcard/yarn.lock @@ -2,15 +2,16 @@ # yarn lockfile v1 -"@atproto/api@0.12.19-next.0": - version "0.12.19-next.0" - resolved "https://registry.npmjs.org/@atproto/api/-/api-0.12.19-next.0.tgz" - integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg== +"@atproto/api@^0.13.4": + version "0.13.4" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.4.tgz#47631e622760b2a18e4aee75b728e9cf8952e221" + integrity sha512-Fwn37hP+Xr9YjA/hadvn7ZKbUPQhJiUus1+govgNKF3/jNdyk2ICoEe0z+hxaO3xX8LCU5yARbgt3SRoXbIwrg== dependencies: "@atproto/common-web" "^0.3.0" - "@atproto/lexicon" "^0.4.0" + "@atproto/lexicon" "^0.4.1" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" + "@atproto/xrpc" "^0.6.1" + await-lock "^2.2.2" multiformats "^9.9.0" tlds "^1.234.0" @@ -36,29 +37,29 @@ multiformats "^9.9.0" pino "^8.15.0" -"@atproto/lexicon@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz" - integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ== +"@atproto/lexicon@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0" + integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/syntax" "^0.3.0" iso-datestring-validator "^2.2.2" multiformats "^9.9.0" - zod "^3.21.4" + zod "^3.23.8" "@atproto/syntax@^0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== -"@atproto/xrpc@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz" - integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog== +"@atproto/xrpc@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5" + integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A== dependencies: - "@atproto/lexicon" "^0.4.0" - zod "^3.21.4" + "@atproto/lexicon" "^0.4.1" + zod "^3.23.8" "@cbor-extract/cbor-extract-darwin-arm64@2.2.0": version "2.2.0" @@ -278,6 +279,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +await-lock@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" + integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== + base64-js@0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz" @@ -371,6 +377,11 @@ color-name@^1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -422,6 +433,11 @@ css-to-react-native@^3.0.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -478,6 +494,13 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" @@ -547,6 +570,11 @@ express@^4.19.2: utils-merge "1.0.1" vary "~1.1.2" +fast-copy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35" + integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ== + fast-printf@^1.6.9: version "1.6.9" resolved "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz" @@ -559,6 +587,11 @@ fast-redact@^3.1.1: resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz" integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fflate@^0.7.3: version "0.7.4" resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz" @@ -653,6 +686,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + hex-rgb@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz" @@ -706,6 +744,11 @@ iso-datestring-validator@^2.2.2: resolved "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz" integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -779,6 +822,11 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -823,6 +871,13 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" @@ -865,7 +920,7 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -pino-abstract-transport@^1.2.0: +pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz" integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== @@ -873,6 +928,26 @@ pino-abstract-transport@^1.2.0: readable-stream "^4.0.0" split2 "^4.0.0" +pino-pretty@^11.2.2: + version "11.2.2" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.2.tgz#5e8ec69b31e90eb187715af07b1d29a544e60d39" + integrity sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.2" + fast-safe-stringify "^2.1.1" + help-me "^5.0.0" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^4.0.1" + strip-json-comments "^3.1.1" + pino-std-serializers@^6.0.0: version "6.2.2" resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz" @@ -940,6 +1015,14 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + qs@6.11.0: version "6.11.0" resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" @@ -1030,6 +1113,11 @@ satori@^0.10.13: postcss-value-parser "^4.2.0" yoga-wasm-web "^0.3.3" +secure-json-parse@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz" @@ -1127,6 +1215,11 @@ string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + thread-stream@^2.6.0: version "2.7.0" resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz" @@ -1253,6 +1346,16 @@ vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -1263,7 +1366,7 @@ yoga-wasm-web@^0.3.3: resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz" integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== -zod@^3.21.4: +zod@^3.21.4, zod@^3.23.8: version "3.23.8" resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==