mirror of
https://github.com/a-sync/game-server-watcher.git
synced 2026-03-31 06:33:44 -04:00
watcher, game-server + history, telegram-bot, discord-bot wip
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
data/servers.json
|
||||
data/discord.json
|
||||
data/telegram.json
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
30
README.md
30
README.md
@@ -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
15
config/bkz.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"host": "armahu.ddns.net",
|
||||
"port": 2332,
|
||||
"type": "arma3",
|
||||
"appId": 107410,
|
||||
"discord": {
|
||||
"channelIds": ["955162997410639962"]
|
||||
},
|
||||
"telegram": {
|
||||
"chatIds": ["325831302"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
26
config/default.config.json
Normal file
26
config/default.config.json
Normal 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
0
data/.gitkeep
Normal file
5
default.env
Normal file
5
default.env
Normal 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
1830
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -1,23 +1,29 @@
|
||||
{
|
||||
"name": "gsw",
|
||||
"name": "game-server-watcher",
|
||||
"version": "1.0.0",
|
||||
"description": "Game Server Watcher",
|
||||
"main": "server",
|
||||
"scripts": {
|
||||
"start": "tsc && node server.js",
|
||||
"dev": "tsc -w",
|
||||
"build": "tsc"
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"dev": "nodemon -e js --exec node server"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "a-sync@devs.space",
|
||||
"license": "ISC",
|
||||
"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": {
|
||||
"@types/gamedig": "^3.0.2",
|
||||
"@types/node": "12.20",
|
||||
"@types/utf8": "^3.0.1",
|
||||
"ts-node": "^10.7.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
28
server.js
28
server.js
@@ -2,18 +2,12 @@
|
||||
var __importDefault = (this && this.__importDefault) || function (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 http_1 = require("http");
|
||||
const url_1 = require("url");
|
||||
const SERVERS = [
|
||||
{
|
||||
type: 'arma3',
|
||||
host: '127.0.0.1',
|
||||
port: '2302',
|
||||
discordChannelId: '99988877700'
|
||||
}
|
||||
];
|
||||
require("dotenv/config");
|
||||
const watcher_1 = require("./src/watcher");
|
||||
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_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);
|
||||
}
|
||||
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 {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<html><head></head><body>404 💢</body></html>');
|
||||
}
|
||||
}).listen(APP_PORT);
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
(0, watcher_1.main)();
|
||||
|
||||
29
server.ts
29
server.ts
@@ -2,18 +2,14 @@ import fs from 'fs';
|
||||
import { createServer } from 'http';
|
||||
import { URL } from 'url';
|
||||
|
||||
const SERVERS = [
|
||||
{
|
||||
type: 'arma3',
|
||||
host: '127.0.0.1',
|
||||
port: '2302',
|
||||
discordChannelId: '99988877700'
|
||||
}
|
||||
];
|
||||
import 'dotenv/config';
|
||||
|
||||
import { main, WatcherConfig } from './src/watcher';
|
||||
|
||||
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_PORT = parseInt(process.env.app_port || '8080', 10);
|
||||
|
||||
const DBG = Boolean(process.env.DBG || false);
|
||||
const SECRET = process.env.SECRET || 'secret';
|
||||
|
||||
@@ -28,6 +24,21 @@ createServer(async (req, 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 {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<html><head></head><body>404 💢</body></html>');
|
||||
@@ -35,3 +46,5 @@ createServer(async (req, res) => {
|
||||
}).listen(APP_PORT);
|
||||
|
||||
console.log('Web service started %s:%s', APP_HOST, APP_PORT);
|
||||
|
||||
main();
|
||||
|
||||
613
src/bot.js
Normal file
613
src/bot.js
Normal 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
81
src/discord-bot.js
Normal 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
89
src/discord-bot.ts
Normal 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
179
src/game-server.js
Normal 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
241
src/game-server.ts
Normal 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
6
src/lib/charturl.js
Normal 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
4
src/lib/charturl.ts
Normal 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
14
src/lib/getip.js
Normal 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
11
src/lib/getip.ts
Normal 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
21
src/lib/hhmmss.js
Normal 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
12
src/lib/hhmmss.ts
Normal 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
19
src/lib/ipregex.js
Normal 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
21
src/lib/ipregex.ts
Normal 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
130
src/telegram-bot.js
Normal 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
156
src/telegram-bot.ts
Normal 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
83
src/watcher.js
Normal 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
81
src/watcher.ts
Normal 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;
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2020"],
|
||||
"lib": [
|
||||
"es2020"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule" : true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"server.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user