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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.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",
|
"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": {
|
||||||
|
|||||||
28
server.js
28
server.js
@@ -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 💢</body></html>');
|
res.end('<html><head></head><body>404 💢</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)();
|
||||||
|
|||||||
29
server.ts
29
server.ts
@@ -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 💢</body></html>');
|
res.end('<html><head></head><body>404 💢</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
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": {
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user