// Stoat Server Info Bot const config = require("./mapinfo_stoat.json"); const srcds = require("srcds-info"); const https = require('https'); const rcon = require("srcds-rcon"); const BOT_TOKEN = ""; const API_URL = ""; const CHANNEL_ID = ""; const channel_livechat = ""; const channel_livechat_mg = ""; const channel_rcon_ze = ""; const channel_adminchat_ze = ""; var recentStats = []; var currentStats = []; var recentMessageId = null; var updateMessage = false; var pendingQueries = 0; // Helper function to make API calls function stoatAPI(method, endpoint, data = null) { return new Promise((resolve, reject) => { const options = { hostname: '', path: `/api${endpoint}`, method: method, headers: { 'x-bot-token': BOT_TOKEN, 'Content-Type': 'application/json' } }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(JSON.parse(body)); } else { reject(new Error(`HTTP ${res.statusCode}: ${body}`)); } } catch (error) { reject(error); } }); }); req.on('error', (error) => { reject(error); }); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } // Send or edit message in Stoat async function sendOrUpdateMessage(content) { try { if (recentMessageId) { // Edit existing message await stoatAPI('PATCH', `/channels/${CHANNEL_ID}/messages/${recentMessageId}`, { content: content }); console.log("Message updated"); } else { // Send new message const response = await stoatAPI('POST', `/channels/${CHANNEL_ID}/messages`, { content: content }); recentMessageId = response._id; console.log("New message sent, ID:", recentMessageId); } } catch (error) { console.error("Error sending/updating message:", error.message); // If edit fails, try sending new message if (recentMessageId) { console.log("Edit failed, clearing message ID and will send new next time"); recentMessageId = null; } } } function checkAndUpdateMessage() { // Check if we should update the message if (recentStats.length) { recentStats.map((stats, index) => { if (!currentStats[index] || currentStats[index].serverName != stats.serverName) updateMessage = true; if (!currentStats[index] || currentStats[index].currentMap != stats.currentMap) updateMessage = true; if (!currentStats[index] || currentStats[index].numPlayers != stats.numPlayers) updateMessage = true; if (!currentStats[index] || currentStats[index].maxPlayers != stats.maxPlayers) updateMessage = true; currentStats[index] = stats; }); } if (updateMessage) { updateMessage = false; // Build message content (Stoat uses Markdown) let time_obj = new Date(Date.now()); let options = {month: 'short', day: 'numeric'}; let date = time_obj.toLocaleString('en-US', options); options = {hour12: true, second: 'numeric', minute: 'numeric', hour: 'numeric'}; let time = time_obj.toLocaleString('en-US', options); let messageContent = "# UNLOZE Server Info\n\n"; if (currentStats.length) { currentStats.map((stats, index) => { messageContent += `### ${stats.serverName}\n`; messageContent += `**${stats.currentMap} (${stats.numPlayers}/${stats.maxPlayers})**\n`; messageContent += `[${stats.adress}:${stats.port}](steam://connect/${stats.adress}:${stats.port})\n\n`; }); } //messageContent += `\n*Last Update: ${date}, ${time}*`; // Send or update the message sendOrUpdateMessage(messageContent); sendServerInfo(messageContent); } } //AI slop function async function sendServerInfo(messageContent) { const url = ''; const data = { name: 'Server-01', content: messageContent }; try { await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) // Convert JS object to JSON string }); } catch (error) { console.error('Network error or CORS issue:', error); } } function updateServerStats() { if (config.servers) { pendingQueries = config.servers.length; config.servers.map((server, index) => { var query = srcds(server.adress, server.port); query.info((error, result) => { query.close(); pendingQueries--; if (error) { if (error == "Error: Request timed out") { // Check if all queries are done if (pendingQueries === 0) { checkAndUpdateMessage(); } return; } console.error(error); // Check if all queries are done if (pendingQueries === 0) { checkAndUpdateMessage(); } return; } if (result) { recentStats[index] = []; recentStats[index].abbr = server.abbr; recentStats[index].adress = server.adress; recentStats[index].port = server.port; recentStats[index].serverName = result.serverName; recentStats[index].currentMap = result.map; recentStats[index].numPlayers = result.numPlayers; recentStats[index].maxPlayers = result.maxPlayers; } // Check if all queries are done if (pendingQueries === 0) { checkAndUpdateMessage(); } }); }); } } const remotecon_css_ze = rcon({ address: '', password: '' }); const remotecon_css_mg = rcon({ address: '', password: '' }); String.prototype.formatUnicorn = String.prototype.formatUnicorn || function () { "use strict"; var str = this.toString(); if (arguments.length) { var t = typeof arguments[0]; var key; var args = ("string" === t || "number" === t) ? Array.prototype.slice.call(arguments) : arguments[0]; for (key in args) { str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]); } } return str; }; let lastProcessedMessageId = null; async function checkNewMessages(channel_selected, admin_chat_bool) { try { let url = ""; // If it's the very first time, we just want to get the // current latest message ID and STOP so we don't spam old messages. if (!lastProcessedMessageId) { url = `/channels/${channel_selected}/messages?limit=1`; const response = await stoatAPI('GET', url); //console.log(JSON.stringify(response, null, 2)); //console.log(` ${lastProcessedMessageId}. ${response[0]["content"]}`); if (response[0]) { lastProcessedMessageId = response[0]._id; //console.log(`[Init] Set starting ID to: ${lastProcessedMessageId}.`); } return; } // Subsequent polls: Get everything AFTER the last ID, sorted Oldest -> Newest url = `/channels/${channel_selected}/messages?limit=50&include_users=true&sort=Oldest&after=${lastProcessedMessageId}`; const response = await stoatAPI('GET', url); const messages = response.messages || []; const users = response.users || []; if (messages.length === 0) return; const userMap = new Map(users.map(u => [u._id, u])); for (const message of messages) { const author = userMap.get(message.author); // Update ID immediately so we don't process this one again even if we skip it lastProcessedMessageId = message._id; if (!author || author.bot) continue; const content = message.content || ""; const username = author.username || "Unknown"; //console.log(`[New] ${username}: ${content}`); if (channel_selected == channel_livechat) { await remotecon_css_ze.connect(); if (admin_chat_bool) { await remotecon_css_ze.command('sm_printtoadminchat_stoat "{0}" "{1}"'.formatUnicorn(username, content)); } else { await remotecon_css_ze.command('sm_printtoallchat_stoat "{0}" "{1}"'.formatUnicorn(username, content)); } await remotecon_css_ze.disconnect(); } else if (channel_selected == channel_livechat_mg) { await remotecon_css_mg.connect(); await remotecon_css_mg.command('sm_printtoallchat_stoat "{0}" "{1}"'.formatUnicorn(username, content)); await remotecon_css_mg.disconnect(); } } } catch (error) { console.error('Error checking messages:', error.message); } } var lastProcessedMessageId_rcon_ze = null; async function checkNewMessages_rcon_ze() { try { // Get just the most recent message const response = await stoatAPI('GET', `/channels/${channel_rcon_ze}/messages?limit=1`); //console.log('API Response:', JSON.stringify(response, null, 2)); // Response is an array directly if (!Array.isArray(response) || response.length === 0) { return; } const message = response[0]; // Skip if we've already processed this message if (lastProcessedMessageId_rcon_ze === message._id) { return; } // Get user info let username = "Unknown"; try { const userInfo = await stoatAPI('GET', `/users/${message.author}`); username = userInfo.username || userInfo.display_name || "Unknown"; // Skip bot messages if (userInfo.bot) { lastProcessedMessageId_rcon_ze = message._id; return; } } catch (error) { console.error('Error fetching user info:', error.message); } const content = message.content || ""; //console.log(`New message from ${username}: ${content}`); remotecon_css_ze.connect().then(() => { remotecon_css_ze.command(content).then(async response => { // Wrap in code blocks const formattedResponse = `\`\`\`\n${response}\n\`\`\``; // Split if too long (Discord/Stoat limit is typically 2000 characters) const maxLength = 1990; // Leave room for code block markers if (formattedResponse.length <= 2000) { await stoatAPI('POST', `/channels/${channel_rcon_ze}/messages`, { content: formattedResponse }); } else { // Split the response (not the code blocks) into chunks const chunks = response.match(/.{1,1990}/gs) || []; for (const chunk of chunks) { await stoatAPI('POST', `/channels/${channel_rcon_ze}/messages`, { content: `\`\`\`\n${chunk}\n\`\`\`` }); } } }).then(() => { remotecon_css_ze.disconnect(); }); }); lastProcessedMessageId_rcon_ze = message._id; } catch (error) { console.error('Error checking messages:', error.message); } } // Start the bot async function start() { console.log("Stoat Server Info Bot started!"); // Update stats every 5 minutes setInterval(updateServerStats, 300000); // Check for new messages 5 every second setInterval(() => checkNewMessages(channel_livechat, false), 5000); setInterval(() => checkNewMessages(channel_livechat_mg, false), 5000); setInterval(() => checkNewMessages(channel_adminchat_ze, true), 5000); // Check rcon ze channel for new messages every 5 seconds setInterval(checkNewMessages_rcon_ze, 5000); // Initial update updateServerStats(); checkNewMessages(channel_livechat, false); checkNewMessages(channel_livechat_mg, false); checkNewMessages(channel_adminchat_ze, true); checkNewMessages_rcon_ze(); } start();