From 7e095f085fe62cc9f4d57a2ed3a9ae792f61b7b5 Mon Sep 17 00:00:00 2001 From: jenz Date: Fri, 13 Feb 2026 15:38:13 +0100 Subject: [PATCH] adding stoat support for server-info and for xenforo node page --- mapinfo_stoat.js | 371 +++++++++++++++++++++++++++++++++++++++++++++ mapinfo_stoat.json | 30 ++++ run.sh | 3 +- 3 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 mapinfo_stoat.js create mode 100644 mapinfo_stoat.json diff --git a/mapinfo_stoat.js b/mapinfo_stoat.js new file mode 100644 index 0000000..9c5d141 --- /dev/null +++ b/mapinfo_stoat.js @@ -0,0 +1,371 @@ +// 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_rcon_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: 'add hostname here', + 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 = 'add here'; + 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: '' +}); + +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() { + 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_livechat}/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_livechat}/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}`); + await remotecon_css_ze.connect(); + await remotecon_css_ze.command('sm_printtoallchat_stoat "{0}" "{1}"'.formatUnicorn(username, content)); + await remotecon_css_ze.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, 5000); + + // Check rcon ze channel for new messages every 5 seconds + setInterval(checkNewMessages_rcon_ze, 5000); + + // Initial update + updateServerStats(); + checkNewMessages(); + checkNewMessages_rcon_ze(); +} + +start(); + diff --git a/mapinfo_stoat.json b/mapinfo_stoat.json new file mode 100644 index 0000000..67cec0c --- /dev/null +++ b/mapinfo_stoat.json @@ -0,0 +1,30 @@ +{ + "servers": [ + { + "abbr": "CSS-ZE", + "adress": "51.195.188.106", + "port": 27015 + }, + { + "abbr": "CSS-ZR", + "adress": "51.195.188.106", + "port": 27016 + }, + { + "abbr": "CSS-MG", + "adress": "51.195.188.106", + "port": 27017 + }, + { + "abbr": "CSS-ZE-2", + "adress": "51.195.188.106", + "port": 27035 + }, + { + "abbr": "SVEN-COOP", + "adress": "51.195.188.106", + "port": 27025 + } + ] +} + diff --git a/run.sh b/run.sh index ab2aec0..392efe0 100644 --- a/run.sh +++ b/run.sh @@ -1,2 +1,3 @@ #!/bin/sh -forever start mapinfo.js \ No newline at end of file +forever start mapinfo.js +forever start mapinfo_stoat.js