From c32070806ac8780fdd760a77e5a1e5f1cef8409e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 30 Jan 2026 20:15:07 +0000 Subject: [PATCH] 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 --- docker-compose.yml | 10 ++-- package.json | 1 + server.js | 138 +++++++++++++++++++++++---------------------- 3 files changed, 77 insertions(+), 72 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 02ae6ac..53f5e8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,10 +13,12 @@ services: - RESEND_API_KEY=${RESEND_API_KEY} - GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID} - GOOGLE_CREDENTIALS=${GOOGLE_CREDENTIALS} - # Listmonk newsletter integration - - LISTMONK_URL=http://listmonk:9000 - - LISTMONK_USER=worldplay-api - - LISTMONK_PASS=worldplay-api-2026 + # Listmonk newsletter integration (direct DB access) + - LISTMONK_DB_HOST=listmonk-db + - LISTMONK_DB_PORT=5432 + - LISTMONK_DB_NAME=listmonk + - LISTMONK_DB_USER=listmonk + - LISTMONK_DB_PASS=listmonk_secure_2025 - LISTMONK_LIST_ID=20 volumes: - worldplay-data:/app/data diff --git a/package.json b/package.json index 409afc6..0522937 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "express": "^4.18.2", "googleapis": "^144.0.0", + "pg": "^8.11.3", "resend": "^4.0.0" }, "engines": { diff --git a/server.js b/server.js index e4c2080..07299c4 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ const fs = require('fs').promises; const path = require('path'); const { google } = require('googleapis'); const { Resend } = require('resend'); +const { Pool } = require('pg'); const app = express(); const PORT = process.env.PORT || 3000; @@ -12,11 +13,15 @@ const REGISTRATIONS_FILE = path.join(DATA_DIR, 'registrations.json'); // Initialize Resend for emails 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'; +// Listmonk PostgreSQL configuration for newsletter management 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 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) { if (!registration.newsletter) { console.log('Newsletter not opted in, skipping Listmonk...'); return; } + if (!listmonkPool) { + console.log('Listmonk database not configured, skipping...'); + return; + } + + const client = await listmonkPool.connect(); 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}`); - } + const fullName = `${registration.firstName} ${registration.lastName}`; + const attribs = { + worldplay: { + firstName: registration.firstName, + lastName: registration.lastName, + location: registration.location || '', + role: registration.role || '', + interests: registration.interests || [], + contribute: registration.contribute || [], + registrationId: registration.id, + registeredAt: registration.registeredAt } + }; + + // 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 { - const errorText = await subscriberResponse.text(); - console.error(`Listmonk API error (${subscriberResponse.status}): ${errorText}`); + // Create new subscriber + 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) { 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(`Google Sheets: ${sheets ? '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})`); }); });