Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/scripts/mailchimp/htmlContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Topic: <a href="${ link }" style="color:#007c89;font-weight:normal;text-decorati
<td valign="top" class="mcnTextContent" style="padding-top: 0;padding-right: 18px;padding-bottom: 9px;padding-left: 18px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;word-break: break-word;color: #202020;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: left;">

Cheers,<br>
AsyncAPI Initiative
<span style="color:#696969">AsyncAPI Initiative</span>
</td>
</tr>
</tbody></table>
Expand Down
8 changes: 0 additions & 8 deletions config/mailchimp-config.json

This file was deleted.

171 changes: 137 additions & 34 deletions netlify/functions/newsletter_subscription.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,153 @@
import mailchimp from '@mailchimp/mailchimp_marketing';
import type { Handler, HandlerEvent } from '@netlify/functions';
import md5 from 'md5';

import config from '../../config/mailchimp-config.json';
const KIT_BASE = 'https://api.kit.com/v4';
const REQUEST_TIMEOUT_MS = 15_000;

const INTEREST_TO_ENV = {
Newsletter: 'KIT_NEWSLETTER_TAG_ID',
Meetings: 'KIT_MEETINGS_TAG_ID',
'TSC Voting': 'KIT_TSC_TAG_ID'
} as const;

type ValidInterest = keyof typeof INTEREST_TO_ENV;

function isValidInterest(s: string): s is ValidInterest {
return Object.hasOwn(INTEREST_TO_ENV, s);
}

function parseTagId(raw: string | undefined): number | null {
if (raw == null || raw.trim() === '') {
return null;
}
const n = Number(raw.trim());
if (!Number.isInteger(n) || n <= 0) {
return null;
}
return n;
}

function isAbortError(err: unknown): boolean {
return err instanceof Error && err.name === 'AbortError';
}

async function kitFetch(url: string, init: RequestInit): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}

export const handler: Handler = async (event: HandlerEvent) => {
if (event.httpMethod === 'POST') {
const { listId } = config;
const { email, name, interest } = JSON.parse(event.body || '');

const subscriberHash = md5(email.toLowerCase());

try {
mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_API_KEY,
server: 'us12'
});

const response = await mailchimp.lists.setListMember(listId,
subscriberHash,
{
email_address: email,
merge_fields: {
FNAME: name
},
status: 'subscribed',
interests: {
[config.interests[interest]]: true
}
});
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ message: 'The specified HTTP method is not allowed.' })
};
}

let body: unknown;
try {
body = JSON.parse(event.body ?? '{}');
} catch {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Invalid request body.' })
};
}

if (body === null || typeof body !== 'object' || Array.isArray(body)) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Invalid subscription request.' })
};
}

const { email, name, interest } = body as Partial<Record<'email' | 'name' | 'interest', unknown>>;

if (typeof email !== 'string' || typeof interest !== 'string' || !isValidInterest(interest)) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'Invalid subscription request.' })
};
}

if (!process.env.KIT_API_KEY) {
return {
statusCode: 503,
body: JSON.stringify({ message: 'Subscription is temporarily unavailable. Please try again later.' })
};
}

const envVarName = INTEREST_TO_ENV[interest];
const tagId = parseTagId(process.env[envVarName]);

if (tagId == null) {
return {
statusCode: 503,
body: JSON.stringify({ message: 'Subscription is temporarily unavailable. Please try again later.' })
};
}

const firstName = typeof name === 'string' ? name : '';

const headers = {
'X-Kit-Api-Key': process.env.KIT_API_KEY,
'Content-Type': 'application/json'
};

try {
const subRes = await kitFetch(`${KIT_BASE}/subscribers`, {
method: 'POST',
headers,
body: JSON.stringify({ email_address: email, first_name: firstName, state: 'active' })
});

if (!subRes.ok) {
await subRes.text().catch(() => undefined);
return {
statusCode: 200,
body: JSON.stringify(response)
statusCode: 502,
body: JSON.stringify({
message: 'Subscription could not be completed. Please try again later.'
})
};
} catch (err) {
}

const tagRes = await kitFetch(`${KIT_BASE}/tags/${tagId}/subscribers`, {
method: 'POST',
headers,
body: JSON.stringify({ email_address: email })
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!tagRes.ok) {
await tagRes.text().catch(() => undefined);
return {
statusCode: 502,
body: JSON.stringify({
message: 'Subscription could not be completed. Please try again later.'
})
};
}

return {
statusCode: 200,
body: JSON.stringify({ message: 'Subscribed successfully.' })
};
} catch (err) {
if (isAbortError(err)) {
return {
statusCode: err.status,
body: JSON.stringify(err)
statusCode: 504,
body: JSON.stringify({
message: 'Subscription service timed out. Please try again later.'
})
};
}
} else {
return {
statusCode: 500,
body: JSON.stringify({
message: 'The specified HTTP method is not allowed.'
message: 'An unexpected error occurred. Please try again later.'
})
};
}
Expand Down
Loading
Loading