diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index c02cbaf..0e6d5d7 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -99,7 +99,11 @@ const app = new Hono<{ Bindings: CloudflareBindings }>() let raw: unknown; try { - raw = await p.fetch(id, { EMBED_USER_AGENT: c.env.EMBED_USER_AGENT }); + raw = await p.fetch(id, { + EMBED_USER_AGENT: c.env.EMBED_USER_AGENT, + REDDIT_CLIENT_ID: c.env.REDDIT_CLIENT_ID, + REDDIT_CLIENT_SECRET: c.env.REDDIT_CLIENT_SECRET, + }); } catch (cause) { const problem = createProblem(EmbedlyErrors.PlatformFetchFailed, { request_id: requestId, diff --git a/apps/api/worker-configuration.d.ts b/apps/api/worker-configuration.d.ts index b656ed5..bf4e80c 100644 --- a/apps/api/worker-configuration.d.ts +++ b/apps/api/worker-configuration.d.ts @@ -9,6 +9,8 @@ declare namespace Cloudflare { CACHE: KVNamespace; AUTH_SECRET: string; EMBED_USER_AGENT: string; + REDDIT_CLIENT_ID: string; + REDDIT_CLIENT_SECRET: string; } } interface CloudflareBindings extends Cloudflare.Env {} @@ -17,7 +19,10 @@ type StringifyValues> = { }; declare namespace NodeJS { interface ProcessEnv extends StringifyValues< - Pick + Pick< + Cloudflare.Env, + "AUTH_SECRET" | "EMBED_USER_AGENT" | "REDDIT_CLIENT_ID" | "REDDIT_CLIENT_SECRET" + > > {} } diff --git a/apps/docs/content/docs/embedly/index.mdx b/apps/docs/content/docs/embedly/index.mdx index 4d10321..bbb573a 100644 --- a/apps/docs/content/docs/embedly/index.mdx +++ b/apps/docs/content/docs/embedly/index.mdx @@ -43,12 +43,6 @@ Embedly replaces Discord's broken native embeds with rich, accurate ones. Paste href="/docs/platforms/instagram" icon={} /> - } - /> } /> - } - /> /comments// https://old.reddit.com/r//comments/ https://redd.it/ ``` - -## Supported features - -### Text - -The post title and body. For link posts with no media, the linked URL is appended to the description. - -### Media - -Up to 10 items — single images, image galleries, Reddit-hosted videos, and preview images for external link posts. - -### Author - -The subreddit is shown as the display name, with the post author as the username. - - - Reddit doesn't expose upvote or comment counts reliably, so stats are not shown. - diff --git a/apps/docs/src/routes/privacy.tsx b/apps/docs/src/routes/privacy.tsx index 806af96..8bc87ca 100644 --- a/apps/docs/src/routes/privacy.tsx +++ b/apps/docs/src/routes/privacy.tsx @@ -81,7 +81,7 @@ function Privacy() { Cloudflare KV for public post cache, self-hosted DragonflyDB for message-cache mappings, self-hosted Grafana LGTM for bot observability, Cloudflare Workers observability for API logs and traces, and platform APIs for public posts. Supported - platforms are Twitter/X, Bluesky, Instagram, Reddit, TikTok and Threads. + platforms are Twitter/X, Bluesky, Instagram, TikTok and Threads.

diff --git a/packages/platforms/src/platforms/index.ts b/packages/platforms/src/platforms/index.ts index 1bcf284..4565694 100644 --- a/packages/platforms/src/platforms/index.ts +++ b/packages/platforms/src/platforms/index.ts @@ -1,6 +1,5 @@ export { Twitter } from "./twitter"; export { Bluesky } from "./bluesky"; export { Instagram } from "./instagram"; -export { Reddit } from "./reddit"; export { TikTok } from "./tiktok"; export { Threads } from "./threads"; diff --git a/packages/platforms/src/platforms/reddit.ts b/packages/platforms/src/platforms/reddit.ts index 03a379c..8f8525b 100644 --- a/packages/platforms/src/platforms/reddit.ts +++ b/packages/platforms/src/platforms/reddit.ts @@ -5,6 +5,50 @@ const MATCH_RE = const FOLLOWUP_RE = /^(?:https?:\/\/)?(?:www\.|old\.|m\.)?reddit\.com\/r\/(?\w+)\/comments\/(?[a-z0-9]+)/; +async function fetchAccessToken(env: { + EMBED_USER_AGENT: string; + REDDIT_CLIENT_ID?: string; + REDDIT_CLIENT_SECRET?: string; +}) { + if (!env.REDDIT_CLIENT_ID || !env.REDDIT_CLIENT_SECRET) { + throw { + code: 500, + message: "Reddit credentials are not configured", + }; + } + + const resp = await fetch("https://www.reddit.com/api/v1/access_token", { + method: "POST", + headers: { + Authorization: `Basic ${btoa(`${env.REDDIT_CLIENT_ID}:${env.REDDIT_CLIENT_SECRET}`)}`, + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": env.EMBED_USER_AGENT, + }, + body: new URLSearchParams({ grant_type: "client_credentials" }), + }); + + if (!resp.ok) { + throw { code: resp.status, message: resp.statusText }; + } + + const data = (await resp.json()) as Record; + return data.access_token as string; +} + +async function fetchReddit( + path: string, + env: { EMBED_USER_AGENT: string; REDDIT_CLIENT_ID?: string; REDDIT_CLIENT_SECRET?: string }, +) { + const token = await fetchAccessToken(env); + return fetch(`https://oauth.reddit.com${path}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "User-Agent": env.EMBED_USER_AGENT, + }, + }); +} + function parseMedia(raw: Record): NormalizedPost["media"] { if (raw.domain === "i.redd.it") { return [ @@ -46,12 +90,17 @@ export const Reddit: Platform<"Reddit", Record, {}> = { async match(url, env) { const match = url.match(MATCH_RE); if (!match) return null; + + const directGroups = url.match(FOLLOWUP_RE)?.groups; + if (directGroups) { + const { subreddit, post_id } = directGroups; + return `${subreddit}/${post_id}`; + } + const req = await fetch(url, { method: "GET", redirect: "follow", - headers: { - "User-Agent": env?.EMBED_USER_AGENT ?? "curl/8.7.1", - }, + headers: env ? { "User-Agent": env.EMBED_USER_AGENT } : undefined, }); const groups = req.url.match(FOLLOWUP_RE)?.groups; @@ -60,14 +109,18 @@ export const Reddit: Platform<"Reddit", Record, {}> = { return `${subreddit}/${post_id}`; }, async fetch(id, env) { + if (!env) { + throw { + code: 500, + message: "Reddit environment is not configured", + }; + } + const [subreddit, reddit_id] = id.split("/"); - const url = `https://www.reddit.com/r/${subreddit}/comments/${reddit_id}.json?raw_json=1`; - const postResp = await fetch(url, { - method: "GET", - headers: { - "User-Agent": env?.EMBED_USER_AGENT ?? "curl/8.7.1", - }, - }); + const postResp = await fetchReddit( + `/r/${subreddit}/comments/${reddit_id}.json?raw_json=1`, + env, + ); if (!postResp.ok) { throw { code: postResp.status, message: postResp.statusText }; @@ -88,15 +141,7 @@ export const Reddit: Platform<"Reddit", Record, {}> = { message: "Reddit post missing author information", }; } - const profileResp = await fetch( - `https://www.reddit.com/user/${authorName}/about.json?raw_json=1`, - { - method: "GET", - headers: { - "User-Agent": env?.EMBED_USER_AGENT ?? "", - }, - }, - ); + const profileResp = await fetchReddit(`/user/${authorName}/about.json?raw_json=1`, env); if (!profileResp.ok) { throw { code: profileResp.status, diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index 8962cef..c15519e 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -29,6 +29,8 @@ interface TransformOptions { interface FetchEnv { EMBED_USER_AGENT: string; + REDDIT_CLIENT_ID?: string; + REDDIT_CLIENT_SECRET?: string; } export interface Platform {