Skip to content
Merged
170 changes: 170 additions & 0 deletions src/pages/api/buffer_posts.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = {
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",
},
});
};
78 changes: 78 additions & 0 deletions src/pages/media/social_media.csv.ts
Original file line number Diff line number Diff line change
@@ -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"',
},
});
}