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
1 change: 1 addition & 0 deletions .github/workflows/syndicate-post.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
AWS_BUILDER_SESSION_TOKEN: ${{ secrets.AWS_BUILDER_SESSION_TOKEN }}
DAILY_DEV_TOKEN: ${{ secrets.DAILY_DEV_TOKEN }}
DAILY_DEV_SESSION_COOKIE: ${{ secrets.DAILY_DEV_SESSION_COOKIE }}
REDDIT_REFRESH_TOKEN: ${{ secrets.REDDIT_REFRESH_TOKEN }}
GOOGLE_WORKSPACE_REFRESH_TOKEN: ${{ secrets.GOOGLE_WORKSPACE_REFRESH_TOKEN }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
157 changes: 155 additions & 2 deletions apps/sanity/functions/syndicate-content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface ContentDocument {
hashnode?: string;
awsbuilder?: string;
dailydev?: string;
medium?: string;
reddit?: string;
tags?: Array<{ _key?: string; label?: string; value?: string }>;
sponsor?: Array<{ _ref: string }>;
syndications?: Array<{ _type: "reference"; _ref: string; _key?: string }>;
Expand Down Expand Up @@ -169,10 +171,13 @@ export const handler = documentEventHandler<ContentDocument>(
Boolean(data.awsbuilder) ||
process.env.FORCE_AWS_BUILDER === "true";

const platforms: Array<"devto" | "hashnode" | "dailydev" | "awsbuilder"> = [
const platforms: Array<
"devto" | "hashnode" | "dailydev" | "reddit" | "awsbuilder"
> = [
"devto",
"hashnode",
"dailydev",
"reddit",
...(shouldSyndicateAws ? (["awsbuilder"] as const) : []),
];

Expand All @@ -199,7 +204,9 @@ export const handler = documentEventHandler<ContentDocument>(
? "hashnode.com"
: platform === "dailydev"
? "daily.dev"
: "builder.aws.com";
: platform === "reddit"
? "reddit.com/r/CodingCatDev"
: "builder.aws.com";
await client.createOrReplace({
_id: syndicationDocId,
_type: "syndication",
Expand Down Expand Up @@ -1013,6 +1020,152 @@ export const handler = documentEventHandler<ContentDocument>(
console.log(
`[Syndicate Content] Successfully published to daily.dev Squad: ${externalUrl}`,
);
} else if (platform === "reddit") {
const redditRefreshToken = process.env.REDDIT_REFRESH_TOKEN;
if (!redditRefreshToken) {
console.warn(
"[Syndicate Content] REDDIT_REFRESH_TOKEN not configured. Skipping Reddit (r/CodingCatDev) live dispatch.",
);
continue;
}

const existingSyndication = await client
.getDocument(syndicationDocId)
.catch(() => null);
let redditUrl =
(existingSyndication?.externalUrl &&
!String(existingSyndication.externalUrl).includes("preview-simulated")
? String(existingSyndication.externalUrl)
: null) ||
data.reddit ||
null;
let thingId = existingSyndication?.externalId
? String(existingSyndication.externalId)
: null;

if (!redditUrl) {
const basicAuth = Buffer.from("TWTsqXa53CexlrYGBWaesQ:").toString("base64");
const refreshRes = await fetch("https://www.reddit.com/api/v1/access_token", {
method: "POST",
headers: {
Authorization: `Basic ${basicAuth}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: redditRefreshToken,
}),
});
const { access_token: redditAccessToken } = (await refreshRes.json()) as {
access_token?: string;
};
if (!redditAccessToken) {
throw new Error("Could not refresh Reddit OAuth access_token");
}

const targetLinkUrl = topYouTubeWatchUrl || canonicalUrl;
const prefix = data._type === "podcast" ? "[Podcast] " : "";
const postTitle = `${prefix}${data.title || "Untitled"}`.slice(0, 300);

const subRes = await fetch("https://oauth.reddit.com/api/submit", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
sr: "CodingCatDev",
kind: "link",
title: postTitle,
url: targetLinkUrl,
resubmit: "true",
}),
});
const subJson = (await subRes.json()) as any;
const subData = subJson?.json?.data;
if (!subData?.url) {
throw new Error(
`Reddit api/submit error: ${JSON.stringify(subJson?.json?.errors || subJson)}`,
);
}

redditUrl = subData.url;
thingId = subData.name || subData.id;

const commentText = [
data.excerpt ? `> ${data.excerpt.trim()}` : "",
"",
`📖 **Read the full guide, code snippets & transcript on CodingCat.dev:** [${canonicalUrl}](${canonicalUrl})`,
topYouTubeWatchUrl
? `📺 **Watch on YouTube & Subscribe:** [${topYouTubeWatchUrl}](${topYouTubeWatchUrl}${topYouTubeWatchUrl.includes("?") ? "&" : "?"}sub_confirmation=1)`
: `📺 **Subscribe to CodingCat.dev on YouTube:** [https://www.youtube.com/@CodingCatDev](https://www.youtube.com/@CodingCatDev?sub_confirmation=1)`,
]
.filter(Boolean)
.join("\n");

const comRes = await fetch("https://oauth.reddit.com/api/comment", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
thing_id: String(thingId),
text: commentText,
}),
});
const comJson = (await comRes.json()) as any;
const comName = comJson?.json?.data?.things?.[0]?.data?.name;
if (comName) {
await fetch("https://oauth.reddit.com/api/distinguish", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
id: String(comName),
how: "yes",
sticky: "true",
}),
});
}
}

if (redditUrl) {
const extractedId =
thingId ||
redditUrl.match(/comments\/([a-z0-9]+)/i)?.[1] ||
data._id;

await client.createOrReplace({
_id: syndicationDocId,
_type: "syndication",
target: { _type: "reference", _ref: data._id },
platform: "reddit",
status: "published",
canonicalUrl,
externalUrl: redditUrl,
externalId: String(extractedId),
syndicatedAt: new Date().toISOString(),
error: undefined,
});

if (data.reddit !== redditUrl) {
parentPatch.reddit = redditUrl;
}

console.log(
`[Syndicate Content] Successfully published to Reddit (r/CodingCatDev): ${redditUrl}`,
);
}
}
} catch (publishErr) {
const errMsg =
Expand Down
140 changes: 140 additions & 0 deletions scripts/run-syndicate-post.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,146 @@ async function run() {
}
}

// --- REDDIT (r/CodingCatDev) SYNDICATION ---
const redditRefreshToken = process.env.REDDIT_REFRESH_TOKEN;
if (redditRefreshToken) {
console.log("\n--- Syndicating to Reddit (r/CodingCatDev) ---");
const syndicationDocId = `syndication.${doc._id}.reddit`;
try {
const existingSyndication = await client.getDocument(syndicationDocId).catch(() => null);
let redditUrl =
(existingSyndication?.externalUrl &&
!String(existingSyndication.externalUrl).includes("preview-simulated")
? existingSyndication.externalUrl
: null) ||
doc.reddit ||
(doc._id === "post-php2shell-cve-2026-109618"
? "https://www.reddit.com/r/CodingCatDev/comments/1xf2yhz/php2shell_cve2026109618_how_ai_found_a_decadeold/"
: doc._id === "05eda048-efff-4e0f-8b76-83a2f482fa13"
? "https://www.reddit.com/r/CodingCatDev/comments/1xf2yg5/sanity_knowledge_bases_why_we_replaced_our_custom/"
: null);

let thingId = existingSyndication?.externalId || null;

if (!redditUrl) {
const basicAuth = Buffer.from("TWTsqXa53CexlrYGBWaesQ:").toString("base64");
const refreshRes = await fetch("https://www.reddit.com/api/v1/access_token", {
method: "POST",
headers: {
Authorization: `Basic ${basicAuth}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: redditRefreshToken,
}),
});
const { access_token: redditAccessToken } = await refreshRes.json();
if (!redditAccessToken) {
throw new Error("Could not refresh Reddit OAuth access_token");
}

const targetLinkUrl = topYouTubeWatchUrl || canonicalUrl;
const prefix = doc._type === "podcast" ? "[Podcast] " : "";
const postTitle = `${prefix}${doc.title || "Untitled"}`.slice(0, 300);

const subRes = await fetch("https://oauth.reddit.com/api/submit", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
sr: "CodingCatDev",
kind: "link",
title: postTitle,
url: targetLinkUrl,
resubmit: "true",
}),
});
const subJson = await subRes.json();
const subData = subJson?.json?.data;
if (!subData?.url) {
throw new Error(`Reddit api/submit error: ${JSON.stringify(subJson?.json?.errors || subJson)}`);
}

redditUrl = subData.url;
thingId = subData.name || subData.id;

const commentText = [
doc.excerpt ? `> ${doc.excerpt.trim()}` : "",
"",
`📖 **Read the full guide, code snippets & transcript on CodingCat.dev:** [${canonicalUrl}](${canonicalUrl})`,
topYouTubeWatchUrl
? `📺 **Watch on YouTube & Subscribe:** [${topYouTubeWatchUrl}](${topYouTubeWatchUrl}${topYouTubeWatchUrl.includes("?") ? "&" : "?"}sub_confirmation=1)`
: `📺 **Subscribe to CodingCat.dev on YouTube:** [https://www.youtube.com/@CodingCatDev](https://www.youtube.com/@CodingCatDev?sub_confirmation=1)`,
]
.filter(Boolean)
.join("\n");

const comRes = await fetch("https://oauth.reddit.com/api/comment", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
thing_id: thingId,
text: commentText,
}),
});
const comJson = await comRes.json();
const comName = comJson?.json?.data?.things?.[0]?.data?.name;
if (comName) {
await fetch("https://oauth.reddit.com/api/distinguish", {
method: "POST",
headers: {
Authorization: `Bearer ${redditAccessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
},
body: new URLSearchParams({
api_type: "json",
id: comName,
how: "yes",
sticky: "true",
}),
});
}
}

if (redditUrl) {
const extractedId =
thingId ||
redditUrl.match(/comments\/([a-z0-9]+)/i)?.[1] ||
doc._id;
console.log(`✓ Reddit (r/CodingCatDev) published: ${redditUrl}`);

await client.createOrReplace({
_id: syndicationDocId,
_type: "syndication",
target: { _type: "reference", _ref: doc._id },
platform: "reddit",
status: "published",
canonicalUrl,
externalUrl: redditUrl,
externalId: String(extractedId),
syndicatedAt: new Date().toISOString(),
});

await client.patch(doc._id).set({ reddit: redditUrl }).commit();
console.log(`✓ Updated Sanity doc ${doc._id} reddit URL`);
}
} catch (err) {
console.error("✗ Reddit (r/CodingCatDev) syndication error:", err);
}
}

// --- LINK SYNDICATION REFERENCES ON PARENT ---
try {
const platforms = [
Expand Down
2 changes: 1 addition & 1 deletion tools/reddit-devvit/devvit.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
name: codingcat-syndicator
name: codingcat-sync
version: 0.1.0
2 changes: 1 addition & 1 deletion tools/reddit-devvit/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Devvit.addSettings([
name: "sanityWriteToken",
label: "Sanity API Write Token (optional: writes Reddit URL back to Sanity)",
isSecret: true,
scope: SettingScope.Installation,
scope: SettingScope.App,
},
]);

Expand Down
Loading