diff --git a/.github/workflows/syndicate-post.yml b/.github/workflows/syndicate-post.yml index 680efb64..d2ef408c 100644 --- a/.github/workflows/syndicate-post.yml +++ b/.github/workflows/syndicate-post.yml @@ -50,3 +50,6 @@ jobs: HASHNODE_ACCESS_TOKEN: ${{ secrets.PRIVATE_HASHNODE }} AWS_BUILDER_SESSION_TOKEN: ${{ secrets.AWS_BUILDER_SESSION_TOKEN }} DAILY_DEV_TOKEN: ${{ secrets.DAILY_DEV_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 0f3eb1da..8f805e6f 100644 --- a/apps/sanity/functions/syndicate-content/index.ts +++ b/apps/sanity/functions/syndicate-content/index.ts @@ -751,6 +751,74 @@ export const handler = documentEventHandler( publishErr instanceof Error ? publishErr.message : String(publishErr); console.error(`[Syndicate Content] Error publishing to ${platform}:`, errMsg); + if (platform === "hashnode") { + const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN; + const clientId = process.env.GOOGLE_CLIENT_ID; + const clientSecret = process.env.GOOGLE_CLIENT_SECRET; + if (refreshToken && clientId && clientSecret) { + try { + const tokenRes = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: "refresh_token", + }), + }); + const tokenData = (await tokenRes.json()) as { access_token?: string }; + if (tokenData.access_token) { + const taskTitle = `Cross-post to Hashnode & Medium: ${data.title || "Untitled"}`; + const taskNotes = [ + `Canonical URL: ${canonicalUrl}`, + ``, + `1. Hashnode (Free UI Editor): https://hashnode.com/draft`, + `2. Medium (1-Click Import): https://medium.com/p/import`, + `3. daily.dev Squad: https://daily.dev/squads/codingcatdev`, + ].join("\n"); + const tasksRes = await fetch( + "https://tasks.googleapis.com/tasks/v1/lists/@default/tasks", + { + method: "POST", + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: taskTitle, + notes: taskNotes, + due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }), + }, + ); + if (!tasksRes.ok) { + const startDate = new Date(Date.now() + 60 * 60 * 1000); + const endDate = new Date(startDate.getTime() + 30 * 60 * 1000); + await fetch( + "https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events", + { + method: "POST", + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + summary: `📋 [Task] ${taskTitle}`, + description: taskNotes, + start: { dateTime: startDate.toISOString() }, + end: { dateTime: endDate.toISOString() }, + }), + }, + ); + } + } + } catch (taskErr) { + console.warn("[Syndicate Content] Failed creating Google Task:", taskErr); + } + } + } + await client.createOrReplace({ _id: syndicationDocId, _type: "syndication", diff --git a/scripts/run-syndicate-post.mjs b/scripts/run-syndicate-post.mjs index 25aa3492..fb1738ce 100644 --- a/scripts/run-syndicate-post.mjs +++ b/scripts/run-syndicate-post.mjs @@ -30,6 +30,102 @@ const client = createClient({ useCdn: false, }); +async function createGoogleSyndicationTask({ title, canonicalUrl, tags = [] }) { + const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN; + const clientId = process.env.GOOGLE_CLIENT_ID; + const clientSecret = process.env.GOOGLE_CLIENT_SECRET; + + if (!refreshToken || !clientId || !clientSecret) { + console.log("Google Workspace credentials not set; skipping Google Task creation."); + return; + } + + try { + const tokenRes = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: "refresh_token", + }), + }); + const tokenData = await tokenRes.json(); + const accessToken = tokenData.access_token; + if (!accessToken) { + console.warn("Could not obtain Google access_token for Task creation."); + return; + } + + const taskTitle = `Cross-post to Hashnode & Medium: ${title}`; + const taskNotes = [ + `Canonical URL: ${canonicalUrl}`, + `Tags: ${tags.join(", ")}`, + ``, + `1. Hashnode (Free UI Editor):`, + ` - Open: https://hashnode.com/draft`, + ` - Set Original Article URL (Canonical): ${canonicalUrl}`, + ``, + `2. Medium (1-Click Import):`, + ` - Open: https://medium.com/p/import`, + ` - Paste: ${canonicalUrl} (automatically sets canonical tag)`, + ``, + `3. daily.dev Squad:`, + ` - Open: https://daily.dev/squads/codingcatdev -> Click "Share a link" -> Paste ${canonicalUrl}`, + ].join("\n"); + + // 1. Try Google Tasks API first + const tasksRes = await fetch("https://tasks.googleapis.com/tasks/v1/lists/@default/tasks", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: taskTitle, + notes: taskNotes, + due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }), + }); + + if (tasksRes.ok) { + const createdTask = await tasksRes.json(); + console.log(`✓ Created Google Task: "${taskTitle}" (ID: ${createdTask.id})`); + return; + } + + // 2. Fallback to Google Calendar To-Do Event on alex.patterson@gmail.com if Tasks scope is not yet authorized + const startDate = new Date(Date.now() + 60 * 60 * 1000); + const endDate = new Date(startDate.getTime() + 30 * 60 * 1000); + const calRes = await fetch( + "https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events", + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + summary: `📋 [Task] ${taskTitle}`, + description: taskNotes, + start: { dateTime: startDate.toISOString() }, + end: { dateTime: endDate.toISOString() }, + }), + } + ); + + if (calRes.ok) { + const createdEvent = await calRes.json(); + console.log(`✓ Created Google Calendar Task Reminder: "${taskTitle}" (${createdEvent.htmlLink})`); + } else { + console.warn("Could not create Google Calendar fallback reminder:", await calRes.text()); + } + } catch (err) { + console.warn("Error creating Google syndication task:", err); + } +} + async function run() { const doc = await client.getDocument(docId); if (!doc) { @@ -303,6 +399,11 @@ async function run() { console.log(`✓ Updated Sanity doc ${doc._id} hashnode slug`); } else { console.error("✗ Hashnode publication failed:", publishData?.errors); + await createGoogleSyndicationTask({ + title: doc.title || "Untitled", + canonicalUrl, + tags: hashnodeTags.map((t) => t.slug), + }); } } catch (err) { console.error("✗ Hashnode request error:", err);