Switch Listmonk integration to direct PostgreSQL access
- Replaced HTTP API calls with direct database insertions - More reliable than API auth which requires session tokens in Listmonk v5+ - Added pg package for PostgreSQL connectivity - Handles both new subscribers and existing ones (updates attributes) - Merges WORLDPLAY attributes with any existing subscriber data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f0641cc04a
commit
c32070806a
|
|
@ -13,10 +13,12 @@ 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 newsletter integration (direct DB access)
|
||||||
- LISTMONK_URL=http://listmonk:9000
|
- LISTMONK_DB_HOST=listmonk-db
|
||||||
- LISTMONK_USER=worldplay-api
|
- LISTMONK_DB_PORT=5432
|
||||||
- LISTMONK_PASS=worldplay-api-2026
|
- LISTMONK_DB_NAME=listmonk
|
||||||
|
- LISTMONK_DB_USER=listmonk
|
||||||
|
- LISTMONK_DB_PASS=listmonk_secure_2025
|
||||||
- LISTMONK_LIST_ID=20
|
- LISTMONK_LIST_ID=20
|
||||||
volumes:
|
volumes:
|
||||||
- worldplay-data:/app/data
|
- worldplay-data:/app/data
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^144.0.0",
|
||||||
|
"pg": "^8.11.3",
|
||||||
"resend": "^4.0.0"
|
"resend": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
138
server.js
138
server.js
|
|
@ -3,6 +3,7 @@ const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { google } = require('googleapis');
|
const { google } = require('googleapis');
|
||||||
const { Resend } = require('resend');
|
const { Resend } = require('resend');
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
@ -12,11 +13,15 @@ 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
|
// Listmonk PostgreSQL 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
|
const LISTMONK_LIST_ID = parseInt(process.env.LISTMONK_LIST_ID) || 20; // WORLDPLAY list
|
||||||
|
const listmonkPool = process.env.LISTMONK_DB_HOST ? new Pool({
|
||||||
|
host: process.env.LISTMONK_DB_HOST || 'listmonk-db',
|
||||||
|
port: parseInt(process.env.LISTMONK_DB_PORT) || 5432,
|
||||||
|
database: process.env.LISTMONK_DB_NAME || 'listmonk',
|
||||||
|
user: process.env.LISTMONK_DB_USER || 'listmonk',
|
||||||
|
password: process.env.LISTMONK_DB_PASS || 'listmonk_secure_2025',
|
||||||
|
}) : null;
|
||||||
|
|
||||||
// Google Sheets configuration
|
// Google Sheets configuration
|
||||||
const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID;
|
const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID;
|
||||||
|
|
@ -248,81 +253,78 @@ async function sendConfirmationEmail(registration) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subscriber to Listmonk for newsletter management
|
// Add subscriber to Listmonk for newsletter management (direct DB access)
|
||||||
async function addToListmonk(registration) {
|
async function addToListmonk(registration) {
|
||||||
if (!registration.newsletter) {
|
if (!registration.newsletter) {
|
||||||
console.log('Newsletter not opted in, skipping Listmonk...');
|
console.log('Newsletter not opted in, skipping Listmonk...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!listmonkPool) {
|
||||||
|
console.log('Listmonk database not configured, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await listmonkPool.connect();
|
||||||
try {
|
try {
|
||||||
const auth = Buffer.from(`${LISTMONK_USER}:${LISTMONK_PASS}`).toString('base64');
|
const fullName = `${registration.firstName} ${registration.lastName}`;
|
||||||
|
const attribs = {
|
||||||
// First, create or update the subscriber
|
worldplay: {
|
||||||
const subscriberResponse = await fetch(`${LISTMONK_URL}/api/subscribers`, {
|
firstName: registration.firstName,
|
||||||
method: 'POST',
|
lastName: registration.lastName,
|
||||||
headers: {
|
location: registration.location || '',
|
||||||
'Authorization': `Basic ${auth}`,
|
role: registration.role || '',
|
||||||
'Content-Type': 'application/json',
|
interests: registration.interests || [],
|
||||||
},
|
contribute: registration.contribute || [],
|
||||||
body: JSON.stringify({
|
registrationId: registration.id,
|
||||||
email: registration.email,
|
registeredAt: registration.registeredAt
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if subscriber exists
|
||||||
|
const existingResult = await client.query(
|
||||||
|
'SELECT id, attribs FROM subscribers WHERE email = $1',
|
||||||
|
[registration.email]
|
||||||
|
);
|
||||||
|
|
||||||
|
let subscriberId;
|
||||||
|
|
||||||
|
if (existingResult.rows.length > 0) {
|
||||||
|
// Subscriber exists - update attributes and get ID
|
||||||
|
subscriberId = existingResult.rows[0].id;
|
||||||
|
const existingAttribs = existingResult.rows[0].attribs || {};
|
||||||
|
const mergedAttribs = { ...existingAttribs, ...attribs };
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
'UPDATE subscribers SET name = $1, attribs = $2, updated_at = NOW() WHERE id = $3',
|
||||||
|
[fullName, JSON.stringify(mergedAttribs), subscriberId]
|
||||||
|
);
|
||||||
|
console.log(`Updated existing Listmonk subscriber: ${registration.email} (ID: ${subscriberId})`);
|
||||||
} else {
|
} else {
|
||||||
const errorText = await subscriberResponse.text();
|
// Create new subscriber
|
||||||
console.error(`Listmonk API error (${subscriberResponse.status}): ${errorText}`);
|
const insertResult = await client.query(
|
||||||
|
`INSERT INTO subscribers (uuid, email, name, status, attribs, created_at, updated_at)
|
||||||
|
VALUES (gen_random_uuid(), $1, $2, 'enabled', $3, NOW(), NOW())
|
||||||
|
RETURNING id`,
|
||||||
|
[registration.email, fullName, JSON.stringify(attribs)]
|
||||||
|
);
|
||||||
|
subscriberId = insertResult.rows[0].id;
|
||||||
|
console.log(`Created new Listmonk subscriber: ${registration.email} (ID: ${subscriberId})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add to WORLDPLAY list if not already a member
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO subscriber_lists (subscriber_id, list_id, status, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, 'confirmed', NOW(), NOW())
|
||||||
|
ON CONFLICT (subscriber_id, list_id) DO UPDATE SET status = 'confirmed', updated_at = NOW()`,
|
||||||
|
[subscriberId, LISTMONK_LIST_ID]
|
||||||
|
);
|
||||||
|
console.log(`Added to Listmonk WORLDPLAY list: ${registration.email}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding to Listmonk:', error.message);
|
console.error('Error adding to Listmonk:', error.message);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,6 +514,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})`);
|
console.log(`Listmonk newsletter sync: ${listmonkPool ? 'enabled' : 'disabled'} (list ID: ${LISTMONK_LIST_ID})`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue