diff --git a/README.md b/README.md index fc35504..9185f58 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,44 @@ -# game-server-watcher -Requirements: [node.js](https://nodejs.org/) _(version 12.20.0 or later)_ +# Game Server Watcher +A simple discord/telegram bot that can be hosted on a free service to monitor your game servers and players in style. 😎 -# Configuration -//TODO: env vars (`.env` file supported); check [default.env](./default.env) -//TODO: [config files](./config/default.config.json) schema description +## Table of Contents +1. [About the Project](#about-the-project) +1. [Project Status](#project-status) +1. [Getting Started](#getting-started) + 1. [Requirements](#requirements) + 1. [Installation](#installation) + 1. [Usage](#usage) + 1. [Configuration](#configuration) + 1. [Managing the service](#managing-the-service) +1. [Deployment](#deployment) +1. [How to Get Help](#how-to-get-help) +1. [Further Reading](#further-reading) +1. [Contributing](#contributing) +1. [License](#license) +1. [Authors](#authors) +1. [Acknowledgments](#acknowledgments) -### Starting the service -Install dependencies: `npm i` -Build & start program: `npm start` - -## The goals of this repo +# About the Project +The main goals of this repo: 1. create a (simple, but capable) service/bot to monitor game servers 1. get gamedig & steam api server info (_eg.: server name, map, players, etc._) 1. relay real time server information to various channels via APIs (_eg.: discord, telegram, slack etc._) 1. should be able to host on a free service (_target atm. is cloudno.de (nodejs 12.20.1)_) - 1. graciously add more features based on community feedback + 1. graciously add more features based on community feedback ([discord](https://discord.gg/4tsbftsGJz) / [github](https://github.com/a-sync/game-server-watcher/issues/new)) -Get support on [discord](https://discord.gg/4tsbftsGJz) or on [github](https://github.com/a-sync/game-server-watcher/issues/new). -More detailed customization options and additional features will be added as requested. +## Screenshots +### Discord +![discord](https://user-images.githubusercontent.com/14183614/162092529-e1645b44-2650-4893-8123-7ba187b1f51c.png) -[SCREENSHOTS](https://github.com/a-sync/game-server-watcher/issues/1#issue-1195221880) +### Telegram +![telegram](https://user-images.githubusercontent.com/14183614/162092488-f28bd60c-88bf-4b1e-a31e-d7dca51d8c28.png) + +**[Back to top](#table-of-contents)** + +# Project Status +The code itself is stable and continuously tested/deployed from the cloud branch. + +The project is in a very early stage. More detailed customization options and additional features will be added as requested. ### Possible features and configuration options to add in the future * optional player list @@ -35,75 +54,202 @@ More detailed customization options and additional features will be added as req * more integrations: slack, ms teams, twillio (email, sms) * web ui to manage & configure the servers and bots * put custom information in the channel name (online status indicator, number of players, map) + * github action workflows to deploy to other cloud providers (azure, aws cdk, etc.) -# Self host on cloudno.de -You can fork (copy) this repo on github, and automatically deploy your configuration or other changes as a cloud service. +**[Back to top](#table-of-contents)** -## Part 1: create and setup [cloudno.de](https://cloudno./de) repo -1. register account -1. create app: https://cloudno.de/myapps?new (make sure to set the latest node.js version and leave server.js as entry point) -1. provision app -1. copy https git URL from app -1. go to profile and save your token (https://cloudno.de/account --> API tokens) -1. add username and token as authentication to the URL like this: `https://username:secrettoken@git.cloudno.de/git/username/xyz`; save it for later use on (github) -1. go to application --> manage --> environment -1. set `DATA_PATH=/mnt/` in cloud app env -1. go to the forked repo on github: settings --> secrets --> actions and setup `CLOUDNODE_REPO_URL` secret with the full URL +# Getting Started +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. -## Part 2 (optional): create and invite discord bot and get token -1. create app (https://discord.com/developers/applications) -1. create bot -1. under the bots name you should seee a token or an option to reset token -1. check message content intent -1. oauth2 --> url generator --> check bot as scopes; create bot invite link with permissions: view channels, send messages, message history, embed links, add reaction (85056), -1. invite bot to servers (guilds) -1. create bot auth token, copy it -1. setup `DISCORD_BOT_TOKEN` in cloud app env -1. //CONFIG: find and copy then setup channel ids in config +## Requirements +[node.js](https://nodejs.org/) _(version 12.20.0 or later)_ -## Part 3 (optional): create telegram bot and get token -//TODO: get token from [botfather](https://t.me/botfather) and setup as `TELEGRAM_BOT_TOKEN` in cloud app env; get chat id from [getidsbot](https://t.me/getidsbot) and setup chat ids in config +## Getting the Source +This project is [hosted on github](https://github.com/a-sync/game-server-watcher). You can clone this project directly using this command: -## Part 4 (optional): create steam web api key -[Request a web api key](https://steamcommunity.com/dev/apikey) and configure it as `STEAM_WEB_API_KEY` in cloud app env; set appId fields in config +``` +git clone git@github.com:a-sync/game-server-watcher.git +``` -## Part 5: create and setup github repo -1. hit fork button -1. repo settings page --> actions --> general: allow all actions (if no confirmation comes up, disable then re-enable all actions) +The latest source and build can also be downloaded as a [zip archive](https://github.com/a-sync/game-server-watcher/archive/refs/heads/master.zip). -## Part 6: edit `cloud` branch, configure and deploy the service -_**Protip:** Use the dot (**.**) key to open your code in github.dev web-based editor when browsing github!_ -1. switch to the `cloud` branch of your fork; (`git checkout -b cloud`) -1. create a new config file (and setup `GSW_CONFIG` in cloud app env to point to it) or edit config/default.config.json and save it -1. commit and push your changes (`git add . && git commit -m :shipit: && git push origin cloud`) +## Installation +Use npm or yarn to install/update all the dependencies. -You can also just use the _edit this file_ button to make changes to the default config file, just make sure to switch to the `cloud` branch to have it automatically deployed. - -Commiting your changes and pushing your commits to github will trigger github actions to push your files to cloudno.de and restart the service. - -# Manage -A few web endpoints are available to clear out service data. -Make sure to configure a proper `SECRET` env var to enable these! - -### Servers `/flush/servers/SECRET` -Removes all stored game server data: - * population history configured by `PLAYERS_HISTORY_HOURS` - -### Telegram `/flush/telegram/SECRET` -The bot has no cleanup functionality, left over messages must be removed manually. -If the original message created by the bot gets deleted, you need to flush the bot data to reinitialize the message. - -### Discord `/flush/discord/SECRET` -The bot has no cleanup functionality, left over messages must be removed manually. -If the original message created by the bot gets deleted, you might need to flush the bot data to reinitialize the message. - -# Development -## Running locally ``` npm i +``` + +If you don't need to build the source, you can skip development dependencies. + +``` +npm i --only=prod +``` + +## Usage +Build the source and start the service in one command. + +``` +npm start +``` + +Build, start and auto restart on source changes. + +``` npm run dev ``` -## Nodejs v12 dependency limitations -* [discord.js v12](https://discord.js.org/#/docs/discord.js/v12/general/welcome) (discord API v8) -* got v11 +## Configuration +The watcher service can be configured via environmental variables. `.env` ([dotenv](https://www.npmjs.com/package/dotenv)) file is also supported, for avaialable values and defaults check the [default.env](./default.env) file. + +Refer to the wiki on how to get tokens for: + * [steam](https://github.com/a-sync/game-server-watcher/wiki/Steam-Web-API-key) + * [discord](https://github.com/a-sync/game-server-watcher/wiki/Discord-bot-token) + * [telegram](https://github.com/a-sync/game-server-watcher/wiki/Telegram-bot-token) + +### Config file schema +The game server and bot configurations are stored in JSON files. +The path of the config file used is defined by the `GSW_CONFIG` env var. (default: [./config/default.config.json](./config/default.config.json)) + +The config file must be a valid JSON file that holds a list (array) of game server configuration objects. + +```json +[ + { + "host": "0.0.0.0", + "port": 0, + "type": "gamedigID", + "appId": 0, + "discord": { + "channelIds": [] + }, + "telegram": { + "chatIds": [] + } + }, + ... +] +``` + +Each configuration object describes a game server (host, port, gamedig id, steam app id) and all the bots options for that game server. (discord channel IDs, telegram chat IDs) + +#### host +String. Can be a domain name or IP. + +#### port +Number. The port used by the game server instance. + +#### type +String. Gamedig ID from the [supported games list](https://raw.githubusercontent.com/a-sync/node8-gamedig/master/games.txt). + +#### appId +Number. Steam app ID from [steamdb](https://steamdb.info/apps/). +_Only used if you have `STEAM_WEB_API_KEY` configured and only as backup. Set it to `0` if you don't need it._ + +#### discord.channelIds +String array. List of discord channel IDs. +_Only used if you have `DISCORD_BOT_TOKEN` configured._ + +#### telegram.chatIds +String array. List of telegram chat IDs. +_Only used if you have `TELEGRAM_BOT_TOKEN` configured._ + +## Managing the service +A few web endpoints are available to clear out service data. +Make sure to configure a proper `SECRET` env var to enable these! + +### Servers data +GET `/flush/servers/SECRET` +Removes population history data. (configured by `PLAYERS_HISTORY_HOURS`) + +### Telegram data +GET `/flush/telegram/SECRET` +If the original message created by the bot gets deleted, you need to flush the bot data to reinitialize the message. +The bot has no cleanup functionality, left over messages must be removed manually. + +### Discord data +GET `/flush/discord/SECRET` +If the original message created by the bot gets deleted, you might need to flush the bot data to reinitialize the message. +The bot has no cleanup functionality, left over messages must be removed manually. + +**[Back to top](#table-of-contents)** + +# Deployment +Check the wiki page for detailed instructions on [how to setup a self deploying free cloud instance at cloudno.de](https://github.com/a-sync/game-server-watcher/wiki/Free-hosting-via-cloudno.de). + +## Hosting +Make sure all the requirements are met! +_**Protip:** check the node.js version with `node -v`._ + +### Build artifacts (optional) +``` +npm i +npm run build +``` + +### Minimum required files on host +Copy the `./package.json` and `index.html` files and the `./dist/` folder to your deployment folder. + +### Install production dependencies on host +Navigate to the deployment folder and execute the following command: +``` +npm i --only=prod +``` + +### Configuration +Create a writeable folder for the data storage. (configured by `DATA_PATH` env var, default: `./data/`) +Create a configuration file. (configured by `GSW_CONFIG`, default: `./config/default.config.json`) + +### Running +Run the program from the deployment folder: +``` +node ./dist/server.js +``` + +**[Back to top](#table-of-contents)** + +# How to Get Help +* Join the support / test [discord](https://discord.gg/4tsbftsGJz) +* Check the [wiki](https://github.com/a-sync/game-server-watcher/wiki) +* Ask a [question](https://github.com/a-sync/game-server-watcher/discussions/new?category=q-a) + +**[Back to top](#table-of-contents)** + +# Further Reading +* [gamedig supported games list](https://raw.githubusercontent.com/a-sync/node8-gamedig/master/games.txt) +* [steam web API documentation](https://steamapi.xpaw.me/#IGameServersService/GetServerList) +* [discord API v8 documentation](https://discord.js.org/#/docs/discord.js/v12/class/MessageEmbed) +* [telegram API documentation](https://core.telegram.org/bots/api#editmessagetext) +* [JSON validator](https://jsonformatter.org/) +* [steamDB](https://steamdb.info/apps/) + +**[Back to top](#table-of-contents)** + +# Contributing +Public contributions are welcome! +Create a [new issue](https://github.com/a-sync/game-server-watcher/issues/new) for bugs, or open a [pull request](https://github.com/a-sync/game-server-watcher/pulls) for any and all your changes. + +**[Back to top](#table-of-contents)** + +# License +This project is licensed under the AGPL License - see [LICENSE](./LICENSE) file for details. + +**[Back to top](#table-of-contents)** + +# Authors +Check the list of [contributors](https://github.com/a-sync/game-server-watcher/contributors) who participated in this project. + +**[Back to top](#table-of-contents)** + +# Acknowledgments +This project was inpired by (the sudden disappearance of) "_Game Status#5371_" bot and its creator [Ramzi Saheb](https://github.com/Ramzi-Sah) on discord. + +The IP regex was stolen from the [ip-regex](https://github.com/sindresorhus/ip-regex) package source. + +## Similar projects +https://github.com/soulkobk/DiscordBot_GameStatus +https://github.com/discord-gamestatus/discord-gamestatus +https://github.com/Ramzi-Sah/game-status-discordbot +https://github.com/Ramzi-Sah/game-status-discordbot-selfhosted + +**[Back to top](#table-of-contents)** diff --git a/config/acme.config.json b/config/acme.config.json index 29404e0..22655d2 100644 --- a/config/acme.config.json +++ b/config/acme.config.json @@ -126,5 +126,131 @@ "325831302" ] } + }, + { + "host": "51.195.60.161", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493493650817024" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "135.125.188.105", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493493650817024" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "95.111.240.225", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493493650817024" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "51.195.209.188", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "51.195.56.191", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "135.125.140.176", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "135.125.140.177", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "51.195.68.16", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } + }, + { + "host": "198.244.131.161", + "port": 2302, + "type": "arma3", + "appId": 107410, + "discord": { + "channelIds": [ + "965493409819283466" + ] + }, + "telegram": { + "chatIds": [] + } } ] diff --git a/config/mta.config.json b/config/mta.config.json index a8742bc..6f9e524 100644 --- a/config/mta.config.json +++ b/config/mta.config.json @@ -10,9 +10,7 @@ ] }, "telegram": { - "chatIds": [ - "" - ] + "chatIds": [] } } ] diff --git a/default.env b/default.env index 8e8de56..e7370d7 100644 --- a/default.env +++ b/default.env @@ -1,11 +1,22 @@ -DBG=1 +# Enable debug mode +DBG=0 +# Game server info refresh interval in minutes REFRESH_TIME_MINUTES=5 +# Player population history max. length in hours PLAYERS_HISTORY_HOURS=12 +# The game server configuration file GSW_CONFIG=./config/default.config.json +# Writeable folder for data storage DATA_PATH=./data/ -SECRET=secret +# Admin secret +SECRET= +# Web service host APP_HOST=localhost +# Web service port APP_PORT=8080 +# Discord bot token DISCORD_BOT_TOKEN= +# Telegram bot token TELEGRAM_BOT_TOKEN= +# Steam web API key STEAM_WEB_API_KEY= diff --git a/dist/game-server.js b/dist/game-server.js index b91ded7..3b8447f 100644 --- a/dist/game-server.js +++ b/dist/game-server.js @@ -12,6 +12,7 @@ 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 || '12', 10); const DATA_PATH = process.env.DATA_PATH || './data/'; +const DBG = Boolean(process.env.DBG || false); const adapter = new lowdb_1.JSONFile(DATA_PATH + 'servers.json'); const db = new lowdb_1.Low(adapter); async function initDb() { @@ -39,11 +40,20 @@ class GameServer { this._niceName = config.host + ':' + config.port; } async update() { + if (DBG) + console.log('gs.up', this.config.host, this.config.port); let info = await this.gamedig(); - if (!info && STEAM_WEB_API_KEY) { + if (DBG) + console.log('gs.gamedig', info); + if (!info && STEAM_WEB_API_KEY && this.config.appId) { info = await this.steam(); + if (DBG) + console.log('gs.steam', info); } if (info) { + if (info.players.length > 0 && DBG) { + console.log('gs.players.0', info.players[0]); + } this.online = true; this.info = info; this.history.add(info); diff --git a/dist/lib/ipregex.js b/dist/lib/ipregex.js index b81c6d0..b03cf44 100644 --- a/dist/lib/ipregex.js +++ b/dist/lib/ipregex.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +// source: https://github.com/sindresorhus/ip-regex 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 = ` @@ -14,6 +15,5 @@ const v6 = ` (?::(?:(?::${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; diff --git a/package.json b/package.json index edaa9bf..3c9939c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "src/server", "scripts": { "start": "tsc && node ./dist/server.js", - "dev": "nodemon -e ts --exec \"tsc --incremental && node ./dist/server.js || exit 1\"" + "dev": "nodemon -e ts --exec \"tsc --incremental && node ./dist/server.js || exit 1\"", + "build": "tsc" }, "keywords": [], "author": "a-sync@devs.space", diff --git a/src/game-server.ts b/src/game-server.ts index 0b43878..ae48eff 100644 --- a/src/game-server.ts +++ b/src/game-server.ts @@ -8,6 +8,7 @@ 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 || '12', 10); const DATA_PATH = process.env.DATA_PATH || './data/'; +const DBG = Boolean(process.env.DBG || false); interface GameServerDb { population: { @@ -64,13 +65,20 @@ export class GameServer { } async update() { + if (DBG) console.log('gs.up', this.config.host, this.config.port); let info = await this.gamedig(); - if (!info && STEAM_WEB_API_KEY) { + if (DBG) console.log('gs.gamedig', info); + if (!info && STEAM_WEB_API_KEY && this.config.appId) { info = await this.steam(); + if (DBG) console.log('gs.steam', info); } if (info) { + if (info.players.length > 0 && DBG) { + console.log('gs.players.0', info.players[0]); + } + this.online = true; this.info = info; this.history.add(info); diff --git a/src/lib/ipregex.ts b/src/lib/ipregex.ts index bd2a564..0ee8277 100644 --- a/src/lib/ipregex.ts +++ b/src/lib/ipregex.ts @@ -1,3 +1,4 @@ +// source: https://github.com/sindresorhus/ip-regex 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}'; @@ -15,7 +16,6 @@ const v6 = ` )(?:%[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;