diff --git a/services/arbiter-3.0/src/routes/stripe.js b/services/arbiter-3.0/src/routes/stripe.js index 45bcb1c..f6cf2db 100644 --- a/services/arbiter-3.0/src/routes/stripe.js +++ b/services/arbiter-3.0/src/routes/stripe.js @@ -13,6 +13,7 @@ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const db = require('../database'); const { syncRole, removeAllRoles, downgradeToAwakened } = require('../services/discordRoleSync'); const { welcomeNewMember } = require('../services/awakenedConcierge'); +const { syncLuckPermsMeta } = require('../services/luckpermsSync'); // CORS configuration for checkout endpoint const corsOptions = { @@ -286,6 +287,15 @@ router.post('/webhook', express.raw({ type: 'application/json' }), async (req, r welcomeNewMember(discordId, client).catch(err => console.error('[Concierge] Background welcome error:', err.message) ); + + // Sync LuckPerms meta across all online servers (non-blocking) + syncLuckPermsMeta(discordId, tierLevel).then(result => { + if (result.success) { + console.log(`⚔️ LP meta sync for ${result.username}: tier ${tierLevel}, ${result.serversUpdated} servers`); + } else { + console.log(`⚠️ LP meta sync skipped for ${discordId}: ${result.reason}`); + } + }).catch(err => console.error('[LPSync] Background sync error:', err.message)); } break; diff --git a/services/arbiter-3.0/src/services/luckpermsSync.js b/services/arbiter-3.0/src/services/luckpermsSync.js new file mode 100644 index 0000000..e3a15b5 --- /dev/null +++ b/services/arbiter-3.0/src/services/luckpermsSync.js @@ -0,0 +1,111 @@ +/** + * LuckPerms Meta Sync Service + * Sets maxclaims and maxchunkloaders on all online servers when a subscriber's tier changes. + * + * Requires: minecraft_username linked via /link command (stored in users table) + * Uses: Pterodactyl sendCommand API to run lp commands on each online server + * + * Date: April 15, 2026 (Chronicler #91, Launch Day) + */ + +const db = require('../database'); +const { sendCommand, getServerResources } = require('./pterodactyl'); + +// Tier level -> LuckPerms meta values +const TIER_META = { + 1: { maxclaims: 90, maxchunkloaders: 0 }, // Awakened + 2: { maxclaims: 25, maxchunkloaders: 0 }, // Elemental + 3: { maxclaims: 25, maxchunkloaders: 0 }, // Elemental (Fire/Frost split tiers) + 4: { maxclaims: 49, maxchunkloaders: 4 }, // Knight + 5: { maxclaims: 49, maxchunkloaders: 4 }, // Knight + 6: { maxclaims: 100, maxchunkloaders: 9 }, // Master + 7: { maxclaims: 100, maxchunkloaders: 9 }, // Master + 8: { maxclaims: 121, maxchunkloaders: 16 }, // Legend + 9: { maxclaims: 121, maxchunkloaders: 16 }, // Legend + 10: { maxclaims: 225, maxchunkloaders: 81 }, // Sovereign +}; + +/** + * Get all online Minecraft server identifiers from server_config + */ +async function getOnlineServers() { + const result = await db.query( + `SELECT server_identifier FROM server_config WHERE server_identifier IS NOT NULL` + ); + const identifiers = result.rows.map(r => r.server_identifier); + + // Filter to only running servers + const online = []; + await Promise.all(identifiers.map(async (id) => { + try { + const res = await getServerResources(id); + if (res.state === 'running') online.push(id); + } catch (e) { + // Server unreachable — skip silently + } + })); + + return online; +} + +/** + * Sync LuckPerms meta for a subscriber across all online servers. + * Called after Stripe checkout or tier change. + * + * @param {string} discordId - Discord user ID + * @param {number} tierLevel - Subscription tier level (1-10) + */ +async function syncLuckPermsMeta(discordId, tierLevel) { + const meta = TIER_META[tierLevel]; + if (!meta) { + console.warn(`[LPSync] Unknown tier level ${tierLevel} for ${discordId} — skipping`); + return { success: false, reason: 'unknown_tier' }; + } + + // Look up Minecraft username + const userResult = await db.query( + 'SELECT minecraft_username FROM users WHERE discord_id = $1', + [discordId] + ); + + if (userResult.rows.length === 0 || !userResult.rows[0].minecraft_username) { + console.log(`[LPSync] No Minecraft username linked for Discord ID ${discordId} — skipping LP meta sync`); + return { success: false, reason: 'no_minecraft_link' }; + } + + const username = userResult.rows[0].minecraft_username; + + // Get online servers + const onlineServers = await getOnlineServers(); + + if (onlineServers.length === 0) { + console.warn(`[LPSync] No online servers found — LP meta sync skipped for ${username}`); + return { success: false, reason: 'no_online_servers' }; + } + + // Send meta commands to all online servers + const results = await Promise.allSettled( + onlineServers.flatMap(serverId => [ + sendCommand(serverId, `lp user ${username} meta set maxclaims ${meta.maxclaims}`), + sendCommand(serverId, `lp user ${username} meta set maxchunkloaders ${meta.maxchunkloaders}`) + ]) + ); + + const succeeded = results.filter(r => r.status === 'fulfilled').length; + const failed = results.filter(r => r.status === 'rejected').length; + + console.log(`[LPSync] Meta sync for ${username} (tier ${tierLevel}): ${succeeded} OK, ${failed} failed across ${onlineServers.length} servers`); + console.log(`[LPSync] maxclaims=${meta.maxclaims}, maxchunkloaders=${meta.maxchunkloaders}`); + + return { + success: true, + username, + tierLevel, + meta, + serversUpdated: onlineServers.length, + commandsSucceeded: succeeded, + commandsFailed: failed + }; +} + +module.exports = { syncLuckPermsMeta };