'use strict'; const express = require('express'); const { CJbot } = require('../model/minecraft'); const database = require('./storage/database'); function createRouter() { const router = express.Router(); function dbAvailable() { return database && database.db; } router.get('/api/invite/sites', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const sites = await database.getInviteSites(); // Add bot online status const result = sites.map(s => ({ ...s, players: s.players ? s.players.split(',') : [], bot_online: !!(CJbot.bots[s.bot_name] && CJbot.bots[s.bot_name].isReady), })); res.json({ sites: result }); } catch (error) { console.error('API Error /api/invite/sites:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/invite/bots', (req, res) => { try { const bots = Object.keys(CJbot.bots); res.json({ bots }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.get('/api/invite/online-players', (req, res) => { try { const playerSet = new Set(); for (const bot of Object.values(CJbot.bots)) { if (bot.isReady && bot.bot && bot.bot.players) { for (const name of Object.keys(bot.bot.players)) { playerSet.add(name); } } } res.json({ players: [...playerSet].sort() }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.post('/api/invite/sites', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const { name, label, bot_name, description } = req.body; if (!name || !label || !bot_name) { return res.status(400).json({ error: 'Missing name, label, or bot_name' }); } await database.addInviteSite(name, label, bot_name, description); res.json({ status: 'created' }); } catch (error) { if (error.message && error.message.includes('UNIQUE')) { return res.status(409).json({ error: 'Site name already exists' }); } console.error('API Error POST /api/invite/sites:', error); res.status(500).json({ error: error.message }); } }); router.put('/api/invite/sites/:id', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const id = parseInt(req.params.id); await database.updateInviteSite(id, req.body); res.json({ status: 'updated' }); } catch (error) { console.error('API Error PUT /api/invite/sites/:id:', error); res.status(500).json({ error: error.message }); } }); router.delete('/api/invite/sites/:id', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const id = parseInt(req.params.id); await database.deleteInviteSite(id); res.json({ status: 'deleted' }); } catch (error) { console.error('API Error DELETE /api/invite/sites/:id:', error); res.status(500).json({ error: error.message }); } }); router.post('/api/invite/sites/:id/players', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const siteId = parseInt(req.params.id); const { player_name } = req.body; if (!player_name) return res.status(400).json({ error: 'Missing player_name' }); await database.addInvitePermission(siteId, player_name); res.json({ status: 'added' }); } catch (error) { console.error('API Error POST /api/invite/sites/:id/players:', error); res.status(500).json({ error: error.message }); } }); router.delete('/api/invite/sites/:id/players/:player', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const siteId = parseInt(req.params.id); const playerName = req.params.player; await database.removeInvitePermission(siteId, playerName); res.json({ status: 'removed' }); } catch (error) { console.error('API Error DELETE /api/invite/sites/:id/players/:player:', error); res.status(500).json({ error: error.message }); } }); router.post('/api/invite/trigger', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const { siteName, playerName } = req.body; if (!siteName || !playerName) { return res.status(400).json({ error: 'Missing siteName or playerName' }); } const site = await database.getInviteSiteByName(siteName); if (!site) return res.status(404).json({ error: 'Site not found' }); const allowed = await database.isPlayerAllowedAtSite(siteName, playerName); if (!allowed) return res.status(403).json({ error: `${playerName} is not allowed at ${siteName}` }); const Invite = require('./invite'); Invite.executeInvite(site.bot_name, playerName) .catch(err => console.error('Web invite trigger error:', err)); res.json({ status: 'triggered', message: `Invite sent for ${playerName} via ${site.bot_name}` }); } catch (error) { console.error('API Error POST /api/invite/trigger:', error); res.status(500).json({ error: error.message }); } }); return router; } const webUI = { tabId: 'invites', tabLabel: 'Invites', tabOrder: 30, html: `
Loading invite sites...
`, css: ` .invite-toolbar{display:flex;gap:8px;margin-bottom:16px;align-items:center;flex-wrap:wrap} .invite-toolbar button{background:#2563eb;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:.85em} .invite-toolbar button:hover{background:#1d4ed8} .invite-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px} .invite-card{background:#1f2937;border:1px solid #374151;border-radius:8px;padding:16px;transition:border-color .2s} .invite-card:hover{border-color:#60a5fa} .invite-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px} .invite-card-header h3{font-size:1em;color:#60a5fa;display:flex;align-items:center;gap:8px} .invite-card-header .actions{display:flex;gap:4px} .invite-card-header .actions button{background:none;border:1px solid #374151;color:#9ca3af;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:.75em} .invite-card-header .actions button:hover{border-color:#60a5fa;color:#60a5fa} .invite-card-header .actions button.del:hover{border-color:#ef4444;color:#ef4444} .invite-meta{font-size:.8em;color:#9ca3af;margin-bottom:10px} .invite-meta span{margin-right:12px} .invite-players{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px;min-height:28px} .player-chip{background:#1e3a5f;color:#60a5fa;padding:3px 8px;border-radius:12px;font-size:.8em;display:flex;align-items:center;gap:4px} .player-chip .remove{background:none;border:none;color:#ef4444;cursor:pointer;font-size:.9em;padding:0 2px;line-height:1} .player-chip .remove:hover{color:#f87171} .invite-add-player{display:flex;gap:4px;margin-bottom:10px} .invite-add-player input{flex:1;padding:6px 8px;border:1px solid #374151;border-radius:4px;background:#111827;color:#e5e7eb;font-size:.8em} .invite-add-player button{background:#059669;color:#fff;border:none;padding:6px 10px;border-radius:4px;cursor:pointer;font-size:.8em} .invite-add-player button:hover{background:#047857} .invite-trigger{display:flex;gap:4px;align-items:center} .invite-trigger input{flex:1;padding:6px 8px;border:1px solid #374151;border-radius:4px;background:#111827;color:#e5e7eb;font-size:.8em} .invite-trigger button{background:#7c3aed;color:#fff;border:none;padding:6px 10px;border-radius:4px;cursor:pointer;font-size:.8em} .invite-trigger button:hover{background:#6d28d9} .invite-trigger-status{font-size:.8em;min-height:16px;margin-top:4px} .invite-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:100;display:flex;align-items:center;justify-content:center} .invite-modal{background:#1f2937;border:1px solid #374151;border-radius:8px;padding:24px;width:400px;max-width:90vw} .invite-modal h3{color:#60a5fa;margin-bottom:16px} .invite-modal label{display:block;font-size:.85em;color:#9ca3af;margin-bottom:4px;margin-top:12px} .invite-modal input,.invite-modal select,.invite-modal textarea{width:100%;padding:8px;border:1px solid #374151;border-radius:6px;background:#111827;color:#e5e7eb;font-size:.85em;box-sizing:border-box} .invite-modal textarea{resize:vertical;min-height:60px} .invite-modal .modal-actions{display:flex;gap:8px;margin-top:20px;justify-content:flex-end} .invite-modal .modal-actions button{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:.85em;border:none} .invite-modal .modal-actions .btn-save{background:#2563eb;color:#fff} .invite-modal .modal-actions .btn-save:hover{background:#1d4ed8} .invite-modal .modal-actions .btn-cancel{background:#374151;color:#e5e7eb} .invite-modal .modal-actions .btn-cancel:hover{background:#4b5563} .invite-empty{padding:40px 20px;color:#6b7280;text-align:center;font-size:.9em} `, onTabActive: 'onInviteTabActive', js: ` let inviteSites=[], inviteBots=[], inviteLoaded=false, inviteOnlinePlayers=[]; function onInviteTabActive() { if (!inviteLoaded) loadInviteSites(); } async function loadInviteSites() { try { const [sitesRes, botsRes, playersRes] = await Promise.all([ fetch('/api/invite/sites'), fetch('/api/invite/bots'), fetch('/api/invite/online-players') ]); if (!sitesRes.ok || !botsRes.ok) { document.getElementById('inviteArea').innerHTML='
Failed to load invite data
'; return; } const sitesData = await sitesRes.json(); const botsData = await botsRes.json(); inviteSites = sitesData.sites || []; inviteBots = botsData.bots || []; if (playersRes.ok) { const playersData = await playersRes.json(); inviteOnlinePlayers = playersData.players || []; } inviteLoaded = true; renderInviteSites(); } catch(e) { document.getElementById('inviteArea').innerHTML='
Failed to load invite data
'; } } function renderInviteSites() { const area = document.getElementById('inviteArea'); let html = '
' + '' + '' + '
'; if (inviteSites.length === 0) { html += '
No invite sites configured
'; area.innerHTML = html; return; } html += '
'; for (const site of inviteSites) { const statusCls = site.bot_online ? 'online' : 'offline'; const playerChips = (site.players || []).map(p => '' + escHtml(p) + ' ' + '' ).join(''); html += '
' + '
' + '

' + escHtml(site.label) + ' (' + escHtml(site.name) + ')

' + '
' + '' + '' + '
' + '
' + '
' + 'Bot: ' + escHtml(site.bot_name) + '' + (site.description ? '' + escHtml(site.description) + '' : '') + '
' + '
' + (playerChips || 'No players') + '
' + '
' + '
' + '' + '
' + '
' + '' + '
' + '
' + '
' + '' + '
' + '
' + '' + '
' + '
' + '
'; } html += '
'; area.innerHTML = html; // Wire up autocomplete on player inputs for (const site of inviteSites) { setupAC('inv-add-' + site.id, 'ac-inv-add-' + site.id, q => { const lower = q.toLowerCase(); return inviteOnlinePlayers .filter(p => !lower || p.toLowerCase().includes(lower)) .map(p => ({label: p, value: p})); } ); setupAC('inv-trig-' + site.id, 'ac-inv-trig-' + site.id, q => { const lower = q.toLowerCase(); return inviteOnlinePlayers .filter(p => !lower || p.toLowerCase().includes(lower)) .map(p => ({label: p, value: p})); } ); } } function showInviteModal(editId) { const site = editId ? inviteSites.find(s => s.id === editId) : null; const title = site ? 'Edit Site' : 'Add Site'; const botOptions = inviteBots.map(b => '' ).join(''); const overlay = document.createElement('div'); overlay.className = 'invite-modal-overlay'; overlay.id = 'inviteModalOverlay'; overlay.innerHTML = '
' + '

' + title + '

' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
'; document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) closeInviteModal(); }); } function closeInviteModal() { const el = document.getElementById('inviteModalOverlay'); if (el) el.remove(); } async function saveInviteSite(editId) { const name = document.getElementById('invModalName').value.trim(); const label = document.getElementById('invModalLabel').value.trim(); const bot_name = document.getElementById('invModalBot').value; const description = document.getElementById('invModalDesc').value.trim(); if (!name || !label || !bot_name) { showToast('Fill in name, label, and bot', 'warning'); return; } try { let r; if (editId) { r = await fetch('/api/invite/sites/' + editId, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, label, bot_name, description }) }); } else { r = await fetch('/api/invite/sites', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, label, bot_name, description }) }); } const d = await r.json(); if (!r.ok) { showToast(d.error || 'Failed', 'error'); return; } closeInviteModal(); showToast('Site saved', 'success'); loadInviteSites(); } catch(e) { showToast('Network error', 'error'); } } async function deleteInviteSite(id, name) { if (!confirm('Delete site "' + name + '"? This removes all permissions too.')) return; try { const r = await fetch('/api/invite/sites/' + id, { method: 'DELETE' }); if (!r.ok) { const d = await r.json(); showToast(d.error || 'Failed', 'error'); return; } showToast('Site deleted', 'success'); loadInviteSites(); } catch(e) { showToast('Network error', 'error'); } } async function addInvitePlayer(siteId) { const input = document.getElementById('inv-add-' + siteId); const name = input.value.trim(); if (!name) return; try { const r = await fetch('/api/invite/sites/' + siteId + '/players', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ player_name: name }) }); if (!r.ok) { const d = await r.json(); showToast(d.error || 'Failed', 'error'); return; } input.value = ''; showToast('Player added', 'success'); loadInviteSites(); } catch(e) { showToast('Network error', 'error'); } } async function removeInvitePlayer(siteId, playerName) { try { const r = await fetch('/api/invite/sites/' + siteId + '/players/' + encodeURIComponent(playerName), { method: 'DELETE' }); if (!r.ok) { const d = await r.json(); showToast(d.error || 'Failed', 'error'); return; } showToast('Player removed', 'success'); loadInviteSites(); } catch(e) { showToast('Network error', 'error'); } } async function triggerInvite(siteName, siteId) { const input = document.getElementById('inv-trig-' + siteId); const statusEl = document.getElementById('inv-status-' + siteId); const playerName = input.value.trim(); if (!playerName) { statusEl.textContent = 'Enter player name'; statusEl.style.color = '#ef4444'; return; } try { statusEl.textContent = 'Sending invite...'; statusEl.style.color = '#9ca3af'; const r = await fetch('/api/invite/trigger', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ siteName, playerName }) }); const d = await r.json(); statusEl.textContent = r.ok ? (d.message || 'Triggered') : (d.error || 'Failed'); statusEl.style.color = r.ok ? '#10b981' : '#ef4444'; } catch(e) { statusEl.textContent = 'Network error'; statusEl.style.color = '#ef4444'; } } `, }; module.exports = { name: 'Invite', createRouter, webUI };