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 {