'use strict'; const express = require('express'); const { CJbot } = require('../model/minecraft'); // In-memory ring buffer for chat messages const MAX_MESSAGES = 500; const messages = []; let messageId = 0; function addMessage(type, from, text, botName) { messages.push({ id: ++messageId, type, // 'chat', 'whisper', 'system', 'bot' from, text, botName, timestamp: Date.now(), }); if (messages.length > MAX_MESSAGES) messages.splice(0, messages.length - MAX_MESSAGES); } // Hook into all bots' chat events (called once per bot connection) const hookedBots = new Set(); function hookBot(bot) { const name = bot.name; if (hookedBots.has(name)) return; hookedBots.add(name); // Re-hook on each spawn (reconnection creates a new mineflayer bot) const attach = () => { if (!bot.bot) return; bot.bot.on('chat', (from, message) => { addMessage('chat', from, message, name); }); bot.bot.on('whisper', (from, message) => { addMessage('whisper', from, message, name); }); bot.bot.on('message', (jsonMsg, position) => { if (position === 'game_info') return; // skip action bar const text = jsonMsg.toString(); // Skip empty or already-captured chat/whisper if (!text || text.startsWith('<')) return; addMessage('system', null, text, name); }); }; // If the bot is already connected, attach now if (bot.bot) attach(); // Also attach on every future spawn const origConnect = bot.connect.bind(bot); bot.connect = async function (...args) { const result = await origConnect(...args); attach(); return result; }; } // Periodically check for new bots to hook setInterval(() => { for (const [name, bot] of Object.entries(CJbot.bots)) { hookBot(bot); } }, 2000); // Also hook any bots that exist right now for (const [name, bot] of Object.entries(CJbot.bots)) { hookBot(bot); } function createRouter() { const router = express.Router(); // Get messages, optionally filtering by ?since= for polling router.get('/api/chat/messages', (req, res) => { try { const since = parseInt(req.query.since) || 0; const filtered = since ? messages.filter(m => m.id > since) : messages.slice(-100); res.json({ messages: filtered, lastId: messageId }); } catch (error) { console.error('API Error /api/chat/messages:', error); res.status(500).json({ error: error.message }); } }); // Send a chat message as a bot router.post('/api/chat/send', async (req, res) => { try { const { botName, message, whisperTo } = req.body; if (!message) return res.status(400).json({ error: 'Missing message' }); // Find a bot to send from let bot = null; if (botName && CJbot.bots[botName]) { bot = CJbot.bots[botName]; } else { // Use first connected bot for (const b of Object.values(CJbot.bots)) { if (b.isReady) { bot = b; break; } } } if (!bot || !bot.isReady) { return res.status(503).json({ error: 'No connected bot available' }); } if (whisperTo) { await bot.whisper(whisperTo, message); addMessage('bot', bot.bot.entity.username, `/msg ${whisperTo} ${message}`, bot.name); } else { await bot.say(message); addMessage('bot', bot.bot.entity.username, message, bot.name); } res.json({ status: 'sent' }); } catch (error) { console.error('API Error /api/chat/send:', error); res.status(500).json({ error: error.message }); } }); return router; } const webUI = { tabId: 'chat', tabLabel: 'Chat', tabOrder: 5, html: `
Loading chat...
`, css: ` #chatArea{display:flex;flex-direction:column;height:calc(100vh - 140px)} .chat-messages{flex:1;overflow-y:auto;padding:12px;background:#0f172a;border:1px solid #374151;border-radius:8px;margin-bottom:12px;font-family:'Consolas','Monaco',monospace;font-size:.85em;line-height:1.6} .chat-msg{padding:2px 0;word-wrap:break-word} .chat-msg .chat-time{color:#4b5563;font-size:.8em;margin-right:6px} .chat-msg .chat-from{font-weight:600} .chat-msg.type-chat .chat-from{color:#60a5fa} .chat-msg.type-whisper .chat-from{color:#a78bfa} .chat-msg.type-whisper{background:rgba(167,139,250,.08);padding:2px 4px;border-radius:3px} .chat-msg.type-system{color:#6b7280;font-style:italic} .chat-msg.type-bot .chat-from{color:#f59e0b} .chat-input-area{display:flex;gap:8px;align-items:center} .chat-filter-bar{display:flex;gap:8px;align-items:center;margin-bottom:8px} .chat-filter-bar input{padding:8px 12px;border:1px solid #374151;border-radius:6px;background:#111827;color:#e5e7eb;font-size:.85em;flex:1} .chat-filter-bar label{font-size:.8em;color:#9ca3af;display:flex;align-items:center;gap:4px;cursor:pointer} .chat-filter-bar label input[type=checkbox]{accent-color:#2563eb} `, onTabActive: 'onChatTabActive', js: ` let chatLastId=0, chatInterval=null, chatAutoScroll=true, chatFilter=''; let chatShowTypes={chat:true,whisper:true,system:true,bot:true}; function onChatTabActive() { loadChatMessages(); populateBotSelect(); if (!chatInterval) chatInterval = setInterval(() => { if (currentTab === 'chat') pollChat(); }, 1500); } async function loadChatMessages() { try { const r = await fetch('/api/chat/messages'); if (!r.ok) return; const d = await r.json(); chatLastId = d.lastId || 0; renderChatMessages(d.messages || []); } catch(e) {} } async function pollChat() { try { const r = await fetch('/api/chat/messages?since=' + chatLastId); if (!r.ok) return; const d = await r.json(); if (d.messages && d.messages.length > 0) { chatLastId = d.lastId || chatLastId; appendChatMessages(d.messages); } } catch(e) {} } function formatChatTime(ts) { const d = new Date(ts); return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); } function renderChatLine(msg) { const time = '' + formatChatTime(msg.timestamp) + ''; if (msg.type === 'system') { return '
' + time + escHtml(msg.text) + '
'; } const label = msg.type === 'whisper' ? ' whispers: ' : ': '; return '
' + time + '' + escHtml(msg.from || '???') + '' + label + escHtml(msg.text) + '
'; } function matchesFilter(msg) { if (!chatShowTypes[msg.type]) return false; if (!chatFilter) return true; const q = chatFilter.toLowerCase(); return (msg.from && msg.from.toLowerCase().includes(q)) || (msg.text && msg.text.toLowerCase().includes(q)); } function renderChatMessages(msgs) { const container = document.getElementById('chatMessages'); const filtered = msgs.filter(matchesFilter); if (filtered.length === 0) { container.innerHTML = '
No messages yet
'; return; } container.innerHTML = filtered.map(renderChatLine).join(''); if (chatAutoScroll) container.scrollTop = container.scrollHeight; } function appendChatMessages(msgs) { const container = document.getElementById('chatMessages'); // Remove placeholder if present const placeholder = container.querySelector('div[style]'); if (placeholder && container.children.length === 1 && placeholder.textContent.includes('No messages')) { container.innerHTML = ''; } const filtered = msgs.filter(matchesFilter); for (const msg of filtered) { container.insertAdjacentHTML('beforeend', renderChatLine(msg)); } // Trim old messages from DOM while (container.children.length > 500) container.removeChild(container.firstChild); if (chatAutoScroll) container.scrollTop = container.scrollHeight; } async function populateBotSelect() { try { const r = await fetch('/api/bots'); if (!r.ok) return; const d = await r.json(); const sel = document.getElementById('chatBot'); const current = sel.value; sel.innerHTML = '' + Object.entries(d.bots || {}).filter(([,b]) => b.connected).map(([name]) => '' ).join(''); sel.value = current; } catch(e) {} } async function sendChatMessage() { const input = document.getElementById('chatInput'); const message = input.value.trim(); if (!message) return; const botName = document.getElementById('chatBot').value || undefined; const whisperTo = document.getElementById('chatWhisper').value.trim() || undefined; try { const r = await fetch('/api/chat/send', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ botName, message, whisperTo }) }); if (r.ok) { input.value = ''; setTimeout(pollChat, 300); } else { const d = await r.json(); showToast(d.error || 'Failed to send', 'error'); } } catch(e) { showToast('Network error', 'error'); } } document.getElementById('chatInput').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); sendChatMessage(); } }); document.getElementById('chatSendBtn').addEventListener('click', sendChatMessage); // Auto-scroll toggle: disable if user scrolls up, re-enable at bottom document.getElementById('chatMessages').addEventListener('scroll', function() { chatAutoScroll = this.scrollTop + this.clientHeight >= this.scrollHeight - 30; }); `, }; module.exports = { name: 'Chat', createRouter, webUI };