diff --git a/.github/workflows/syndicate-post.yml b/.github/workflows/syndicate-post.yml index 1a470268..429cf908 100644 --- a/.github/workflows/syndicate-post.yml +++ b/.github/workflows/syndicate-post.yml @@ -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 }} diff --git a/apps/sanity/functions/syndicate-content/index.ts b/apps/sanity/functions/syndicate-content/index.ts index 68df19b8..4c0d9ae8 100644 --- a/apps/sanity/functions/syndicate-content/index.ts +++ b/apps/sanity/functions/syndicate-content/index.ts @@ -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 }>; @@ -169,10 +171,13 @@ export const handler = documentEventHandler( 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) : []), ]; @@ -199,7 +204,9 @@ export const handler = documentEventHandler( ? "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", @@ -1013,6 +1020,152 @@ export const handler = documentEventHandler( 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 = diff --git a/scripts/run-syndicate-post.mjs b/scripts/run-syndicate-post.mjs index 819ba60d..152b2c03 100644 --- a/scripts/run-syndicate-post.mjs +++ b/scripts/run-syndicate-post.mjs @@ -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 = [ diff --git a/tools/reddit-devvit/devvit.yaml b/tools/reddit-devvit/devvit.yaml index db46c421..2950985b 100644 --- a/tools/reddit-devvit/devvit.yaml +++ b/tools/reddit-devvit/devvit.yaml @@ -1,2 +1,2 @@ -name: codingcat-syndicator +name: codingcat-sync version: 0.1.0 diff --git a/tools/reddit-devvit/src/main.ts b/tools/reddit-devvit/src/main.ts index c1e01e87..4868a151 100644 --- a/tools/reddit-devvit/src/main.ts +++ b/tools/reddit-devvit/src/main.ts @@ -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, }, ]);