Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion apps/api/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -17,7 +19,10 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<
Pick<Cloudflare.Env, "AUTH_SECRET" | "EMBED_USER_AGENT">
Pick<
Cloudflare.Env,
"AUTH_SECRET" | "EMBED_USER_AGENT" | "REDDIT_CLIENT_ID" | "REDDIT_CLIENT_SECRET"
>
> {}
}

Expand Down
6 changes: 0 additions & 6 deletions apps/docs/content/docs/embedly/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ Embedly replaces Discord's broken native embeds with rich, accurate ones. Paste
href="/docs/platforms/instagram"
icon={<Icon icon="simple-icons:instagram" />}
/>
<Card
title="Reddit"
description="Posts with text, images, galleries, and video"
href="/docs/platforms/reddit"
icon={<Icon icon="simple-icons:reddit" />}
/>
<Card
title="Threads"
description="Posts with text, images, carousels, and video"
Expand Down
6 changes: 0 additions & 6 deletions apps/docs/content/docs/platforms/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ description: Every platform Embedly supports and what it embeds.
href="/docs/platforms/instagram"
icon={<Icon icon="simple-icons:instagram" />}
/>
<Card
title="Reddit"
description="Text posts, image posts, galleries, and video"
href="/docs/platforms/reddit"
icon={<Icon icon="simple-icons:reddit" />}
/>
<Card
title="Threads"
description="Posts with images, carousels, and video"
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/platforms/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Platforms",
"icon": "ph:globe-hemisphere-west",
"pages": ["twitter", "instagram", "reddit", "threads", "tiktok"]
"pages": ["twitter", "instagram", "threads", "tiktok"]
}
26 changes: 5 additions & 21 deletions apps/docs/content/docs/platforms/reddit.mdx
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
---
title: Reddit
icon: simple-icons:reddit
description: Posts with text, images, galleries, and video.
description: Reddit support is temporarily disabled.
---

All Reddit URL formats are supported. Short links (`redd.it`) and old/mobile URLs are automatically resolved before fetching.
Reddit support is temporarily disabled while Reddit API access is pending.

## Supported URLs
Reddit now requires explicit approval before apps can access Reddit data through the API. Embedly will restore Reddit embeds after API access is approved and configured.

## Previous URL formats

```
https://reddit.com/r/<subreddit>/comments/<id>/<slug>
https://old.reddit.com/r/<subreddit>/comments/<id>
https://redd.it/<id>
```

## 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.

<Callout type="info">
Reddit doesn't expose upvote or comment counts reliably, so stats are not shown.
</Callout>
2 changes: 1 addition & 1 deletion apps/docs/src/routes/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</p>
</section>

Expand Down
1 change: 0 additions & 1 deletion packages/platforms/src/platforms/index.ts
Original file line number Diff line number Diff line change
@@ -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";
83 changes: 64 additions & 19 deletions packages/platforms/src/platforms/reddit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ const MATCH_RE =
const FOLLOWUP_RE =
/^(?:https?:\/\/)?(?:www\.|old\.|m\.)?reddit\.com\/r\/(?<subreddit>\w+)\/comments\/(?<post_id>[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<string, any>;
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<string, any>): NormalizedPost["media"] {
if (raw.domain === "i.redd.it") {
return [
Expand Down Expand Up @@ -46,12 +90,17 @@ export const Reddit: Platform<"Reddit", Record<string, any>, {}> = {
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;
Expand All @@ -60,14 +109,18 @@ export const Reddit: Platform<"Reddit", Record<string, any>, {}> = {
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 };
Expand All @@ -88,15 +141,7 @@ export const Reddit: Platform<"Reddit", Record<string, any>, {}> = {
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,
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface TransformOptions {

interface FetchEnv {
EMBED_USER_AGENT: string;
REDDIT_CLIENT_ID?: string;
REDDIT_CLIENT_SECRET?: string;
}

export interface Platform<PlatformName extends string, PlatformData, PlatformMeta> {
Expand Down
Loading