Add Listmonk integration for newsletter subscriber sync
- New registrations with newsletter=yes are automatically added to WORLDPLAY list - Stores registration metadata (role, interests, location) as subscriber attributes - Handles existing subscribers by adding them to the list - Connected to Listmonk via internal Docker network Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
61b409dd89
commit
f0641cc04a
|
|
@ -13,6 +13,11 @@ services:
|
||||||
- RESEND_API_KEY=${RESEND_API_KEY}
|
- RESEND_API_KEY=${RESEND_API_KEY}
|
||||||
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}
|
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}
|
||||||
- GOOGLE_CREDENTIALS=${GOOGLE_CREDENTIALS}
|
- GOOGLE_CREDENTIALS=${GOOGLE_CREDENTIALS}
|
||||||
|
# Listmonk newsletter integration
|
||||||
|
- LISTMONK_URL=http://listmonk:9000
|
||||||
|
- LISTMONK_USER=worldplay-api
|
||||||
|
- LISTMONK_PASS=worldplay-api-2026
|
||||||
|
- LISTMONK_LIST_ID=20
|
||||||
volumes:
|
volumes:
|
||||||
- worldplay-data:/app/data
|
- worldplay-data:/app/data
|
||||||
labels:
|
labels:
|
||||||
|
|
@ -25,6 +30,7 @@ services:
|
||||||
- "traefik.http.services.worldplay.loadbalancer.server.port=3000"
|
- "traefik.http.services.worldplay.loadbalancer.server.port=3000"
|
||||||
networks:
|
networks:
|
||||||
- traefik-public
|
- traefik-public
|
||||||
|
- listmonk-internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
@ -39,3 +45,6 @@ volumes:
|
||||||
networks:
|
networks:
|
||||||
traefik-public:
|
traefik-public:
|
||||||
external: true
|
external: true
|
||||||
|
listmonk-internal:
|
||||||
|
external: true
|
||||||
|
name: listmonk_listmonk-internal
|
||||||
|
|
|
||||||
88
server.js
88
server.js
|
|
@ -12,6 +12,12 @@ const REGISTRATIONS_FILE = path.join(DATA_DIR, 'registrations.json');
|
||||||
// Initialize Resend for emails
|
// Initialize Resend for emails
|
||||||
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null;
|
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null;
|
||||||
|
|
||||||
|
// Listmonk configuration for newsletter management
|
||||||
|
const LISTMONK_URL = process.env.LISTMONK_URL || 'http://listmonk:9000';
|
||||||
|
const LISTMONK_USER = process.env.LISTMONK_USER || 'worldplay-api';
|
||||||
|
const LISTMONK_PASS = process.env.LISTMONK_PASS || 'worldplay-api-2026';
|
||||||
|
const LISTMONK_LIST_ID = parseInt(process.env.LISTMONK_LIST_ID) || 20; // WORLDPLAY list
|
||||||
|
|
||||||
// Google Sheets configuration
|
// Google Sheets configuration
|
||||||
const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID;
|
const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID;
|
||||||
const GOOGLE_CREDENTIALS = process.env.GOOGLE_CREDENTIALS ? JSON.parse(process.env.GOOGLE_CREDENTIALS) : null;
|
const GOOGLE_CREDENTIALS = process.env.GOOGLE_CREDENTIALS ? JSON.parse(process.env.GOOGLE_CREDENTIALS) : null;
|
||||||
|
|
@ -242,6 +248,84 @@ async function sendConfirmationEmail(registration) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add subscriber to Listmonk for newsletter management
|
||||||
|
async function addToListmonk(registration) {
|
||||||
|
if (!registration.newsletter) {
|
||||||
|
console.log('Newsletter not opted in, skipping Listmonk...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = Buffer.from(`${LISTMONK_USER}:${LISTMONK_PASS}`).toString('base64');
|
||||||
|
|
||||||
|
// First, create or update the subscriber
|
||||||
|
const subscriberResponse = await fetch(`${LISTMONK_URL}/api/subscribers`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: registration.email,
|
||||||
|
name: `${registration.firstName} ${registration.lastName}`,
|
||||||
|
status: 'enabled',
|
||||||
|
lists: [LISTMONK_LIST_ID],
|
||||||
|
attribs: {
|
||||||
|
firstName: registration.firstName,
|
||||||
|
lastName: registration.lastName,
|
||||||
|
location: registration.location || '',
|
||||||
|
role: registration.role || '',
|
||||||
|
interests: registration.interests || [],
|
||||||
|
contribute: registration.contribute || [],
|
||||||
|
registrationId: registration.id,
|
||||||
|
registeredAt: registration.registeredAt
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscriberResponse.ok) {
|
||||||
|
const data = await subscriberResponse.json();
|
||||||
|
console.log(`Added to Listmonk WORLDPLAY list: ${registration.email} (ID: ${data.data?.id})`);
|
||||||
|
} else if (subscriberResponse.status === 409) {
|
||||||
|
// Subscriber already exists, add to list
|
||||||
|
console.log(`Subscriber exists in Listmonk, updating list membership: ${registration.email}`);
|
||||||
|
|
||||||
|
// Get existing subscriber
|
||||||
|
const searchResponse = await fetch(`${LISTMONK_URL}/api/subscribers?query=email='${registration.email}'`, {
|
||||||
|
headers: { 'Authorization': `Basic ${auth}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchResponse.ok) {
|
||||||
|
const searchData = await searchResponse.json();
|
||||||
|
if (searchData.data?.results?.length > 0) {
|
||||||
|
const subscriberId = searchData.data.results[0].id;
|
||||||
|
|
||||||
|
// Add to WORLDPLAY list
|
||||||
|
await fetch(`${LISTMONK_URL}/api/subscribers/lists`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: [subscriberId],
|
||||||
|
action: 'add',
|
||||||
|
target_list_ids: [LISTMONK_LIST_ID],
|
||||||
|
status: 'confirmed'
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
console.log(`Added existing subscriber to WORLDPLAY list: ${registration.email}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorText = await subscriberResponse.text();
|
||||||
|
console.error(`Listmonk API error (${subscriberResponse.status}): ${errorText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding to Listmonk:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Registration endpoint
|
// Registration endpoint
|
||||||
app.post('/api/register', async (req, res) => {
|
app.post('/api/register', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -292,6 +376,9 @@ app.post('/api/register', async (req, res) => {
|
||||||
// Send confirmation email (async, don't block response)
|
// Send confirmation email (async, don't block response)
|
||||||
sendConfirmationEmail(registration).catch(err => console.error('Email error:', err));
|
sendConfirmationEmail(registration).catch(err => console.error('Email error:', err));
|
||||||
|
|
||||||
|
// Add to Listmonk for newsletter management (async, don't block response)
|
||||||
|
addToListmonk(registration).catch(err => console.error('Listmonk error:', err));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Registration successful',
|
message: 'Registration successful',
|
||||||
|
|
@ -425,5 +512,6 @@ ensureDataDir().then(() => {
|
||||||
console.log(`Admin token: ${process.env.ADMIN_TOKEN || 'worldplay-admin-2026'}`);
|
console.log(`Admin token: ${process.env.ADMIN_TOKEN || 'worldplay-admin-2026'}`);
|
||||||
console.log(`Google Sheets: ${sheets ? 'enabled' : 'disabled'}`);
|
console.log(`Google Sheets: ${sheets ? 'enabled' : 'disabled'}`);
|
||||||
console.log(`Email notifications: ${resend ? 'enabled' : 'disabled'}`);
|
console.log(`Email notifications: ${resend ? 'enabled' : 'disabled'}`);
|
||||||
|
console.log(`Listmonk newsletter sync: enabled (list ID: ${LISTMONK_LIST_ID})`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue