watcher, game-server + history, telegram-bot, discord-bot wip

This commit is contained in:
Smith
2022-04-04 23:24:12 +02:00
parent c3f4304ba2
commit fb749b5515
28 changed files with 3548 additions and 216 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
data/servers.json
data/discord.json
data/telegram.json
# Logs # Logs
logs logs
*.log *.log

View File

@@ -1 +1,29 @@
# game-server-watcher # game-server-watcher
The goals of this repo:
1. create a (very simple, but capable) bot to monitor game servers
1. get gamedig server info
1. get steam api server info
1. should be able to host on a free service (target atm: cloudno.de (nodejs v12.20.1))
1. discord bot (show and refresh server info (on command?))
1. telegram bot (send updates (on command?))
# Self host on cloudno.de
## Part 1: create and setup github repo
//TODO: fork this repo
## Part 2: create and setup cloudno.de repo
//TODO: create app, copy git url & append with `login:token@`
// on github: goto settings --> secrets \[actions\], setup CLOUDNODE_REPO_URL
## Part 3 (optional): create and invite discord bot
//TODO: create discord bot; invite to server(guild) with permissions: view channels, send messages, message history, embed stuff, create bot auth token and setup DISCORD_BOT_TOKEN in cloud app env
## Part 4 (optional): create telegram bot and get token
//TODO: talk to botfather, get chat id: https://t.me/getidsbot, setup bot token as TELEGRAM_BOT_TOKEN in cloud app env
## Part 5 (optional): create steam web api key
//TODO: submit form and create key, setup web key as STEAM_WEB_API_KEY in cloud app env
## Part 6: create `cloud` branch, configure and deploy the service
//TODO: create a custom.config.json file, and setup GSW_CONFIG in cloud app env to point to it.
//git branch cloud && git add . && git commit -m :shipit: && git push origin cloud

15
config/bkz.config.json Normal file
View File

@@ -0,0 +1,15 @@
[
{
"host": "armahu.ddns.net",
"port": 2332,
"type": "arma3",
"appId": 107410,
"discord": {
"channelIds": ["955162997410639962"]
},
"telegram": {
"chatIds": ["325831302"]
}
}
]

View File

@@ -0,0 +1,26 @@
[
{
"host": "85.190.148.52",
"port": 2102,
"type": "arma3",
"appId": 107410,
"discord": {
"channelIds": ["935912356335190026"]
},
"telegram": {
"chatIds": ["325831302"]
}
},
{
"host": "85.190.148.62",
"port": 2302,
"type": "arma3",
"appId": 107410,
"discord": {
"channelIds": ["935912356335190026"]
},
"telegram": {
"chatIds": ["325831302"]
}
}
]

0
data/.gitkeep Normal file
View File

5
default.env Normal file
View File

@@ -0,0 +1,5 @@
DBG=1
REFRESH_TIME_MINUTES=1
DISCORD_BOT_TOKEN=
TELEGRAM_BOT_TOKEN=
STEAM_WEB_API_KEY=

1830
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,29 @@
{ {
"name": "gsw", "name": "game-server-watcher",
"version": "1.0.0", "version": "1.0.0",
"description": "Game Server Watcher", "description": "Game Server Watcher",
"main": "server", "main": "server",
"scripts": { "scripts": {
"start": "tsc && node server.js", "start": "tsc && node server.js",
"dev": "tsc -w", "build": "tsc",
"build": "tsc" "watch": "tsc -w",
"dev": "nodemon -e js --exec node server"
}, },
"keywords": [], "keywords": [],
"author": "a-sync@devs.space", "author": "a-sync@devs.space",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^13.6.0" "@commonify/lowdb": "^3.0.0",
"discord.js": "^12.5.3",
"dotenv": "^16.0.0",
"gamedig": "github:a-sync/node8-gamedig",
"got": "^11.8.3",
"grammy": "^1.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/gamedig": "^3.0.2",
"@types/node": "12.20", "@types/node": "12.20",
"@types/utf8": "^3.0.1", "nodemon": "^2.0.15",
"ts-node": "^10.7.0",
"typescript": "^4.6.3" "typescript": "^4.6.3"
}, },
"engines": { "engines": {

View File

@@ -2,18 +2,12 @@
var __importDefault = (this && this.__importDefault) || function (mod) { var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); //Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs")); const fs_1 = __importDefault(require("fs"));
const http_1 = require("http"); const http_1 = require("http");
const url_1 = require("url"); const url_1 = require("url");
const SERVERS = [ require("dotenv/config");
{ const watcher_1 = require("./src/watcher");
type: 'arma3',
host: '127.0.0.1',
port: '2302',
discordChannelId: '99988877700'
}
];
const CACHE_MAX_AGE = parseInt(process.env.CACHE_MAX_AGE || '0', 10); const CACHE_MAX_AGE = parseInt(process.env.CACHE_MAX_AGE || '0', 10);
const APP_HOST = process.env.app_host || '0.0.0.0'; const APP_HOST = process.env.app_host || '0.0.0.0';
const APP_PORT = parseInt(process.env.app_port || '8080', 10); const APP_PORT = parseInt(process.env.app_port || '8080', 10);
@@ -30,9 +24,25 @@ const SECRET = process.env.SECRET || 'secret';
}); });
fs_1.default.createReadStream('./index.html').pipe(res); fs_1.default.createReadStream('./index.html').pipe(res);
} }
else if (reqUrl.pathname === '/discord/post') { //DEBUG
console.log('REQ.HEADERS', req.headers);
let body = '';
req.on('data', (chunk) => {
body += chunk; // convert Buffer to string
});
req.on('end', () => {
console.log('POST.DATA:', String(body));
res.end('');
});
}
else if (reqUrl.pathname === '/ping') {
console.log('ping');
res.end('pong');
}
else { else {
res.writeHead(404, { 'Content-Type': 'text/html' }); res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<html><head></head><body>404 &#x1F4A2</body></html>'); res.end('<html><head></head><body>404 &#x1F4A2</body></html>');
} }
}).listen(APP_PORT); }).listen(APP_PORT);
console.log('Web service started %s:%s', APP_HOST, APP_PORT); console.log('Web service started %s:%s', APP_HOST, APP_PORT);
(0, watcher_1.main)();

View File

@@ -2,18 +2,14 @@ import fs from 'fs';
import { createServer } from 'http'; import { createServer } from 'http';
import { URL } from 'url'; import { URL } from 'url';
const SERVERS = [ import 'dotenv/config';
{
type: 'arma3', import { main, WatcherConfig } from './src/watcher';
host: '127.0.0.1',
port: '2302',
discordChannelId: '99988877700'
}
];
const CACHE_MAX_AGE = parseInt(process.env.CACHE_MAX_AGE || '0', 10); const CACHE_MAX_AGE = parseInt(process.env.CACHE_MAX_AGE || '0', 10);
const APP_HOST = process.env.app_host || '0.0.0.0'; const APP_HOST = process.env.app_host || '0.0.0.0';
const APP_PORT = parseInt(process.env.app_port || '8080', 10); const APP_PORT = parseInt(process.env.app_port || '8080', 10);
const DBG = Boolean(process.env.DBG || false); const DBG = Boolean(process.env.DBG || false);
const SECRET = process.env.SECRET || 'secret'; const SECRET = process.env.SECRET || 'secret';
@@ -28,6 +24,21 @@ createServer(async (req, res) => {
}); });
fs.createReadStream('./index.html').pipe(res); fs.createReadStream('./index.html').pipe(res);
} }
else if (reqUrl.pathname === '/discord/post') {//DEBUG
console.log('REQ.HEADERS', req.headers);
let body = '';
req.on('data', (chunk) => {
body += chunk; // convert Buffer to string
});
req.on('end', () => {
console.log('POST.DATA:', String(body));
res.end('');
});
}
else if (reqUrl.pathname === '/ping') {
console.log('ping');
res.end('pong');
}
else { else {
res.writeHead(404, { 'Content-Type': 'text/html' }); res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<html><head></head><body>404 &#x1F4A2</body></html>'); res.end('<html><head></head><body>404 &#x1F4A2</body></html>');
@@ -35,3 +46,5 @@ createServer(async (req, res) => {
}).listen(APP_PORT); }).listen(APP_PORT);
console.log('Web service started %s:%s', APP_HOST, APP_PORT); console.log('Web service started %s:%s', APP_HOST, APP_PORT);
main();

613
src/bot.js Normal file
View File

@@ -0,0 +1,613 @@
/*
Author:
Ramzi Sah#2992
Desription:
main bot code for game status discord bot (gamedig) - https://discord.gg/vsw2ecxYnH
Updated:
20220403 - soulkobk, updated player parsing from gamedig, and various other code adjustments
*/
// read configs
const fs = require('fs');
var config = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf8'));
// await for instance id
var instanceId = -1;
process.on('message', function(m) {
// get message type
if (Object.keys(m)[0] == "id") {
// set instance id
instanceId = m.id
// send ok signal to main process
process.send({
instanceid : instanceId,
message : "instance started."
});
// init bot
init();
};
});
function init() {
// get config
config["instances"][instanceId]["webServerHost"] = config["webServerHost"];
config["instances"][instanceId]["webServerPort"] = config["webServerPort"];
config["instances"][instanceId]["statusUpdateTime"] = config["statusUpdateTime"];
config["instances"][instanceId]["timezone"] = config["timezone"];
config["instances"][instanceId]["format24h"] = config["format24h"];
config = config["instances"][instanceId];
// connect to discord API
client.login(config["discordBotToken"]);
};
//----------------------------------------------------------------------------------------------------------
// common
function Sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
};
//----------------------------------------------------------------------------------------------------------
// create client
require('dotenv').config();
const {Client, MessageEmbed, Intents, MessageActionRow, MessageButton} = require('discord.js');
const client = new Client({
messageEditHistoryMaxSize: 0,
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES]
});
//----------------------------------------------------------------------------------------------------------
// on client ready
client.on('ready', async () => {
process.send({
instanceid : instanceId,
message : "Logged in as \"" + client.user.tag + "\"."
});
// wait until process instance id receaived
while (instanceId < 0) {
await Sleep(1000);
};
// get broadcast chanel
let statusChannel = client.channels.cache.get(config["serverStatusChanelId"]);
if (statusChannel == undefined) {
process.send({
instanceid : instanceId,
message : "ERROR: channel id " + config["serverStatusChanelId"] + ", does not exist."
});
return;
};
// get a status message
let statusMessage = await createStatusMessage(statusChannel);
if (statusMessage == undefined) {
process.send({
instanceid : instanceId,
message : "ERROR: could not send the status message."
});
return;
};
// start server status loop
startStatusMessage(statusMessage);
// start generate graph loop
generateGraph();
});
//----------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------
// create/get last status message
async function createStatusMessage(statusChannel) {
// delete old messages except the last one
await clearOldMessages(statusChannel, 1);
// get last message
let statusMessage = await getLastMessage(statusChannel);
if (statusMessage != undefined) {
// return last message if exists
return statusMessage;
};
// delete all messages
await clearOldMessages(statusChannel, 0);
// create new message
let embed = new MessageEmbed();
embed.setTitle("instance starting...");
embed.setColor('#ffff00');
return await statusChannel.send({ embeds: [embed] }).then((sentMessage)=> {
return sentMessage;
});
};
function clearOldMessages(statusChannel, nbr) {
return statusChannel.messages.fetch({limit: 99}).then(messages => {
// select bot messages
messages = messages.filter(msg => (msg.author.id == client.user.id && !msg.system));
// keep track of all promises
let promises = [];
// delete messages
let i = 0;
messages.each(mesasge => {
// let nbr last messages
if (i >= nbr) {
// push to promises
promises.push(
mesasge.delete().catch(function(error) {
return;
})
);
};
i += 1;
});
// return when all promises are done
return Promise.all(promises).then(() => {
return;
});
}).catch(function(error) {
return;
});
};
function getLastMessage(statusChannel) {
return statusChannel.messages.fetch({limit: 20}).then(messages => {
// select bot messages
messages = messages.filter(msg => (msg.author.id == client.user.id && !msg.system));
// return first message
return messages.first();
}).catch(function(error) {
return;
});
};
//----------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------
// main loops
async function startStatusMessage(statusMessage) {
while(true){
try {
// steam link button
let row = new MessageActionRow()
row.addComponents(
new MessageButton()
.setCustomId('steamLink')
.setLabel('Connect')
.setStyle('PRIMARY')
);
let embed = await generateStatusEmbed();
statusMessage.edit({ embeds: [embed], components: config["steam_btn"] ? [row] : [] });
} catch (error) {
process.send({
instanceid : instanceId,
message : "ERROR: could not edit status message. " + error
});
};
await Sleep(config["statusUpdateTime"] * 1000);
};
};
client.on('interactionCreate', interaction => {
if (!interaction.isButton()) return;
interaction.reply({ content: 'steam://connect/' + config["server_host"] + ':' + config["server_port"], ephemeral: true });
});
//----------------------------------------------------------------------------------------------------------
// fetch data
const gamedig = require('gamedig');
var tic = false;
function generateStatusEmbed() {
let embed = new MessageEmbed();
// set embed name and logo
embed.setAuthor({ name: '', iconURL: '', url: '' })
// set embed updated time
tic = !tic;
let ticEmojy = tic ? "⚪" : "⚫";
let updatedTime = new Date();
updatedTime.setHours(updatedTime.getHours() + config["timezone"][0] - 1);
updatedTime.setMinutes(updatedTime.getMinutes() + config["timezone"][1]);
let footertimestamp = ticEmojy + ' ' + "Last Update" + ': ' + updatedTime.toLocaleTimeString('en-US', {hour12: !config["format24h"], month: 'short', day: 'numeric', hour: "numeric", minute: "numeric"})
embed.setFooter({ text: footertimestamp, iconURL: '' });
try {
return gamedig.query({
type: config["server_type"],
host: config["server_host"],
port: config["server_port"],
maxAttempts: 5,
socketTimeout: 1000,
debug: false
}).then((state) => {
//-----------------------------------------------------------------------------------------------
// soulkobk edit 20220403 - updated 'players' keys to { rank, name, time, score } for use with dataKeys
let oldplayers = state.players;
delete state["players"];
Object.assign(state, {players: []});
for (let p = 0; p < oldplayers.length; p++) {
var playername = oldplayers[p].name;
var playerscore = oldplayers[p].raw.score;
var playertime = oldplayers[p].raw.time;
if (playername) {
let zero = p + 1 > 9 ? p + 1 : "0" + (p + 1);
let rank = p < 10 ? zero : p;
state.players.push({ rank: `${rank}`, name: `${playername}`, time: `${playertime}`, score: `${playerscore}` });
};
};
//-----------------------------------------------------------------------------------------------
// set embed color
embed.setColor(config["server_color"]);
//-----------------------------------------------------------------------------------------------
// set server name
let serverName = state.name.toUpperCase();
// refactor server name
for (let i = 0; i < serverName.length; i++) {
if (serverName[i] == "^") {
serverName = serverName.slice(0, i) + " " + serverName.slice(i+2);
} else if (serverName[i] == "█") {
serverName = serverName.slice(0, i) + " " + serverName.slice(i+1);
} else if (serverName[i] == "<22>") {
serverName = serverName.slice(0, i) + " " + serverName.slice(i+2);
};
};
serverName = serverName.substring(0,45) + "...";
let stringlength = serverName.length;
let stringpadding = ((45 - stringlength) / 2 );
serverName = serverName.padStart((stringlength + stringpadding), '');
serverName = (serverName.padEnd(stringlength + (stringpadding * 2),''));
embed.setTitle(serverName);
//-----------------------------------------------------------------------------------------------
// basic server info
if (config["server_enable_headers"]) {
embed.addField('\u200B', '` SERVER DETAILS `');
};
embed.addField("Status" + ' :', "🟢 " + "Online", true);
embed.addField("Direct Connect" + ' :', state.connect, true);
embed.addField("Location" + ' :', `:flag_${config["server_country"].toLowerCase()}:`, true);
embed.addField("Game Mode" + ' :', config["server_type"].charAt(0).toUpperCase() + config["server_type"].slice(1) , true);
if (state.map == "") {
embed.addField("\u200B", "\u200B", true);
} else {
embed.addField("Map" + ' :', state.map.charAt(0).toUpperCase() + state.map.slice(1), true);
};
embed.addField("Online Players" + ' :', state.players.length + " / " + state.maxplayers, true);
//-----------------------------------------------------------------------------------------------
// player list
if (config["server_enable_playerlist"] && state.players.length > 0) {
if (config["server_enable_headers"]) {
embed.addField('\u200B', '` PLAYER LIST `');
};
// recover game data
let dataKeys = Object.keys(state.players[0]);
// remove some unwanted data
dataKeys = dataKeys.filter(e =>
e !== 'frags' &&
e !== 'guid' &&
e !== 'id' &&
e !== 'team' &&
e !== 'squad' &&
e !== 'raw' &&
e !== 'skin'
);
if (!config["server_enable_rank"]) {
dataKeys = dataKeys.filter(e =>
e !== 'rank'
);
};
if (!config["server_enable_score"]) {
dataKeys = dataKeys.filter(e =>
e !== 'score'
);
};
for (let j = 0; j < dataKeys.length; j++) {
// check if data key empty
if (dataKeys[j] == "") {
dataKeys[j] = "\u200B";
};
let player_datas = "```\n";
for (let i = 0; i < state.players.length; i++) {
// break if too many players, prevent discord message overflood
if (i + 1 > 50) {
player_datas += "...";
break;
};
// set player data
if (state.players[i][dataKeys[j]] != undefined) {
let player_data = state.players[i][dataKeys[j]].toString();
if (player_data == "") {
player_data = "-";
};
// handle discord markdown strings
player_data = player_data.replace(/_/g, " ");
for (let k = 0; k < player_data.length; k++) {
if (player_data[k] == "^") {
player_data = player_data.slice(0, k) + " " + player_data.slice(k+2);
};
};
// time duration on server
if (dataKeys[j] == "time") {
let date = new Date(state.players[i].time * 1000).toISOString().substr(11,8);
player_datas += date;
} else {
// handle very long strings
player_data = (player_data.length > 16) ? player_data.substring(0, 16 - 3) + "..." : player_data;
if (config["server_enable_numbers"]) {
let index = i + 1 > 9 ? i + 1 : "0" + (i + 1);
player_datas += j == 0 ? index + " - " + player_data : player_data;
} else {
player_datas += player_data;
};
if (dataKeys[j] == "ping") player_datas += " ms";
};
};
player_datas += "\n";
};
player_datas += "```";
dataKeys[j] = dataKeys[j].charAt(0).toUpperCase() + dataKeys[j].slice(1);
embed.addField(dataKeys[j] + ' :', player_datas, true);
};
};
// set bot activity
client.user.setActivity("🟢 Online: " + state.players.length + "/" + state.maxplayers, { type: 'WATCHING' });
// add graph data
graphDataPush(updatedTime, state.players.length);
// set graph image
if (config["server_enable_graph"]) {
if (config["server_enable_headers"]) {
embed.addField('\u200B', '` PLAYER GRAPH `');
};
embed.setImage(
"http://" + config["webServerHost"] + ":" + config["webServerPort"] + "/" + 'graph_' + instanceId + '.png' + "?id=" + Date.now()
);
};
return embed;
}).catch(function(error) {
// set bot activity
client.user.setActivity("🔴 Offline.", { type: 'WATCHING' });
// offline status message
embed.setColor('#ff0000');
embed.setTitle('🔴 ' + "Server Offline" + '.');
// add graph data
graphDataPush(updatedTime, 0);
return embed;
});
} catch (error) {
console.log(error);
// set bot activity
client.user.setActivity("🔴 Offline.", { type: 'WATCHING' });
// offline status message
embed.setColor('#ff0000');
embed.setTitle('🔴 ' + "Server Offline" + '.');
// add graph data
graphDataPush(updatedTime, 0);
return embed;
};
};
function graphDataPush(updatedTime, nbrPlayers) {
// save data to json file
fs.readFile(__dirname + '/temp/data/serverData_' + instanceId + '.json', function (err, data) {
// create file if does not exist
if (err) {
fs.writeFile(__dirname + '/temp/data/serverData_' + instanceId + '.json', JSON.stringify([]),function(err){if (err) throw err;});
return;
};
let json;
// read old data and concat new data
try {
json = JSON.parse(data);
} catch (err) {
console.log("error on graph data")
console.error(err)
json = JSON.parse("[]");
};
// 1 day history
let nbrMuchData = json.length - 24 * 60 * 60 / config["statusUpdateTime"];
if (nbrMuchData > 0) {
json.splice(0, nbrMuchData);
};
json.push({"x": updatedTime, "y": nbrPlayers});
// rewrite data file
fs.writeFile(__dirname + '/temp/data/serverData_' + instanceId + '.json', JSON.stringify(json), function(err){});
});
};
const width = 800;
const height = 400;
const { ChartJSNodeCanvas } = require('chartjs-node-canvas');
var canvasRenderService = new ChartJSNodeCanvas({width, height});
async function generateGraph() {
while(true){
try {
// generate graph
let data = [];
try {
data = JSON.parse(fs.readFileSync(__dirname + '/temp/data/serverData_' + instanceId + '.json', {encoding:'utf8', flag:'r'}));
} catch (error) {
data = [];
}
let graph_labels = [];
let graph_datas = [];
// set data
for (let i = 0; i < data.length; i += 1) {
graph_labels.push(new Date(data[i]["x"]));
graph_datas.push(data[i]["y"]);
};
let graphConfig = {
type: 'line',
data: {
labels: graph_labels,
datasets: [{
label: 'number of players',
data: graph_datas,
pointRadius: 0,
backgroundColor: hexToRgb(config["server_color"], 0.2),
borderColor: hexToRgb(config["server_color"], 1.0),
borderWidth: 1
}]
},
options: {
downsample: {
enabled: true,
threshold: 500 // max number of points to display per dataset
},
legend: {
display: true,
labels: {
fontColor: 'white'
}
},
scales: {
yAxes: [{
ticks: {
fontColor: 'rgba(255,255,255,1)',
precision: 0,
beginAtZero: true
},
gridLines: {
zeroLineColor: 'rgba(255,255,255,1)',
zeroLineWidth: 0,
color: 'rgba(255,255,255,0.2)',
lineWidth: 0.5
}
}],
xAxes: [{
type: 'time',
ticks: {
fontColor: 'rgba(255,255,255,1)',
autoSkip: true,
maxTicksLimit: 10
},
time: {
displayFormats: {
quarter: 'h a'
}
},
gridLines: {
zeroLineColor: 'rgba(255,255,255,1)',
zeroLineWidth: 0,
color: 'rgba(255,255,255,0.2)',
lineWidth: 0.5
}
}]
},
datasets: {
normalized: true,
line: {
pointRadius: 0
}
},
elements: {
point: {
radius: 0
},
line: {
tension: 0
}
},
animation: {
duration: 0
},
responsiveAnimationDuration: 0,
hover: {
animationDuration: 0
}
}
};
let graphFile = 'graph_' + instanceId + '.png';
canvasRenderService.renderToBuffer(graphConfig).then(data => {
fs.writeFileSync(__dirname + '/temp/graphs/' + graphFile, data);
}).catch(function(error) {
console.error("graph creation for guild " + instanceId + " failed.");
console.error(error);
});
} catch (error) {
console.error(error);
process.send({
instanceid : instanceId,
message : "could not generate graph image " + error
});
};
await Sleep(60 * 1000); // every minute
};
};
// does what its name says
function hexToRgb(hex, opacity) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? "rgba(" + parseInt(result[1], 16) + ", " + parseInt(result[2], 16) + ", " + parseInt(result[3], 16) + ", " + opacity + ")" : null;
}

81
src/discord-bot.js Normal file
View File

@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.serverUpdate = exports.init = void 0;
const discord_js_1 = require("discord.js");
//https://github.com/soulkobk/DiscordBot_GameStatus
const prefix = '@gsw';
let client;
function init(token) {
client = new discord_js_1.Client({
//messageEditHistoryMaxSize: 0,
//ws: {intents: ['GUILDS', 'GUILD_MESSAGES']}
});
console.log(' dc.init'); //DEBUG
client.on('ready', () => {
console.log(' dc.client.READY'); //DEBUG
});
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
// client.on('messageCreate', (message) => {
// console.log('!!!!!dc.messageCreate', message.author, message.content);//DEBUG
// if (message.author.bot) return;
// if (!message.content.startsWith(prefix)) return;
// const commandBody = message.content.slice(prefix.length);
// const args = commandBody.split(' ');
// const command = String(args.shift()).toLowerCase();
// if (command === 'ping') {
// const timeTaken = Date.now() - message.createdTimestamp;
// message.reply(`Pong! ${timeTaken}ms`);
// }
// });
return client.login(token);
}
exports.init = init;
async function serverUpdate(gs) {
if (!gs.info)
return;
console.log('discord.serverUpdate', gs.config.host, gs.config.port, gs.config.discord);
/*
for (const cid of gs.config.discord.chatIds) {
let m = await getServerInfoMessage(cid, gs.config.host, gs.config.port);
const stats = gs.history.stats();
let statsText = '';
if (stats.length > 0) {
const s = stats.pop();
if (s) {
statsText = ' (hourly max: ' + s.max + ', hourly avg: ' + s.avg.toFixed(1) + ')';
}
}
const infoText: string[] = [
gs.niceName,
gs.info.game + ' / ' + gs.info.map,
gs.info.connect,
'Players ' + gs.info.playersNum + '/' + gs.info.playersMax + statsText
];
if (gs.info?.players.length > 0) {
infoText.push('```');
for(const p of gs.info?.players) {
let playerLine = '';
if (p.raw?.time !== undefined) {
playerLine += '[' + hhmmss(p.raw.time) + '] ';
}
playerLine += p.name;
if (p.raw?.score !== undefined) {
playerLine += ' [score: ' + p.raw.score + ']';
}
infoText.push(playerLine);
}
infoText.push('```');
}
m.setText(infoText.join('\n'));
}
*/
}
exports.serverUpdate = serverUpdate;

89
src/discord-bot.ts Normal file
View File

@@ -0,0 +1,89 @@
import {Client, MessageEmbed, Intents} from 'discord.js';
import { GameServer } from './game-server';
//https://github.com/soulkobk/DiscordBot_GameStatus
const prefix = '@gsw';
let client: Client;
export function init(token: string) {
client = new Client({
//messageEditHistoryMaxSize: 0,
//ws: {intents: ['GUILDS', 'GUILD_MESSAGES']}
});
console.log(' dc.init');//DEBUG
client.on('ready', ()=> {
console.log(' dc.client.READY');//DEBUG
})
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
// client.on('messageCreate', (message) => {
// console.log('!!!!!dc.messageCreate', message.author, message.content);//DEBUG
// if (message.author.bot) return;
// if (!message.content.startsWith(prefix)) return;
// const commandBody = message.content.slice(prefix.length);
// const args = commandBody.split(' ');
// const command = String(args.shift()).toLowerCase();
// if (command === 'ping') {
// const timeTaken = Date.now() - message.createdTimestamp;
// message.reply(`Pong! ${timeTaken}ms`);
// }
// });
return client.login(token);
}
export async function serverUpdate(gs: GameServer) {
if (!gs.info) return;
console.log('discord.serverUpdate', gs.config.host, gs.config.port, gs.config.discord);
/*
for (const cid of gs.config.discord.chatIds) {
let m = await getServerInfoMessage(cid, gs.config.host, gs.config.port);
const stats = gs.history.stats();
let statsText = '';
if (stats.length > 0) {
const s = stats.pop();
if (s) {
statsText = ' (hourly max: ' + s.max + ', hourly avg: ' + s.avg.toFixed(1) + ')';
}
}
const infoText: string[] = [
gs.niceName,
gs.info.game + ' / ' + gs.info.map,
gs.info.connect,
'Players ' + gs.info.playersNum + '/' + gs.info.playersMax + statsText
];
if (gs.info?.players.length > 0) {
infoText.push('```');
for(const p of gs.info?.players) {
let playerLine = '';
if (p.raw?.time !== undefined) {
playerLine += '[' + hhmmss(p.raw.time) + '] ';
}
playerLine += p.name;
if (p.raw?.score !== undefined) {
playerLine += ' [score: ' + p.raw.score + ']';
}
infoText.push(playerLine);
}
infoText.push('```');
}
m.setText(infoText.join('\n'));
}
*/
}

179
src/game-server.js Normal file
View File

@@ -0,0 +1,179 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameServer = exports.saveDb = exports.initDb = void 0;
const got_1 = __importDefault(require("got"));
const gamedig_1 = require("gamedig");
const lowdb_1 = require("@commonify/lowdb");
const ipregex_1 = __importDefault(require("./lib/ipregex"));
const getip_1 = __importDefault(require("./lib/getip"));
const STEAM_WEB_API_KEY = process.env.STEAM_WEB_API_KEY || '';
const PLAYERS_HISTORY_HOURS = parseInt(process.env.PLAYERS_HISTORY_HOURS || '10', 10);
const DATA_PATH = process.env.DATA_PATH || './data/';
const adapter = new lowdb_1.JSONFile(DATA_PATH + 'servers.json');
const db = new lowdb_1.Low(adapter);
async function initDb() {
await db.read();
db.data = db.data || {
population: {}
};
}
exports.initDb = initDb;
function saveDb() {
return db.write();
}
exports.saveDb = saveDb;
class GameServer {
constructor(config) {
console.log('game-server.init', config.host, config.port, config.type, config.appId);
this.config = config;
this.history = new ServerHistory(config.host + ':' + config.port);
}
async update() {
let info;
info = await this.gamedig();
if (!info && STEAM_WEB_API_KEY) {
info = await this.steam();
}
if (info) {
this.info = info;
this.history.add(info);
}
else {
console.error('game-server.update no info!');
}
return;
}
async gamedig() {
try {
const res = await (0, gamedig_1.query)({
host: this.config.host,
port: this.config.port,
type: this.config.type,
});
const raw = res.raw;
const game = raw.game || raw.folder || this.config.type;
const players = res.players; //todo: map / filter
return {
connect: res.connect,
name: res.name,
game: game,
map: res.map,
playersNum: res.numplayers || res.players.length,
playersMax: res.maxplayers,
players
};
}
catch (e) {
console.error(e.message || e);
}
return null;
}
async steam() {
if (!this.ip) {
if (ipregex_1.default.test(this.config.host)) {
this.ip = this.config.host;
}
else {
this.ip = await (0, getip_1.default)(this.config.host);
if (!this.ip) {
return null;
}
}
}
const reqUrl = 'https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\\appid\\' + this.config.appId + '\\addr\\' + this.ip + '&key=' + STEAM_WEB_API_KEY;
try {
const res = await (0, got_1.default)(reqUrl, {
responseType: 'json',
headers: { 'user-agent': 'game-server-watcher/1.0' }
}).json();
if (Array.isArray(res.response.servers)) {
const matching = res.response.servers.find((s) => s.gameport === this.config.port);
if (matching) {
return {
connect: matching.addr,
name: matching.name,
game: matching.gamedir,
map: matching.map,
playersNum: matching.players,
playersMax: matching.max_players,
players: []
};
}
}
}
catch (e) {
console.error(e.message || e);
}
return null;
}
get niceName() {
var _a;
let nn = ((_a = this.info) === null || _a === void 0 ? void 0 : _a.name) || '';
for (let i = 0; i < nn.length; i++) {
if (nn[i] == '^') {
nn = nn.slice(0, i) + ' ' + nn.slice(i + 2);
}
else if (nn[i] == '█') {
nn = nn.slice(0, i) + ' ' + nn.slice(i + 1);
}
else if (nn[i] == '<27>') {
nn = nn.slice(0, i) + ' ' + nn.slice(i + 2);
}
;
}
;
return nn;
}
}
exports.GameServer = GameServer;
class ServerHistory {
constructor(id) {
this.id = id;
}
yyyymmddhh(d) {
return parseInt(d.toISOString().slice(0, 13).replace(/\D/g, ''), 10);
}
add(info) {
var _a;
if (!((_a = db.data) === null || _a === void 0 ? void 0 : _a.population))
return;
const d = new Date();
const dh = this.yyyymmddhh(d);
if (!db.data.population[this.id]) {
db.data.population[this.id] = [];
}
db.data.population[this.id].push({
dateHour: dh,
playersNum: info.playersNum
});
d.setHours(d.getHours() - PLAYERS_HISTORY_HOURS);
const minDh = this.yyyymmddhh(d);
db.data.population[this.id] = db.data.population[this.id].filter(i => i.dateHour > minDh);
}
stats() {
var _a;
if (!((_a = db.data) === null || _a === void 0 ? void 0 : _a.population))
return [];
const grouped = {};
for (const d of db.data.population[this.id]) {
if (!grouped[d.dateHour]) {
grouped[d.dateHour] = [];
}
grouped[d.dateHour].push(d);
}
const stats = [];
for (const dh in grouped) {
const avg = grouped[dh].reduce((total, next) => total + next.playersNum, 0) / grouped[dh].length;
const max = grouped[dh].reduce((max, next) => next.playersNum > max ? next.playersNum : max, 0);
stats.push({
dateHour: parseInt(dh, 10),
avg,
max
});
}
return stats.sort((a, b) => a.dateHour - b.dateHour);
}
}

241
src/game-server.ts Normal file
View File

@@ -0,0 +1,241 @@
import got from 'got';
import { Player, query, QueryResult } from 'gamedig';
import { Low, JSONFile } from '@commonify/lowdb';
import ipRegex from './lib/ipregex';
import getIP from './lib/getip';
import { WatcherConfig } from './watcher';
const STEAM_WEB_API_KEY = process.env.STEAM_WEB_API_KEY || '';
const PLAYERS_HISTORY_HOURS = parseInt(process.env.PLAYERS_HISTORY_HOURS || '10', 10);
const DATA_PATH = process.env.DATA_PATH || './data/';
interface GameServerDb {
population: {
[x: string]: Population[];
}
}
const adapter = new JSONFile<GameServerDb>(DATA_PATH + 'servers.json');
const db = new Low<GameServerDb>(adapter);
export async function initDb() {
await db.read();
db.data = db.data || {
population: {}
};
}
export function saveDb() {
return db.write();
}
interface gsPlayer extends Player {
raw?: {
[key: string]: any;
};
}
export interface Info {
connect: string;
name: string;
game: string;
map: string;
playersNum: number;
playersMax: number;
players: gsPlayer[];
}
interface qRes extends QueryResult {
game: string;
numplayers: number;
}
export class GameServer {
public ip?: string;
public info?: Info;
public config: WatcherConfig;
public history: ServerHistory;
constructor(config: WatcherConfig) {
console.log('game-server.init', config.host, config.port, config.type, config.appId);
this.config = config;
this.history = new ServerHistory(config.host + ':' + config.port);
}
async update() {
let info: Info | null;
info = await this.gamedig();
if (!info && STEAM_WEB_API_KEY) {
info = await this.steam();
}
if (info) {
this.info = info;
this.history.add(info);
} else {
console.error('game-server.update no info!');
}
return;
}
async gamedig(): Promise<Info | null> {
try {
const res = await query({
host: this.config.host,
port: this.config.port,
type: this.config.type,
}) as qRes;
const raw = res.raw as { game: string; folder: string;};
const game = raw.game || raw.folder || this.config.type;
const players: Player[] = res.players;//todo: map / filter
return {
connect: res.connect,
name: res.name,
game: game,
map: res.map,
playersNum: res.numplayers || res.players.length,
playersMax: res.maxplayers,
players
};
} catch (e: any) {
console.error(e.message || e);
}
return null;
}
async steam(): Promise<Info | null> {
if (!this.ip) {
if (ipRegex.test(this.config.host)) {
this.ip = this.config.host;
} else {
this.ip = await getIP(this.config.host);
if (!this.ip) {
return null;
}
}
}
const reqUrl = 'https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\\appid\\' + this.config.appId + '\\addr\\' + this.ip + '&key=' + STEAM_WEB_API_KEY;
try {
const res: any = await got(reqUrl, {
responseType: 'json',
headers: { 'user-agent': 'game-server-watcher/1.0' }
}).json();
if (Array.isArray(res.response.servers)) {
const matching = res.response.servers.find((s: any) => s.gameport === this.config.port);
if (matching) {
return {
connect: matching.addr,
name: matching.name,
game: matching.gamedir,
map: matching.map,
playersNum: matching.players,
playersMax: matching.max_players,
players: []
}
}
}
} catch (e: any) {
console.error(e.message || e);
}
return null;
}
get niceName() {
let nn = this.info?.name || '';
for (let i = 0; i < nn.length; i++) {
if (nn[i] == '^') {
nn = nn.slice(0, i) + ' ' + nn.slice(i+2);
} else if (nn[i] == '█') {
nn = nn.slice(0, i) + ' ' + nn.slice(i+1);
} else if (nn[i] == '<27>') {
nn = nn.slice(0, i) + ' ' + nn.slice(i+2);
};
};
return nn;
}
}
interface Population {
dateHour: number;
playersNum: number;
}
interface GroupedPopulation {
[x: number]: Population[];
}
interface Stat {
dateHour: number;
avg: number;
max: number;
}
class ServerHistory {
public id: string;
constructor(id: string) {
this.id = id;
}
yyyymmddhh(d: Date): number {
return parseInt(d.toISOString().slice(0, 13).replace(/\D/g, ''), 10);
}
add(info: Info) {
if (!db.data?.population) return;
const d = new Date();
const dh = this.yyyymmddhh(d);
if (!db.data.population[this.id]) {
db.data.population[this.id] = [];
}
db.data.population[this.id].push({
dateHour: dh,
playersNum: info.playersNum
});
d.setHours(d.getHours() - PLAYERS_HISTORY_HOURS);
const minDh = this.yyyymmddhh(d);
db.data.population[this.id] = db.data.population[this.id].filter(i => i.dateHour > minDh);
}
stats() {
if (!db.data?.population) return [];
const grouped: GroupedPopulation = {};
for (const d of db.data.population[this.id]) {
if (!grouped[d.dateHour]) {
grouped[d.dateHour] = [];
}
grouped[d.dateHour].push(d);
}
const stats: Stat[] = [];
for (const dh in grouped) {
const avg = grouped[dh].reduce((total, next) => total + next.playersNum, 0) / grouped[dh].length;
const max = grouped[dh].reduce((max, next) => next.playersNum > max ? next.playersNum : max, 0);
stats.push({
dateHour: parseInt(dh, 10),
avg,
max
});
}
return stats.sort((a, b) => a.dateHour - b.dateHour);
}
}

6
src/lib/charturl.js Normal file
View File

@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = (gs) => {
//https://image-charts.com/chart
return 'str.chart.url.todo';
};

4
src/lib/charturl.ts Normal file
View File

@@ -0,0 +1,4 @@
export default (gs: any): string => {
//https://image-charts.com/chart
return 'str.chart.url.todo';
}

14
src/lib/getip.js Normal file
View File

@@ -0,0 +1,14 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dns_1 = __importDefault(require("dns"));
async function getIP(hostname) {
let obj = await dns_1.default.promises.lookup(hostname)
.catch((e) => {
console.error(e.message || e);
});
return obj === null || obj === void 0 ? void 0 : obj.address;
}
exports.default = getIP;

11
src/lib/getip.ts Normal file
View File

@@ -0,0 +1,11 @@
import dns from 'dns';
export default async function getIP(hostname: string) {
let obj = await dns.promises.lookup(hostname)
.catch((e: any) => {
console.error(e.message || e);
});
return obj?.address;
}

21
src/lib/hhmmss.js Normal file
View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = (sec_num) => {
let hours = Math.floor(sec_num / 3600);
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
let secs = Math.floor(sec_num - (hours * 3600) - (minutes * 60));
//if (hours < 10) {hours = `0${hours}`;}
if (hours == 0) {
hours = '';
}
else {
hours = `${hours}:`;
}
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (secs < 10) {
secs = `0${secs}`;
}
return [hours, minutes, ':', secs].join('');
};

12
src/lib/hhmmss.ts Normal file
View File

@@ -0,0 +1,12 @@
export default (sec_num: number) => {
let hours: number | string = Math.floor(sec_num / 3600);
let minutes: number | string = Math.floor((sec_num - (hours * 3600)) / 60);
let secs: number | string = Math.floor(sec_num - (hours * 3600) - (minutes * 60));
//if (hours < 10) {hours = `0${hours}`;}
if (hours == 0) {hours = '';}
else {hours = `${hours}:`;}
if (minutes < 10) { minutes = `0${minutes}`; }
if (secs < 10) { secs = `0${secs}`; }
return [hours, minutes, ':', secs].join('');
}

19
src/lib/ipregex.js Normal file
View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const v4 = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}';
const v6segment = '[a-fA-F\\d]{1,4}';
const v6 = `
(?:
(?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
(?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
(?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
(?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
(?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
(?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
(?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
(?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
)(?:%[0-9a-zA-Z]{1,})? // %eth0 %1
`.replace(/\s*\/\/.*$/gm, '').replace(/\n/g, '').trim();
// Pre-compile only the exact regexes because adding a global flag make regexes stateful
const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
exports.default = v46Exact;

21
src/lib/ipregex.ts Normal file
View File

@@ -0,0 +1,21 @@
const v4 = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}';
const v6segment = '[a-fA-F\\d]{1,4}';
const v6 = `
(?:
(?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
(?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
(?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
(?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
(?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
(?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
(?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
(?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
)(?:%[0-9a-zA-Z]{1,})? // %eth0 %1
`.replace(/\s*\/\/.*$/gm, '').replace(/\n/g, '').trim();
// Pre-compile only the exact regexes because adding a global flag make regexes stateful
const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
export default v46Exact;

130
src/telegram-bot.js Normal file
View File

@@ -0,0 +1,130 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.serverUpdate = exports.init = void 0;
const grammy_1 = require("grammy");
const hhmmss_1 = __importDefault(require("./lib/hhmmss"));
const lowdb_1 = require("@commonify/lowdb");
const DATA_PATH = process.env.DATA_PATH || './data/';
const adapter = new lowdb_1.JSONFile(DATA_PATH + 'telegram.json');
const db = new lowdb_1.Low(adapter);
const serverInfoMessages = [];
let bot;
async function init(token) {
console.log('telegram-bot starting...');
bot = new grammy_1.Bot(token);
const me = await bot.api.getMe();
console.log('telegram-bot ready', me);
// bot.on('message:text', ctx => {ctx.reply('echo: ' + ctx.message.text);});
// bot.command('start', ctx => ctx.reply('cmd.start.response'));
// bot.start();
await db.read();
db.data = db.data || [];
return;
}
exports.init = init;
async function getServerInfoMessage(cid, host, port) {
let m = serverInfoMessages.find(n => {
return n.chatId === cid && n.host === host && n.port === port;
});
if (!m) {
m = new ServerInfoMessage(cid, host, port);
let msgId;
if (db.data) {
const md = db.data.find(d => {
return d.chatId === cid && d.host === host && d.port === port;
});
if (md) {
msgId = md.messageId;
}
}
await m.init(msgId);
serverInfoMessages.push(m);
}
return m;
}
async function serverUpdate(gs) {
var _a, _b, _c, _d;
if (!gs.info)
return;
console.log('telegram.serverUpdate', gs.config.host, gs.config.port, gs.config.telegram);
for (const cid of gs.config.telegram.chatIds) {
let m = await getServerInfoMessage(cid, gs.config.host, gs.config.port);
const stats = gs.history.stats();
let statsText = '';
if (stats.length > 0) {
const s = stats.pop();
if (s) {
statsText = ' (hourly max: ' + s.max + ', hourly avg: ' + s.avg.toFixed(1) + ')';
}
}
const infoText = [
gs.niceName,
gs.info.game + ' / ' + gs.info.map,
gs.info.connect,
'Players ' + gs.info.playersNum + '/' + gs.info.playersMax + statsText
];
if (((_a = gs.info) === null || _a === void 0 ? void 0 : _a.players.length) > 0) {
infoText.push('```');
for (const p of (_b = gs.info) === null || _b === void 0 ? void 0 : _b.players) {
let playerLine = '';
if (((_c = p.raw) === null || _c === void 0 ? void 0 : _c.time) !== undefined) {
playerLine += '[' + (0, hhmmss_1.default)(p.raw.time) + '] ';
}
playerLine += p.name;
if (((_d = p.raw) === null || _d === void 0 ? void 0 : _d.score) !== undefined) {
playerLine += ' [score: ' + p.raw.score + ']';
}
infoText.push(playerLine);
}
infoText.push('```');
}
m.setText(infoText.join('\n'));
}
}
exports.serverUpdate = serverUpdate;
class ServerInfoMessage {
constructor(chatId, host, port) {
this.messageId = 0;
this.chatId = chatId;
this.host = host;
this.port = port;
}
async init(msgId) {
if (msgId) {
this.messageId = msgId;
}
else {
const msg = await bot.api.sendMessage(this.chatId, 'Initializing server info...');
this.messageId = msg.message_id;
}
if (db.data) {
const mi = db.data.findIndex(d => {
return d.chatId === this.chatId && d.host === this.host && d.port === this.port;
});
if (mi === -1 || mi === undefined) {
db.data.push({
chatId: this.chatId,
host: this.host,
port: this.port,
messageId: this.messageId
});
}
else {
db.data[mi].messageId = this.messageId;
}
await db.write();
}
}
async setText(text) {
console.log('setText', this.host, this.port);
try {
await bot.api.editMessageText(this.chatId, this.messageId, text, { parse_mode: 'Markdown' });
}
catch (e) {
console.log(e.message || e);
}
}
}

156
src/telegram-bot.ts Normal file
View File

@@ -0,0 +1,156 @@
import { Bot } from 'grammy';
import { GameServer } from './game-server';
import hhmmss from './lib/hhmmss';
import { Low, JSONFile } from '@commonify/lowdb';
const DATA_PATH = process.env.DATA_PATH || './data/';
interface TelegramData {
chatId: string;
host: string;
port: number;
messageId: number;
}
const adapter = new JSONFile<TelegramData[]>(DATA_PATH + 'telegram.json');
const db = new Low<TelegramData[]>(adapter);
const serverInfoMessages: ServerInfoMessage[] = [];
let bot: Bot;
export async function init(token: string) {
console.log('telegram-bot starting...');
bot = new Bot(token);
const me = await bot.api.getMe();
console.log('telegram-bot ready', me);
// bot.on('message:text', ctx => {ctx.reply('echo: ' + ctx.message.text);});
// bot.command('start', ctx => ctx.reply('cmd.start.response'));
// bot.start();
await db.read();
db.data = db.data || [];
return;
}
async function getServerInfoMessage(cid: string, host: string, port: number) {
let m = serverInfoMessages.find(n => {
return n.chatId === cid && n.host === host && n.port === port;
});
if (!m) {
m = new ServerInfoMessage(cid, host, port);
let msgId;
if (db.data) {
const md = db.data.find(d => {
return d.chatId === cid && d.host === host && d.port === port;
});
if (md) {
msgId = md.messageId;
}
}
await m.init(msgId);
serverInfoMessages.push(m);
}
return m;
}
export async function serverUpdate(gs: GameServer) {
if (!gs.info) return;
console.log('telegram.serverUpdate', gs.config.host, gs.config.port, gs.config.telegram);
for (const cid of gs.config.telegram.chatIds) {
let m = await getServerInfoMessage(cid, gs.config.host, gs.config.port);
const stats = gs.history.stats();
let statsText = '';
if (stats.length > 0) {
const s = stats.pop();
if (s) {
statsText = ' (hourly max: ' + s.max + ', hourly avg: ' + s.avg.toFixed(1) + ')';
}
}
const infoText: string[] = [
gs.niceName,
gs.info.game + ' / ' + gs.info.map,
gs.info.connect,
'Players ' + gs.info.playersNum + '/' + gs.info.playersMax + statsText
];
if (gs.info?.players.length > 0) {
infoText.push('```');
for(const p of gs.info?.players) {
let playerLine = '';
if (p.raw?.time !== undefined) {
playerLine += '[' + hhmmss(p.raw.time) + '] ';
}
playerLine += p.name;
if (p.raw?.score !== undefined) {
playerLine += ' [score: ' + p.raw.score + ']';
}
infoText.push(playerLine);
}
infoText.push('```');
}
m.setText(infoText.join('\n'));
}
}
class ServerInfoMessage {
public chatId: string;
public host: string;
public port: number;
public messageId: number = 0;
constructor(chatId: string, host: string, port: number) {
this.chatId = chatId;
this.host = host;
this.port = port;
}
async init(msgId?: number) {
if (msgId) {
this.messageId = msgId;
} else {
const msg = await bot.api.sendMessage(this.chatId, 'Initializing server info...');
this.messageId = msg.message_id;
}
if (db.data) {
const mi = db.data.findIndex(d => {
return d.chatId === this.chatId && d.host === this.host && d.port === this.port;
});
if (mi === -1 || mi === undefined) {
db.data.push({
chatId: this.chatId,
host: this.host,
port: this.port,
messageId: this.messageId
});
} else {
db.data[mi].messageId = this.messageId;
}
await db.write();
}
}
async setText(text: string) {
console.log('setText', this.host, this.port);
try {
await bot.api.editMessageText(this.chatId, this.messageId, text, {parse_mode: 'Markdown'});
} catch (e: any) {
console.log(e.message || e);
}
}
}

83
src/watcher.js Normal file
View File

@@ -0,0 +1,83 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = void 0;
const game_server_1 = require("./game-server");
const discordBot = __importStar(require("./discord-bot"));
const telegramBot = __importStar(require("./telegram-bot"));
const fs_1 = require("fs");
const { readFile } = fs_1.promises;
const REFRESH_TIME_MINUTES = parseInt(process.env.REFRESH_TIME_MINUTES || '1', 10);
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
const GSW_CONFIG = process.env.GSW_CONFIG || './config/default.config.json';
class Watcher {
constructor() {
this.servers = [];
}
async init(config) {
console.log('watcher starting...');
if (DISCORD_BOT_TOKEN) {
await discordBot.init(DISCORD_BOT_TOKEN);
}
if (TELEGRAM_BOT_TOKEN) {
await telegramBot.init(TELEGRAM_BOT_TOKEN);
}
await (0, game_server_1.initDb)();
for (const c of config) {
const gs = new game_server_1.GameServer(c);
this.servers.push(gs);
}
}
check() {
console.log('watcher checking...');
const promises = [];
for (const gs of this.servers) {
promises.push(gs.update().then(() => {
if (DISCORD_BOT_TOKEN) {
discordBot.serverUpdate(gs);
}
if (TELEGRAM_BOT_TOKEN) {
telegramBot.serverUpdate(gs);
}
}));
}
return Promise.allSettled(promises).then(() => (0, game_server_1.saveDb)());
}
}
let loop = null;
async function main() {
console.log('reading config...', GSW_CONFIG);
const buffer = await readFile(GSW_CONFIG);
console.log('buffer', buffer.toString());
try {
const conf = JSON.parse(buffer.toString());
const watcher = new Watcher();
await watcher.init(conf);
console.log('starting loop...', REFRESH_TIME_MINUTES);
loop = setInterval(async () => { await watcher.check(); }, 1000 * 60 * REFRESH_TIME_MINUTES);
await watcher.check();
} catch (e) {
console.error(e.message || e);
}
// return watcher;
}
exports.main = main;

81
src/watcher.ts Normal file
View File

@@ -0,0 +1,81 @@
import { Type } from 'gamedig';
import { GameServer, initDb, saveDb } from './game-server';
import * as discordBot from './discord-bot';
import * as telegramBot from './telegram-bot';
import { promises as fs } from 'fs';
const { readFile } = fs;
const REFRESH_TIME_MINUTES = parseInt(process.env.REFRESH_TIME_MINUTES || '1', 10);
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
const GSW_CONFIG = process.env.GSW_CONFIG || './config/default.config.json';
export interface WatcherConfig {
host: string;
port: number;
type: Type;
appId: number;
discord: {
channelIds: string[]
},
telegram: {
chatIds: string[];
}
}
class Watcher {
private servers: GameServer[] = [];
async init(config: WatcherConfig[]) {
console.log('watcher starting...');
if (DISCORD_BOT_TOKEN) {
await discordBot.init(DISCORD_BOT_TOKEN);
}
if (TELEGRAM_BOT_TOKEN) {
await telegramBot.init(TELEGRAM_BOT_TOKEN);
}
await initDb();
for (const c of config) {
const gs = new GameServer(c);
this.servers.push(gs);
}
}
check() {
console.log('watcher checking...');
const promises: Promise<void>[] = [];
for (const gs of this.servers) {
promises.push(gs.update().then(() => {
if (DISCORD_BOT_TOKEN) {
discordBot.serverUpdate(gs);
}
if (TELEGRAM_BOT_TOKEN) {
telegramBot.serverUpdate(gs);
}
}));
}
return Promise.allSettled(promises).then(() => saveDb());
}
}
let loop = null;
export async function main() {
console.log('reading config...', GSW_CONFIG);
const buffer = await readFile(GSW_CONFIG);
const conf = JSON.parse(buffer.toString());
const watcher = new Watcher();
await watcher.init(conf);
console.log('starting loop...', REFRESH_TIME_MINUTES);
loop = setInterval(async () => { await watcher.check() }, 1000 * 60 * REFRESH_TIME_MINUTES);
await watcher.check();
// return watcher;
}

View File

@@ -1,11 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["es2020"], "lib": [
"es2020"
],
"module": "commonjs", "module": "commonjs",
"target": "es2019", "target": "es2019",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true,
} "resolveJsonModule" : true
} },
"include": [
"src/**/*",
"server.ts",
],
"exclude": [
"node_modules"
]
}