diff --git a/src/pages/api/buffer_posts.ts b/src/pages/api/buffer_posts.ts new file mode 100644 index 000000000..6e1ff6d2a --- /dev/null +++ b/src/pages/api/buffer_posts.ts @@ -0,0 +1,170 @@ +import { getCollection, getEntry } from "astro:content"; +import type { APIRoute } from "astro"; + +// Get @username from Twitter URL +function getTwitterUsername(url: string): string | undefined { + if (!url) return undefined; + const username = url.split("/").pop(); + return (username ?? url).startsWith("@") ? username : `@${username}`; +} + +// Get @username from Bluesky URL +function getBlueskyUsername(url: string): string | undefined { + if (!url) return undefined; + const username = url.split("/").pop()?.replace(/^@/, ""); + return username ? `@${username}` : undefined; +} + +// Get Bluesky profile link from username +function getBlueskyProfileLink(username: string): string { + // Remove any leading @ if present + const cleanUsername = username.replace(/^@/, ""); + return `https://bsky.app/profile/${cleanUsername}`; +} + +// Get @username@instance.tld from Mastodon URL +function getMastodonUsername(url: string): string | undefined { + if (!url) return undefined; + const match = url.match(/https?:\/\/([^\/]+)\/@([^\/]+)(\/|\?|$)/); + return match ? `@${match[2]}@${match[1]}` : undefined; +} + +function getLinkedInUsernameHandler(url: string): string | undefined { + if (!url) return undefined; + const match = url.match(/https?:\/\/([^\/]+)\/in\/([^\/]+)(\/|\?|$)/); + if (match) { + try { + return `https://www.linkedin.com/in/${decodeURIComponent(match[2])}`; + } catch { + return `https://www.linkedin.com/in/${match[2]}`; + } + } + return undefined; +} + +export const GET: APIRoute = async ({ params, request }) => { + const limit = Infinity; + const speakers = await getCollection("speakers"); + const exclude = [ + "sebastian-ramirez", + "savannah-ostrowski", + "nerea-luis", + "petr-baudis", + "brett-cannon", + ]; + const records: any[] = []; + + const charLimits: Record = { + instagram: 2200, + x: 280, + linkedin: 3000, + bsky: 300, + fosstodon: 500, + }; + + // Tailor message templates for each platform using appropriate handle formats + const message_template = { + instagram: ({ name, talkTitle, talkUrl }) => + `Join ${name} at EuroPython for "${talkTitle}".`, + + x: ({ name, handle, talkTitle, talkUrl }) => + handle + ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". Talk: ${talkUrl}` + : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`, + + linkedin: ({ name, handle, talkTitle, talkUrl }) => + `Join ${name} at EuroPython for "${talkTitle}".`, + + bsky: ({ name, handle, talkTitle, talkUrl }) => + handle + ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". Talk: ${talkUrl}` + : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`, + + fosstodon: ({ name, handle, talkTitle, talkUrl }) => + handle + ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". talk: ${talkUrl}` + : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`, + }; + + const trimToLimit = (text: string, limit: number) => + text.length <= limit ? text : text.slice(0, limit - 1) + "…"; + + for (const speaker of speakers) { + if (records.length >= limit) break; + if (exclude.includes(speaker.id)) continue; + + const { + name, + twitter_url, + linkedin_url, + bluesky_url, + mastodon_url, + submissions, + } = speaker.data; + + const sessions = await Promise.all( + submissions.map((session) => getEntry("sessions", session.id)) + ); + + const validSessions = sessions.filter( + (session) => session && session.data.title + ); + + if (validSessions.length === 0) continue; + + const talkTitle = validSessions[0]?.data.title || "an exciting topic"; + const talkCode = validSessions[0]?.data.code; + const talkUrl = `https://ep2025.europython.eu/${talkCode}`; + const speakerImage = `https://ep2025.europython.eu/media/social-${speaker.id}.png`; + const fallbackUrl = `https://ep2025.europython.eu/speaker/${speaker.id}`; + + // Extract handles for each platform + const handles = { + x: getTwitterUsername(twitter_url || ""), + linkedin: getLinkedInUsernameHandler(linkedin_url || ""), + bsky: getBlueskyUsername(bluesky_url || ""), + fosstodon: getMastodonUsername(mastodon_url || ""), + }; + + // Generate appropriate messages for each platform + const generateMessage = (platform: keyof typeof message_template) => { + const templateFn = message_template[platform]; + const handle = + platform === "instagram" + ? undefined + : handles[platform as keyof typeof handles]; + + const full = templateFn({ + name, + handle, + talkTitle, + talkUrl: platform === "instagram" ? fallbackUrl : talkUrl, + }); + + const limit = charLimits[platform]; + return trimToLimit(full, limit); + }; + + const record = { + name, + image: speakerImage, + handles: handles, + channel: { + instagram: generateMessage("instagram"), + x: generateMessage("x"), + linkedin: generateMessage("linkedin"), + bsky: generateMessage("bsky"), + fosstodon: generateMessage("fosstodon"), + }, + }; + + records.push(record); + } + + return new Response(JSON.stringify(records, null, 2), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); +}; diff --git a/src/pages/media/social_media.csv.ts b/src/pages/media/social_media.csv.ts new file mode 100644 index 000000000..553668b6d --- /dev/null +++ b/src/pages/media/social_media.csv.ts @@ -0,0 +1,78 @@ +import { getCollection, getEntry } from "astro:content"; +export async function GET({ params, request }) { + const speakers = await getCollection("speakers"); + + const header = [ + "Talk Title", + "Speaker Name", + "Speaker Photo URL", + "Primary Social ULR", + "X URL", + "LinkedIn URL", + "Bluesky URL", + "Mastodon URL", + ]; + + const exclude = [ + "sebastian-ramirez", + "savannah-ostrowski", + "nerea-luis", + "petr-baudis", + "brett-cannon", + ]; + + const rows: string[][] = []; + + for (const speaker of speakers) { + if (exclude.includes(speaker.id)) continue; + + const { + name, + twitter_url, + linkedin_url, + bluesky_url, + mastodon_url, + submissions, + } = speaker.data; + + const sessions = await Promise.all( + submissions.map((session) => getEntry("sessions", session.id)) + ); + + for (const session of sessions) { + if (session) { + const speaker_page = `https://ep2025.europython.eu/speaker/${speaker.id}`; + rows.push([ + session.data.title || "", + name, + `https://ep2025-buffer.ep-preview.click/media/social-${speaker.id}.png`, + twitter_url || linkedin_url || mastodon_url || speaker_page, + twitter_url ?? speaker_page, + linkedin_url ?? speaker_page, + bluesky_url ?? speaker_page, + mastodon_url ?? speaker_page, + ]); + } + } + } + + const csvLines = [header, ...rows] + .map((row) => + row + .map((field) => + field.includes('"') || field.includes(",") || field.includes("\n") + ? `"${field.replace(/"/g, '""')}"` + : field + ) + .join(",") + ) + .join("\r\n"); + + return new Response(csvLines, { + status: 200, + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": 'attachment; filename="social_media.csv"', + }, + }); +}