init commit,

This commit is contained in:
louiscklaw
2025-05-28 09:55:51 +08:00
commit efe70ceb69
8042 changed files with 951668 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
import type { FormattedTweet } from './types.ts'
/**
* Supports plain text, images, quote tweets.
*
* Needs support for images, GIFs, and replies maybe?
* Styles use !important to override Tailwind .prose inside MDX.
*/
export default function Tweet({
id,
text,
author,
media,
created_at,
public_metrics,
referenced_tweets,
}: FormattedTweet) {
const authorUrl = `https://twitter.com/${author.username}`
const likeUrl = `https://twitter.com/intent/like?tweet_id=${id}`
const retweetUrl = `https://twitter.com/intent/retweet?tweet_id=${id}`
const replyUrl = `https://twitter.com/intent/tweet?in_reply_to=${id}`
const tweetUrl = `https://twitter.com/${author.username}/status/${id}`
const createdAt = new Date(created_at)
const formattedText = text.replace(/https:\/\/[\n\S]+/g, '').replace('&', '&')
const quoteTweet = referenced_tweets && referenced_tweets.find((t) => t.type === 'quoted')
return (
<div tw="flex">
<div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
<h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-left">
<span>{formattedText}</span>
<span tw="text-gray-300">{`- @${author.username}`}</span>
</h2>
</div>
</div>
)
}

View File

@@ -0,0 +1,61 @@
import type { TwitterApiResponse, Tweet, FormattedTweet } from './types.ts'
export const getTweets: (ids: string[]) => Promise<FormattedTweet[]> = async (ids: string[]) => {
if (ids.length === 0) {
return []
}
const queryParams = new URLSearchParams({
ids: ids.join(','),
expansions:
'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
'tweet.fields':
'attachments,author_id,public_metrics,created_at,id,in_reply_to_user_id,referenced_tweets,text',
'user.fields': 'id,name,profile_image_url,protected,url,username,verified',
'media.fields': 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics',
})
const response = await fetch(`https://api.twitter.com/2/tweets?${queryParams}`, {
headers: {
Authorization: `Bearer ${Deno.env.get('TWITTER_API_TOKEN')}`,
},
})
const tweets: TwitterApiResponse = await response.json()
const getAuthorInfo = (author_id: string) => {
return tweets.includes.users.find((user) => user.id === author_id)
}
const getReferencedTweets = (mainTweet: Tweet) => {
return (
mainTweet?.referenced_tweets?.map((referencedTweet) => {
const fullReferencedTweet = tweets.includes.tweets.find(
(tweet) => tweet.id === referencedTweet.id
)
return {
type: referencedTweet.type,
author: getAuthorInfo(fullReferencedTweet!.author_id),
...fullReferencedTweet,
}
}) || []
)
}
return (
tweets.data.reduce<any>((allTweets, tweet) => {
const tweetWithAuthor = {
...tweet,
media:
tweet?.attachments?.media_keys.map((key: string) =>
tweets.includes.media.find((media) => media.media_key === key)
) || [],
referenced_tweets: getReferencedTweets(tweet),
author: getAuthorInfo(tweet.author_id),
}
return [tweetWithAuthor, ...allTweets]
}, []) || [] // If the Twitter API key isn't set, don't break the build
)
}

View File

@@ -0,0 +1,93 @@
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'
import { getTweets } from './getTweet.ts'
import Tweet from './Tweet.tsx'
const STORAGE_URL =
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
// Load custom font
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
export async function handler(req: Request) {
const url = new URL(req.url)
const tweetId = url.searchParams.get('tweetId')
if (!tweetId)
return new Response(JSON.stringify({ error: 'missing tweetId param' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
try {
// Try to get image from Supabase Storage CDN.
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
if (storageResponse.ok) return storageResponse
// Else, generate image and upload to storage.
const fontData = await font
const tweets = await getTweets([tweetId])
const tweet = tweets[0]
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
const generatedImage = new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1c1c1c',
color: '#EDEDED',
}}
>
<Tweet {...tweet} />
</div>
),
{
width: 1200,
height: 630,
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
// Default to 'twemoji'
emoji: 'twemoji',
fonts: [
{
name: 'Circular',
data: fontData,
style: 'normal',
},
],
}
)
const supabaseAdminClient = createClient(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Upload image to storage.
const { error } = await supabaseAdminClient.storage
.from('images')
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
contentType: 'image/png',
cacheControl: '31536000',
upsert: false,
})
if (error) throw error
return generatedImage
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
}

View File

@@ -0,0 +1,9 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { handler } from './handler.tsx'
console.log(`Function "tweet-to-image" up and running!`)
Deno.serve(handler)

View File

@@ -0,0 +1,74 @@
export type Tweet = {
edit_history_tweet_ids: string[]
text: string
referenced_tweets:
| Array<{
type: string
id: string
}>
| undefined
attachments: {
media_keys: string[]
}
id: string
created_at: string
public_metrics: {
retweet_count: number
reply_count: number
like_count: number
quote_count: number
impression_count: number
}
author_id: string
author?: User
}
export type Media = {
width: number
media_key: string
type: string
url: string
height: number
}
export type User = {
name: string
url: string
verified: boolean
profile_image_url: string
id: string
username: string
protected: boolean
}
// raw
export type TwitterApiResponse = {
data: Tweet[]
includes: {
media: Media[]
users: User[]
tweets: Tweet[]
}
}
export type FormattedTweet = {
type: string
edit_history_tweet_ids: string[]
text: string
referenced_tweets: FormattedTweet[]
attachments: {
media_keys: string[]
}
id: string
created_at: string
public_metrics: {
retweet_count: number
reply_count: number
like_count: number
quote_count: number
impression_count: number
}
author_id: string
media: Media[]
author: User
}