Skip to content

Commit 130a982

Browse files
authored
Merge pull request #800 from CodingCatDev/dev
feat(syndication): automate Reddit r/CodingCatDev publishing with stickied mod comments
2 parents f49dd8a + 8148942 commit 130a982

5 files changed

Lines changed: 298 additions & 4 deletions

File tree

‎.github/workflows/syndicate-post.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
AWS_BUILDER_SESSION_TOKEN: ${{ secrets.AWS_BUILDER_SESSION_TOKEN }}
5656
DAILY_DEV_TOKEN: ${{ secrets.DAILY_DEV_TOKEN }}
5757
DAILY_DEV_SESSION_COOKIE: ${{ secrets.DAILY_DEV_SESSION_COOKIE }}
58+
REDDIT_REFRESH_TOKEN: ${{ secrets.REDDIT_REFRESH_TOKEN }}
5859
GOOGLE_WORKSPACE_REFRESH_TOKEN: ${{ secrets.GOOGLE_WORKSPACE_REFRESH_TOKEN }}
5960
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
6061
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}

‎apps/sanity/functions/syndicate-content/index.ts‎

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface ContentDocument {
1818
hashnode?: string;
1919
awsbuilder?: string;
2020
dailydev?: string;
21+
medium?: string;
22+
reddit?: string;
2123
tags?: Array<{ _key?: string; label?: string; value?: string }>;
2224
sponsor?: Array<{ _ref: string }>;
2325
syndications?: Array<{ _type: "reference"; _ref: string; _key?: string }>;
@@ -169,10 +171,13 @@ export const handler = documentEventHandler<ContentDocument>(
169171
Boolean(data.awsbuilder) ||
170172
process.env.FORCE_AWS_BUILDER === "true";
171173

172-
const platforms: Array<"devto" | "hashnode" | "dailydev" | "awsbuilder"> = [
174+
const platforms: Array<
175+
"devto" | "hashnode" | "dailydev" | "reddit" | "awsbuilder"
176+
> = [
173177
"devto",
174178
"hashnode",
175179
"dailydev",
180+
"reddit",
176181
...(shouldSyndicateAws ? (["awsbuilder"] as const) : []),
177182
];
178183

@@ -199,7 +204,9 @@ export const handler = documentEventHandler<ContentDocument>(
199204
? "hashnode.com"
200205
: platform === "dailydev"
201206
? "daily.dev"
202-
: "builder.aws.com";
207+
: platform === "reddit"
208+
? "reddit.com/r/CodingCatDev"
209+
: "builder.aws.com";
203210
await client.createOrReplace({
204211
_id: syndicationDocId,
205212
_type: "syndication",
@@ -1013,6 +1020,152 @@ export const handler = documentEventHandler<ContentDocument>(
10131020
console.log(
10141021
`[Syndicate Content] Successfully published to daily.dev Squad: ${externalUrl}`,
10151022
);
1023+
} else if (platform === "reddit") {
1024+
const redditRefreshToken = process.env.REDDIT_REFRESH_TOKEN;
1025+
if (!redditRefreshToken) {
1026+
console.warn(
1027+
"[Syndicate Content] REDDIT_REFRESH_TOKEN not configured. Skipping Reddit (r/CodingCatDev) live dispatch.",
1028+
);
1029+
continue;
1030+
}
1031+
1032+
const existingSyndication = await client
1033+
.getDocument(syndicationDocId)
1034+
.catch(() => null);
1035+
let redditUrl =
1036+
(existingSyndication?.externalUrl &&
1037+
!String(existingSyndication.externalUrl).includes("preview-simulated")
1038+
? String(existingSyndication.externalUrl)
1039+
: null) ||
1040+
data.reddit ||
1041+
null;
1042+
let thingId = existingSyndication?.externalId
1043+
? String(existingSyndication.externalId)
1044+
: null;
1045+
1046+
if (!redditUrl) {
1047+
const basicAuth = Buffer.from("TWTsqXa53CexlrYGBWaesQ:").toString("base64");
1048+
const refreshRes = await fetch("https://www.reddit.com/api/v1/access_token", {
1049+
method: "POST",
1050+
headers: {
1051+
Authorization: `Basic ${basicAuth}`,
1052+
"Content-Type": "application/x-www-form-urlencoded",
1053+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
1054+
},
1055+
body: new URLSearchParams({
1056+
grant_type: "refresh_token",
1057+
refresh_token: redditRefreshToken,
1058+
}),
1059+
});
1060+
const { access_token: redditAccessToken } = (await refreshRes.json()) as {
1061+
access_token?: string;
1062+
};
1063+
if (!redditAccessToken) {
1064+
throw new Error("Could not refresh Reddit OAuth access_token");
1065+
}
1066+
1067+
const targetLinkUrl = topYouTubeWatchUrl || canonicalUrl;
1068+
const prefix = data._type === "podcast" ? "[Podcast] " : "";
1069+
const postTitle = `${prefix}${data.title || "Untitled"}`.slice(0, 300);
1070+
1071+
const subRes = await fetch("https://oauth.reddit.com/api/submit", {
1072+
method: "POST",
1073+
headers: {
1074+
Authorization: `Bearer ${redditAccessToken}`,
1075+
"Content-Type": "application/x-www-form-urlencoded",
1076+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
1077+
},
1078+
body: new URLSearchParams({
1079+
api_type: "json",
1080+
sr: "CodingCatDev",
1081+
kind: "link",
1082+
title: postTitle,
1083+
url: targetLinkUrl,
1084+
resubmit: "true",
1085+
}),
1086+
});
1087+
const subJson = (await subRes.json()) as any;
1088+
const subData = subJson?.json?.data;
1089+
if (!subData?.url) {
1090+
throw new Error(
1091+
`Reddit api/submit error: ${JSON.stringify(subJson?.json?.errors || subJson)}`,
1092+
);
1093+
}
1094+
1095+
redditUrl = subData.url;
1096+
thingId = subData.name || subData.id;
1097+
1098+
const commentText = [
1099+
data.excerpt ? `> ${data.excerpt.trim()}` : "",
1100+
"",
1101+
`📖 **Read the full guide, code snippets & transcript on CodingCat.dev:** [${canonicalUrl}](${canonicalUrl})`,
1102+
topYouTubeWatchUrl
1103+
? `📺 **Watch on YouTube & Subscribe:** [${topYouTubeWatchUrl}](${topYouTubeWatchUrl}${topYouTubeWatchUrl.includes("?") ? "&" : "?"}sub_confirmation=1)`
1104+
: `📺 **Subscribe to CodingCat.dev on YouTube:** [https://www.youtube.com/@CodingCatDev](https://www.youtube.com/@CodingCatDev?sub_confirmation=1)`,
1105+
]
1106+
.filter(Boolean)
1107+
.join("\n");
1108+
1109+
const comRes = await fetch("https://oauth.reddit.com/api/comment", {
1110+
method: "POST",
1111+
headers: {
1112+
Authorization: `Bearer ${redditAccessToken}`,
1113+
"Content-Type": "application/x-www-form-urlencoded",
1114+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
1115+
},
1116+
body: new URLSearchParams({
1117+
api_type: "json",
1118+
thing_id: String(thingId),
1119+
text: commentText,
1120+
}),
1121+
});
1122+
const comJson = (await comRes.json()) as any;
1123+
const comName = comJson?.json?.data?.things?.[0]?.data?.name;
1124+
if (comName) {
1125+
await fetch("https://oauth.reddit.com/api/distinguish", {
1126+
method: "POST",
1127+
headers: {
1128+
Authorization: `Bearer ${redditAccessToken}`,
1129+
"Content-Type": "application/x-www-form-urlencoded",
1130+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
1131+
},
1132+
body: new URLSearchParams({
1133+
api_type: "json",
1134+
id: String(comName),
1135+
how: "yes",
1136+
sticky: "true",
1137+
}),
1138+
});
1139+
}
1140+
}
1141+
1142+
if (redditUrl) {
1143+
const extractedId =
1144+
thingId ||
1145+
redditUrl.match(/comments\/([a-z0-9]+)/i)?.[1] ||
1146+
data._id;
1147+
1148+
await client.createOrReplace({
1149+
_id: syndicationDocId,
1150+
_type: "syndication",
1151+
target: { _type: "reference", _ref: data._id },
1152+
platform: "reddit",
1153+
status: "published",
1154+
canonicalUrl,
1155+
externalUrl: redditUrl,
1156+
externalId: String(extractedId),
1157+
syndicatedAt: new Date().toISOString(),
1158+
error: undefined,
1159+
});
1160+
1161+
if (data.reddit !== redditUrl) {
1162+
parentPatch.reddit = redditUrl;
1163+
}
1164+
1165+
console.log(
1166+
`[Syndicate Content] Successfully published to Reddit (r/CodingCatDev): ${redditUrl}`,
1167+
);
1168+
}
10161169
}
10171170
} catch (publishErr) {
10181171
const errMsg =

‎scripts/run-syndicate-post.mjs‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,146 @@ async function run() {
823823
}
824824
}
825825

826+
// --- REDDIT (r/CodingCatDev) SYNDICATION ---
827+
const redditRefreshToken = process.env.REDDIT_REFRESH_TOKEN;
828+
if (redditRefreshToken) {
829+
console.log("\n--- Syndicating to Reddit (r/CodingCatDev) ---");
830+
const syndicationDocId = `syndication.${doc._id}.reddit`;
831+
try {
832+
const existingSyndication = await client.getDocument(syndicationDocId).catch(() => null);
833+
let redditUrl =
834+
(existingSyndication?.externalUrl &&
835+
!String(existingSyndication.externalUrl).includes("preview-simulated")
836+
? existingSyndication.externalUrl
837+
: null) ||
838+
doc.reddit ||
839+
(doc._id === "post-php2shell-cve-2026-109618"
840+
? "https://www.reddit.com/r/CodingCatDev/comments/1xf2yhz/php2shell_cve2026109618_how_ai_found_a_decadeold/"
841+
: doc._id === "05eda048-efff-4e0f-8b76-83a2f482fa13"
842+
? "https://www.reddit.com/r/CodingCatDev/comments/1xf2yg5/sanity_knowledge_bases_why_we_replaced_our_custom/"
843+
: null);
844+
845+
let thingId = existingSyndication?.externalId || null;
846+
847+
if (!redditUrl) {
848+
const basicAuth = Buffer.from("TWTsqXa53CexlrYGBWaesQ:").toString("base64");
849+
const refreshRes = await fetch("https://www.reddit.com/api/v1/access_token", {
850+
method: "POST",
851+
headers: {
852+
Authorization: `Basic ${basicAuth}`,
853+
"Content-Type": "application/x-www-form-urlencoded",
854+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
855+
},
856+
body: new URLSearchParams({
857+
grant_type: "refresh_token",
858+
refresh_token: redditRefreshToken,
859+
}),
860+
});
861+
const { access_token: redditAccessToken } = await refreshRes.json();
862+
if (!redditAccessToken) {
863+
throw new Error("Could not refresh Reddit OAuth access_token");
864+
}
865+
866+
const targetLinkUrl = topYouTubeWatchUrl || canonicalUrl;
867+
const prefix = doc._type === "podcast" ? "[Podcast] " : "";
868+
const postTitle = `${prefix}${doc.title || "Untitled"}`.slice(0, 300);
869+
870+
const subRes = await fetch("https://oauth.reddit.com/api/submit", {
871+
method: "POST",
872+
headers: {
873+
Authorization: `Bearer ${redditAccessToken}`,
874+
"Content-Type": "application/x-www-form-urlencoded",
875+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
876+
},
877+
body: new URLSearchParams({
878+
api_type: "json",
879+
sr: "CodingCatDev",
880+
kind: "link",
881+
title: postTitle,
882+
url: targetLinkUrl,
883+
resubmit: "true",
884+
}),
885+
});
886+
const subJson = await subRes.json();
887+
const subData = subJson?.json?.data;
888+
if (!subData?.url) {
889+
throw new Error(`Reddit api/submit error: ${JSON.stringify(subJson?.json?.errors || subJson)}`);
890+
}
891+
892+
redditUrl = subData.url;
893+
thingId = subData.name || subData.id;
894+
895+
const commentText = [
896+
doc.excerpt ? `> ${doc.excerpt.trim()}` : "",
897+
"",
898+
`📖 **Read the full guide, code snippets & transcript on CodingCat.dev:** [${canonicalUrl}](${canonicalUrl})`,
899+
topYouTubeWatchUrl
900+
? `📺 **Watch on YouTube & Subscribe:** [${topYouTubeWatchUrl}](${topYouTubeWatchUrl}${topYouTubeWatchUrl.includes("?") ? "&" : "?"}sub_confirmation=1)`
901+
: `📺 **Subscribe to CodingCat.dev on YouTube:** [https://www.youtube.com/@CodingCatDev](https://www.youtube.com/@CodingCatDev?sub_confirmation=1)`,
902+
]
903+
.filter(Boolean)
904+
.join("\n");
905+
906+
const comRes = await fetch("https://oauth.reddit.com/api/comment", {
907+
method: "POST",
908+
headers: {
909+
Authorization: `Bearer ${redditAccessToken}`,
910+
"Content-Type": "application/x-www-form-urlencoded",
911+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
912+
},
913+
body: new URLSearchParams({
914+
api_type: "json",
915+
thing_id: thingId,
916+
text: commentText,
917+
}),
918+
});
919+
const comJson = await comRes.json();
920+
const comName = comJson?.json?.data?.things?.[0]?.data?.name;
921+
if (comName) {
922+
await fetch("https://oauth.reddit.com/api/distinguish", {
923+
method: "POST",
924+
headers: {
925+
Authorization: `Bearer ${redditAccessToken}`,
926+
"Content-Type": "application/x-www-form-urlencoded",
927+
"User-Agent": "codingcat-sync/0.1.0 by u/ajonp",
928+
},
929+
body: new URLSearchParams({
930+
api_type: "json",
931+
id: comName,
932+
how: "yes",
933+
sticky: "true",
934+
}),
935+
});
936+
}
937+
}
938+
939+
if (redditUrl) {
940+
const extractedId =
941+
thingId ||
942+
redditUrl.match(/comments\/([a-z0-9]+)/i)?.[1] ||
943+
doc._id;
944+
console.log(`✓ Reddit (r/CodingCatDev) published: ${redditUrl}`);
945+
946+
await client.createOrReplace({
947+
_id: syndicationDocId,
948+
_type: "syndication",
949+
target: { _type: "reference", _ref: doc._id },
950+
platform: "reddit",
951+
status: "published",
952+
canonicalUrl,
953+
externalUrl: redditUrl,
954+
externalId: String(extractedId),
955+
syndicatedAt: new Date().toISOString(),
956+
});
957+
958+
await client.patch(doc._id).set({ reddit: redditUrl }).commit();
959+
console.log(`✓ Updated Sanity doc ${doc._id} reddit URL`);
960+
}
961+
} catch (err) {
962+
console.error("✗ Reddit (r/CodingCatDev) syndication error:", err);
963+
}
964+
}
965+
826966
// --- LINK SYNDICATION REFERENCES ON PARENT ---
827967
try {
828968
const platforms = [

‎tools/reddit-devvit/devvit.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
name: codingcat-syndicator
1+
name: codingcat-sync
22
version: 0.1.0

‎tools/reddit-devvit/src/main.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Devvit.addSettings([
2626
name: "sanityWriteToken",
2727
label: "Sanity API Write Token (optional: writes Reddit URL back to Sanity)",
2828
isSecret: true,
29-
scope: SettingScope.Installation,
29+
scope: SettingScope.App,
3030
},
3131
]);
3232

0 commit comments

Comments
 (0)