Skip to content

Commit f6002f9

Browse files
authored
Merge pull request #792 from CodingCatDev/dev
feat(syndication): create Google Task / Calendar reminder when Hashnode or Medium needs manual cross-posting
2 parents 948edc9 + 197ae77 commit f6002f9

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ jobs:
5050
HASHNODE_ACCESS_TOKEN: ${{ secrets.PRIVATE_HASHNODE }}
5151
AWS_BUILDER_SESSION_TOKEN: ${{ secrets.AWS_BUILDER_SESSION_TOKEN }}
5252
DAILY_DEV_TOKEN: ${{ secrets.DAILY_DEV_TOKEN }}
53+
GOOGLE_WORKSPACE_REFRESH_TOKEN: ${{ secrets.GOOGLE_WORKSPACE_REFRESH_TOKEN }}
54+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
55+
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,74 @@ export const handler = documentEventHandler<ContentDocument>(
751751
publishErr instanceof Error ? publishErr.message : String(publishErr);
752752
console.error(`[Syndicate Content] Error publishing to ${platform}:`, errMsg);
753753

754+
if (platform === "hashnode") {
755+
const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN;
756+
const clientId = process.env.GOOGLE_CLIENT_ID;
757+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
758+
if (refreshToken && clientId && clientSecret) {
759+
try {
760+
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
761+
method: "POST",
762+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
763+
body: new URLSearchParams({
764+
client_id: clientId,
765+
client_secret: clientSecret,
766+
refresh_token: refreshToken,
767+
grant_type: "refresh_token",
768+
}),
769+
});
770+
const tokenData = (await tokenRes.json()) as { access_token?: string };
771+
if (tokenData.access_token) {
772+
const taskTitle = `Cross-post to Hashnode & Medium: ${data.title || "Untitled"}`;
773+
const taskNotes = [
774+
`Canonical URL: ${canonicalUrl}`,
775+
``,
776+
`1. Hashnode (Free UI Editor): https://hashnode.com/draft`,
777+
`2. Medium (1-Click Import): https://medium.com/p/import`,
778+
`3. daily.dev Squad: https://daily.dev/squads/codingcatdev`,
779+
].join("\n");
780+
const tasksRes = await fetch(
781+
"https://tasks.googleapis.com/tasks/v1/lists/@default/tasks",
782+
{
783+
method: "POST",
784+
headers: {
785+
Authorization: `Bearer ${tokenData.access_token}`,
786+
"Content-Type": "application/json",
787+
},
788+
body: JSON.stringify({
789+
title: taskTitle,
790+
notes: taskNotes,
791+
due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
792+
}),
793+
},
794+
);
795+
if (!tasksRes.ok) {
796+
const startDate = new Date(Date.now() + 60 * 60 * 1000);
797+
const endDate = new Date(startDate.getTime() + 30 * 60 * 1000);
798+
await fetch(
799+
"https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events",
800+
{
801+
method: "POST",
802+
headers: {
803+
Authorization: `Bearer ${tokenData.access_token}`,
804+
"Content-Type": "application/json",
805+
},
806+
body: JSON.stringify({
807+
summary: `📋 [Task] ${taskTitle}`,
808+
description: taskNotes,
809+
start: { dateTime: startDate.toISOString() },
810+
end: { dateTime: endDate.toISOString() },
811+
}),
812+
},
813+
);
814+
}
815+
}
816+
} catch (taskErr) {
817+
console.warn("[Syndicate Content] Failed creating Google Task:", taskErr);
818+
}
819+
}
820+
}
821+
754822
await client.createOrReplace({
755823
_id: syndicationDocId,
756824
_type: "syndication",

‎scripts/run-syndicate-post.mjs‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,102 @@ const client = createClient({
3030
useCdn: false,
3131
});
3232

33+
async function createGoogleSyndicationTask({ title, canonicalUrl, tags = [] }) {
34+
const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN;
35+
const clientId = process.env.GOOGLE_CLIENT_ID;
36+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
37+
38+
if (!refreshToken || !clientId || !clientSecret) {
39+
console.log("Google Workspace credentials not set; skipping Google Task creation.");
40+
return;
41+
}
42+
43+
try {
44+
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
45+
method: "POST",
46+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
47+
body: new URLSearchParams({
48+
client_id: clientId,
49+
client_secret: clientSecret,
50+
refresh_token: refreshToken,
51+
grant_type: "refresh_token",
52+
}),
53+
});
54+
const tokenData = await tokenRes.json();
55+
const accessToken = tokenData.access_token;
56+
if (!accessToken) {
57+
console.warn("Could not obtain Google access_token for Task creation.");
58+
return;
59+
}
60+
61+
const taskTitle = `Cross-post to Hashnode & Medium: ${title}`;
62+
const taskNotes = [
63+
`Canonical URL: ${canonicalUrl}`,
64+
`Tags: ${tags.join(", ")}`,
65+
``,
66+
`1. Hashnode (Free UI Editor):`,
67+
` - Open: https://hashnode.com/draft`,
68+
` - Set Original Article URL (Canonical): ${canonicalUrl}`,
69+
``,
70+
`2. Medium (1-Click Import):`,
71+
` - Open: https://medium.com/p/import`,
72+
` - Paste: ${canonicalUrl} (automatically sets canonical tag)`,
73+
``,
74+
`3. daily.dev Squad:`,
75+
` - Open: https://daily.dev/squads/codingcatdev -> Click "Share a link" -> Paste ${canonicalUrl}`,
76+
].join("\n");
77+
78+
// 1. Try Google Tasks API first
79+
const tasksRes = await fetch("https://tasks.googleapis.com/tasks/v1/lists/@default/tasks", {
80+
method: "POST",
81+
headers: {
82+
Authorization: `Bearer ${accessToken}`,
83+
"Content-Type": "application/json",
84+
},
85+
body: JSON.stringify({
86+
title: taskTitle,
87+
notes: taskNotes,
88+
due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
89+
}),
90+
});
91+
92+
if (tasksRes.ok) {
93+
const createdTask = await tasksRes.json();
94+
console.log(`✓ Created Google Task: "${taskTitle}" (ID: ${createdTask.id})`);
95+
return;
96+
}
97+
98+
// 2. Fallback to Google Calendar To-Do Event on alex.patterson@gmail.com if Tasks scope is not yet authorized
99+
const startDate = new Date(Date.now() + 60 * 60 * 1000);
100+
const endDate = new Date(startDate.getTime() + 30 * 60 * 1000);
101+
const calRes = await fetch(
102+
"https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events",
103+
{
104+
method: "POST",
105+
headers: {
106+
Authorization: `Bearer ${accessToken}`,
107+
"Content-Type": "application/json",
108+
},
109+
body: JSON.stringify({
110+
summary: `📋 [Task] ${taskTitle}`,
111+
description: taskNotes,
112+
start: { dateTime: startDate.toISOString() },
113+
end: { dateTime: endDate.toISOString() },
114+
}),
115+
}
116+
);
117+
118+
if (calRes.ok) {
119+
const createdEvent = await calRes.json();
120+
console.log(`✓ Created Google Calendar Task Reminder: "${taskTitle}" (${createdEvent.htmlLink})`);
121+
} else {
122+
console.warn("Could not create Google Calendar fallback reminder:", await calRes.text());
123+
}
124+
} catch (err) {
125+
console.warn("Error creating Google syndication task:", err);
126+
}
127+
}
128+
33129
async function run() {
34130
const doc = await client.getDocument(docId);
35131
if (!doc) {
@@ -303,6 +399,11 @@ async function run() {
303399
console.log(`✓ Updated Sanity doc ${doc._id} hashnode slug`);
304400
} else {
305401
console.error("✗ Hashnode publication failed:", publishData?.errors);
402+
await createGoogleSyndicationTask({
403+
title: doc.title || "Untitled",
404+
canonicalUrl,
405+
tags: hashnodeTags.map((t) => t.slug),
406+
});
306407
}
307408
} catch (err) {
308409
console.error("✗ Hashnode request error:", err);

0 commit comments

Comments
 (0)