Merge branch 'master' into pterodactyl-🥚
2
.github/workflows/create.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
||||
run: flyctl apps create ${{ github.repository_owner }}-gsw --org personal
|
||||
|
||||
- name: 💽 Create fly.io volume
|
||||
run: flyctl volumes create gsw_data --size 1 --region ${{ secrets.FLY_REGION }} --app ${{ github.repository_owner }}-gsw
|
||||
run: flyctl volumes create gsw_data --size 1 --region ${{ secrets.FLY_REGION }} --app ${{ github.repository_owner }}-gsw --yes
|
||||
|
||||
20
README.md
@@ -1,4 +1,4 @@
|
||||
# <img src="https://i.imgur.com/2Ok3pxv.png" width="40" align="left"> Game Server Watcher
|
||||
# <img src="https://github.com/a-sync/game-server-watcher/assets/14183614/66a8ffa8-c547-4f9c-b312-4c855da80c20" width="40" align="left"> 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. 😎
|
||||
|
||||
# About the Project
|
||||
@@ -9,18 +9,8 @@ The main goals of this repo:
|
||||
1. should be able to host on a free service (_target atm. is fly.io (node.js 16)_)
|
||||
1. graciously add more features based on community feedback via [discord](https://discord.gg/4tsbftsGJz) and [github](https://github.com/a-sync/game-server-watcher/discussions/new?category=ideas-requests)
|
||||
|
||||
<details>
|
||||
<summary><h1>Screenshots</h1></summary>
|
||||
|
||||
### Discord
|
||||

|
||||
|
||||
### Telegram
|
||||

|
||||
|
||||
### GSW Control Panel
|
||||

|
||||
</details>
|
||||
# Screenshots
|
||||
<img src="https://user-images.githubusercontent.com/14183614/162092529-e1645b44-2650-4893-8123-7ba187b1f51c.png" height="360"> <img src="https://user-images.githubusercontent.com/14183614/162092488-f28bd60c-88bf-4b1e-a31e-d7dca51d8c28.png" height="360"> <img src="https://user-images.githubusercontent.com/14183614/167517240-a4d02cb2-8037-44c0-abaa-c92901357e35.png" height="360">
|
||||
|
||||
# Project Status
|
||||
The code itself is stable and continuously tested/deployed from the master branch.
|
||||
@@ -37,14 +27,14 @@ The project is in a very early stage. More detailed customization options and fe
|
||||
* refresh on reaction
|
||||
* watched players (notify when a watched player enters/leaves the server)
|
||||
* detect when the server goes offline, notify when player number crosses a threshold
|
||||
* bot commands (reinit message, cleanup, start/stop, configure)
|
||||
* bot commands (reinit message, cleanup, start/stop, post server status, configure)
|
||||
* more integrations: slack, ms teams, twillio (email, sms)
|
||||
* ~~web ui to manage & configure the servers and bots~~
|
||||
* put custom information in the channel name or bot status (online status indicator, number of players, map)
|
||||
* github action workflows to deploy to other cloud providers (aws, linode, atlantic, vultr, pikapods, okteto, ibm cloud etc.)
|
||||
* SQL, JSON or object store database support (postgres, redis etc.)
|
||||
* run as stateless serverless function (aws lambda, azure function, heroku, vercel, fly.io machines etc.)
|
||||
* [pterodactyl](https://pterodactyl.io) egg release
|
||||
* pterodactyl egg release
|
||||
|
||||
# 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](#deployment) for notes on how to deploy the project on a live system.
|
||||
|
||||
8
TODO
Normal file
@@ -0,0 +1,8 @@
|
||||
// move loop inside watcher instance
|
||||
// flush game server specific (d/t) data
|
||||
// response normalization: https://github.com/GameServerManagers/LinuxGSM/blob/master/lgsm/functions/query_gamedig.sh
|
||||
|
||||
//v3
|
||||
// refactor config structure
|
||||
// introduce storage layer and add free postgresql support
|
||||
// support gamedig udpListenPort config
|
||||
2
app.json
@@ -3,7 +3,7 @@
|
||||
"description": "A simple discord/telegram bot that can be hosted on a free service to monitor your game servers and players in style. 😎",
|
||||
"website": "https://github.com/a-sync/game-server-watcher",
|
||||
"repository": "https://github.com/a-sync/game-server-watcher",
|
||||
"logo": "https://i.imgur.com/2Ok3pxv.png",
|
||||
"logo": "https://github.com/a-sync/game-server-watcher/assets/14183614/66a8ffa8-c547-4f9c-b312-4c855da80c20",
|
||||
"env": {
|
||||
"SECRET": {
|
||||
"description": "Admin secret",
|
||||
|
||||
59
fly.toml
@@ -1,46 +1,31 @@
|
||||
app = "game-server-watcher"
|
||||
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = []
|
||||
primary_region = "ams"
|
||||
|
||||
[env]
|
||||
DBG=0
|
||||
REFRESH_TIME_MINUTES=2
|
||||
DATA_PATH="/data/"
|
||||
PRIMARY_REGION = "ams"
|
||||
DBG = "0"
|
||||
DATA_PATH = "/data/"
|
||||
REFRESH_TIME_MINUTES = "2"
|
||||
|
||||
[mounts]
|
||||
source="gsw_data"
|
||||
destination="/data"
|
||||
source = "gsw_data"
|
||||
destination = "/data"
|
||||
|
||||
[experimental]
|
||||
auto_rollback = true
|
||||
allowed_public_ports = []
|
||||
# cmd = ["node", "--max-old-space-size=180", "server.js"]
|
||||
|
||||
[[services]]
|
||||
http_checks = []
|
||||
[http_service]
|
||||
internal_port = 8080
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
type = "connections"
|
||||
[http_service.concurrency]
|
||||
type = "requests"
|
||||
soft_limit = 50
|
||||
hard_limit = 60
|
||||
|
||||
[[services.ports]]
|
||||
force_https = true
|
||||
handlers = ["http"]
|
||||
port = 80
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["tls", "http"]
|
||||
port = 443
|
||||
|
||||
[[services.tcp_checks]]
|
||||
grace_period = "1s"
|
||||
interval = "15s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
[[http_service.checks]]
|
||||
grace_period = "10s"
|
||||
interval = "30s"
|
||||
method = "GET"
|
||||
timeout = "5s"
|
||||
path = "/"
|
||||
|
||||
3034
package-lock.json
generated
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "game-server-watcher",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "A simple discord/telegram bot that can be hosted on a free service to monitor your game servers and players in style 😎",
|
||||
"main": "src/server",
|
||||
"scripts": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@commonify/lowdb": "^3.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios": "^1.4.0",
|
||||
"discord.js": "^14.7.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"gamedig": "^4.0.6",
|
||||
@@ -31,11 +31,11 @@
|
||||
"mustache": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gamedig": "^4.0.0",
|
||||
"@types/gamedig": "^4.0.1",
|
||||
"@types/node": "^16.9.0",
|
||||
"eslint": "^8.34.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"typescript": "^4.9.5"
|
||||
"nodemon": "^3.0.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
BIN
public/bg0.png
Normal file
|
After Width: | Height: | Size: 690 KiB |
BIN
public/bg1.png
Normal file
|
After Width: | Height: | Size: 545 KiB |
BIN
public/bg2.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
public/bg3.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
public/bg4.png
Normal file
|
After Width: | Height: | Size: 518 KiB |
BIN
public/bg5.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
188
public/game-server-config.schema.json
Normal file
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"title": "Game Server",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Gamedig type",
|
||||
"description": "Look for the <i>GameDig Type ID</i> in the <a href=\"https://github.com/gamedig/node-gamedig#games-list\">games list</a>.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"title": "Host name or IP",
|
||||
"type": "string",
|
||||
"pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\\-]*[A-Za-z0-9])$",
|
||||
"options": {
|
||||
"grid_columns": 3,
|
||||
"error_messages": {
|
||||
"en": {
|
||||
"error_pattern": "Value must be a valid host name or IP"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"title": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"appId": {
|
||||
"title": "Steam App ID",
|
||||
"description": "Look for the <i>AppID</i> in the <a href=\"https://steamdb.info/apps/\">apps list</a>.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "array",
|
||||
"title": "Discord channels",
|
||||
"description": "The simplest way to get a discord channel ID is to enable developer mode in settings, then right clicking on the channel name will give you the <b>Copy ID</b> option.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Channel",
|
||||
"required": [
|
||||
"channelId"
|
||||
],
|
||||
"properties": {
|
||||
"channelId": {
|
||||
"title": "Channel ID",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"type": "array",
|
||||
"title": "Telegram chats",
|
||||
"description": "The simplest way to get a telegram chat ID is to invite <a target=\"_blank\" href=\"https://telegram.me/getidsbot\">@getidsbot</a> and use <code>/start</code> to make it post (among other data) the chat ID.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Chat",
|
||||
"required": [
|
||||
"chatId"
|
||||
],
|
||||
"properties": {
|
||||
"chatId": {
|
||||
"type": "string",
|
||||
"title": "Chat ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"graphHistoryHours": {
|
||||
"title": "Graph history time span (hours)",
|
||||
"type": "integer",
|
||||
"default": 12,
|
||||
"minimum": 1,
|
||||
"maximum": 24,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"timezoneOffset": {
|
||||
"title": "Time zone offset of the server",
|
||||
"default": 0,
|
||||
"type": "number",
|
||||
"minimum": -12,
|
||||
"maximum": 14,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"givenPortOnly": {
|
||||
"title": "givenPortOnly",
|
||||
"description": "Only attempt to query server on given port.",
|
||||
"type": "boolean",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"requestRules": {
|
||||
"title": "requestRules (Valve)",
|
||||
"description": "Additional server info may be fetched into the <code>raw</code> field.",
|
||||
"type": "boolean",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"teamspeakQueryPort": {
|
||||
"title": "teamspeakQueryPort (TeamSpeak 3)",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"guildId": {
|
||||
"title": "guildId (Discord)",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "login (Nadeo)",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"title": "password (Nadeo)",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"title": "token (Terraria)",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "grid-strict",
|
||||
"headerTemplate": "{{#self.name}}{{self.name}}{{/self.name}}{{^self.name}}{{self.type}}:{{self.host}}:{{self.port}}{{/self.name}}"
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://i.imgur.com/2Ok3pxv.png" rel="icon" type="image/png">
|
||||
<link href="favicon.png" rel="icon" type="image/png">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/@forevolve/bootstrap-dark@latest/dist/css/toggle-bootstrap-dark.min.css" rel="stylesheet"> -->
|
||||
@@ -38,7 +38,7 @@
|
||||
<body>
|
||||
<nav id="top-menu" class="navbar navbar-expand-lg navbar-dark invisible">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<img src="https://i.imgur.com/2Ok3pxv.png" width="30" height="30" class="d-inline-block align-middle mr-sm-1 mr-md-2 mr-lg-3" alt="">
|
||||
<img src="favicon.png" width="30" height="30" class="d-inline-block align-middle mr-sm-1 mr-md-2 mr-lg-3" alt="">
|
||||
<span>GSW Control Panel</span>
|
||||
</span>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
|
||||
@@ -12,7 +12,7 @@ body {
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-image: url('https://i.imgur.com/lTly23N.png');
|
||||
background-image: url('bg0.png');
|
||||
}
|
||||
|
||||
body>nav {
|
||||
@@ -61,13 +61,6 @@ body>nav {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#config-form input[type=checkbox] {
|
||||
vertical-align: middle;
|
||||
zoom: 2;
|
||||
cursor: pointer;
|
||||
margin-right: 0.25rem !important;
|
||||
}
|
||||
|
||||
#config-form .btn-group {
|
||||
display: inline-flex !important;
|
||||
margin-bottom: 0.25rem;
|
||||
@@ -86,14 +79,62 @@ body>nav {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-add {
|
||||
#config-form .json-editor-btn-add,
|
||||
#config-form .json-editor-btn-save,
|
||||
#config-form .json-editor-btn-file-import {
|
||||
background-color: darkgreen;
|
||||
}
|
||||
#config-form .json-editor-btn-add span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-delete {
|
||||
#config-form .json-editor-btn-delete,
|
||||
#config-form .json-editor-btn-cancel {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-delete span,
|
||||
#config-form .json-editor-btn-cancel span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-copy,
|
||||
#config-form .json-editor-btn-file-export {
|
||||
background-color: darkblue;
|
||||
}
|
||||
|
||||
#config-form .json-editor-btn-copy span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#config-form .je-edit-json--textarea {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
#config-form .je-modal {
|
||||
box-shadow: rgba(0,0,0,0.5) 3px 3px 6px 2px !important;
|
||||
border: 1px solid rgba(0,0,0,0.25) !important;
|
||||
}
|
||||
|
||||
#config-form .property-selector {
|
||||
min-width: max-content;
|
||||
max-height: none !important;
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
#config-form .property-selector .form-check-input {
|
||||
cursor: pointer;
|
||||
margin-left: -0.85rem !important;
|
||||
}
|
||||
|
||||
#config-form .property-selector .form-check-label {
|
||||
cursor: pointer;
|
||||
padding-left: 5px !important;
|
||||
}
|
||||
|
||||
footer>a>svg {
|
||||
fill: #c6cbd1;
|
||||
}
|
||||
@@ -124,4 +165,4 @@ footer>a>svg {
|
||||
|
||||
#bye kbd {
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
}
|
||||
|
||||
197
public/main.js
@@ -2,7 +2,7 @@ let configEditor;
|
||||
$(async () => {
|
||||
const gswFeatures = await fetchApi('GET', 'features');
|
||||
if (gswFeatures) {
|
||||
await setRandomBg().catch(() => {});
|
||||
await setRandomBg().catch(() => { });
|
||||
setInterval(setRandomBg, 60000);
|
||||
|
||||
let unsavedChanges = false;
|
||||
@@ -33,21 +33,22 @@ $(async () => {
|
||||
"iconlib": "fontawesome5",
|
||||
"object_layout": "normal",
|
||||
"template": "mustache",
|
||||
"compact": 0,
|
||||
"show_errors": "always",
|
||||
"required_by_default": 0,
|
||||
"no_additional_properties": 1,
|
||||
"display_required_only": 0,
|
||||
"remove_empty_properties": 1,
|
||||
"keep_oneof_values": 0,
|
||||
"ajax": 0,
|
||||
"display_required_only": 1,
|
||||
"show_opt_in": 0,
|
||||
"remove_empty_properties": 0,
|
||||
"use_default_values": 1,
|
||||
"ajax": 1,
|
||||
"ajaxCredentials": 0,
|
||||
"show_opt_in": 1,
|
||||
"disable_edit_json": 0,
|
||||
"disable_collapse": 1,
|
||||
"disable_properties": 1,
|
||||
"disable_properties": 0,
|
||||
"disable_array_add": 0,
|
||||
"disable_array_reorder": 1,
|
||||
"disable_array_delete": 0,
|
||||
"disable_array_reorder": 1,
|
||||
"enable_array_copy": 1,
|
||||
"array_controls_top": 0,
|
||||
"disable_array_delete_all_rows": 1,
|
||||
@@ -61,148 +62,15 @@ $(async () => {
|
||||
"uniqueItems": true,
|
||||
"format": "tabs-top",
|
||||
"items": {
|
||||
"title": "Game Server",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Name",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"title": "Gamedig type",
|
||||
"description": "Look for the <i>GameDig Type ID</i> in the <a href=\"https://github.com/gamedig/node-gamedig#games-list\">games list</a>.",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"appId": {
|
||||
"title": "Steam App ID",
|
||||
"description": "Look for the <i>AppID</i> in the <a href=\"https://steamdb.info/apps/\">apps list</a>.",
|
||||
"type": "integer",
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"title": "Host name or IP",
|
||||
"type": "string",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"port": {
|
||||
"title": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"format": "number",
|
||||
"options": {
|
||||
"grid_columns": 6
|
||||
}
|
||||
},
|
||||
"updateIntervalMinutes": {
|
||||
"title": "Update interval (minutes) [NOT IMPLEMENTED]",
|
||||
"type": "integer",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"graphHistoryHours": {
|
||||
"title": "Graph history time span (hours)",
|
||||
"type": "integer",
|
||||
"default": 12,
|
||||
"minimum": 1,
|
||||
"maximum": 24,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"timezoneOffset": {
|
||||
"title": "Time zone offset of the server",
|
||||
"default": 0,
|
||||
"type": "number",
|
||||
"minimum": -12,
|
||||
"maximum": 14,
|
||||
"format": "range",
|
||||
"options": {
|
||||
"grid_columns": 6,
|
||||
"grid_break": true
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"type": "array",
|
||||
"title": "Discord channels",
|
||||
"description": "The simplest way to get a discord channel ID is to enable developer mode in settings, then right clicking on the channel name will give you the <b>Copy ID</b> option.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Channel",
|
||||
"required": [
|
||||
"channelId"
|
||||
],
|
||||
"properties": {
|
||||
"channelId": {
|
||||
"title": "Channel ID",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"type": "array",
|
||||
"title": "Telegram chats",
|
||||
"description": "The simplest way to get a telegram chat ID is to invite <a target=\"_blank\" href=\"https://telegram.me/getidsbot\">@getidsbot</a> and use <code>/start</code> to make it post (among other data) the chat ID.",
|
||||
"minItems": 0,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Chat",
|
||||
"required": [
|
||||
"chatId"
|
||||
],
|
||||
"properties": {
|
||||
"chatId": {
|
||||
"type": "string",
|
||||
"title": "Chat ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "table",
|
||||
"options": {
|
||||
"grid_columns": 12
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": "grid",
|
||||
"headerTemplate": "{{#self.name}}{{self.name}}{{/self.name}}{{^self.name}}{{self.type}}:{{self.host}}:{{self.port}}{{/self.name}}"
|
||||
"$ref": "game-server-config.schema.json"
|
||||
}
|
||||
},
|
||||
startval: gswConfig.config
|
||||
});
|
||||
|
||||
configEditor.on('ready', () => {
|
||||
addExportButton();
|
||||
addExportImportButtons();
|
||||
|
||||
$('#config-form').submit(e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
@@ -295,13 +163,15 @@ function checkForChanges(formCurrVal) {
|
||||
}
|
||||
};
|
||||
|
||||
function addExportButton() {
|
||||
function addExportImportButtons() {
|
||||
const button_holder = configEditor.root.theme.getHeaderButtonHolder();
|
||||
const exportBtn = configEditor.root.getButton('Export', 'save', 'Export As JSON File');
|
||||
const exportBtn = configEditor.root.getButton('Export', 'file-export', 'Export as JSON file');
|
||||
const importBtn = configEditor.root.getButton('Import', 'file-import', 'Import from JSON file');
|
||||
button_holder.appendChild(exportBtn);
|
||||
button_holder.appendChild(importBtn);
|
||||
configEditor.root.header.parentNode.insertBefore(button_holder, configEditor.root.header.nextSibling);
|
||||
|
||||
exportBtn.addEventListener('click', e => {
|
||||
exportBtn.onclick = e => {
|
||||
e.preventDefault();
|
||||
const dt = (new Date()).toISOString().slice(0, 19).replace(/\D/g, '');
|
||||
const filename = dt + '-gsw.config.json';
|
||||
@@ -322,7 +192,28 @@ function addExportButton() {
|
||||
'cancelable': false
|
||||
}));
|
||||
}
|
||||
}, false);
|
||||
};
|
||||
|
||||
const fileSelector = document.createElement('input');
|
||||
fileSelector.type = 'file';
|
||||
fileSelector.accept = 'application/json';
|
||||
|
||||
fileSelector.onchange = e => {
|
||||
const file = e.target.files[0];
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = readerEvent => {
|
||||
const content = JSON.parse(readerEvent.target.result);
|
||||
configEditor.setValue(content);
|
||||
};
|
||||
};
|
||||
|
||||
importBtn.onclick = e => {
|
||||
e.preventDefault();
|
||||
fileSelector.click();
|
||||
};
|
||||
}
|
||||
|
||||
function logout() {
|
||||
@@ -513,11 +404,11 @@ async function generateBearerToken(secret) {
|
||||
|
||||
function setRandomBg() {
|
||||
const bgs = [
|
||||
'https://i.imgur.com/bDzhwG5.png',
|
||||
'https://i.imgur.com/rA8JXuI.png',
|
||||
'https://i.imgur.com/pstAPIw.png',
|
||||
'https://i.imgur.com/gQD3xfo.png',
|
||||
'https://i.imgur.com/iKTfM8z.png'
|
||||
'bg1.png',
|
||||
'bg2.png',
|
||||
'bg3.png',
|
||||
'bg4.png',
|
||||
'bg5.png'
|
||||
];
|
||||
|
||||
const bgImg = new Image();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { Player, query, QueryResult } from 'gamedig';
|
||||
import { Player, query, QueryResult, Type } from 'gamedig';
|
||||
import { Low, JSONFile } from '@commonify/lowdb';
|
||||
import ipRegex from './lib/ipregex';
|
||||
import getIP from './lib/getip';
|
||||
@@ -48,6 +48,28 @@ interface qRes extends QueryResult {
|
||||
numplayers: number;
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
type: Type;
|
||||
host: string;
|
||||
port: number;
|
||||
maxAttempts?: number;
|
||||
socketTimeout?: number;
|
||||
attemptTimeout?: number;
|
||||
givenPortOnly?: boolean;
|
||||
ipfamily?: 0 | 4 | 6 | undefined;
|
||||
debug?: boolean;
|
||||
requestRules?: boolean;
|
||||
// Discord
|
||||
guildId?: string;
|
||||
// Nadeo
|
||||
login?: string;
|
||||
password?: string;
|
||||
// Teamspeak 3
|
||||
teamspeakQueryPort?: number;
|
||||
// Terraria
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export class GameServer {
|
||||
public ip: string;
|
||||
public info?: Info;
|
||||
@@ -70,7 +92,7 @@ export class GameServer {
|
||||
if (DBG) console.log('gs.up', this.config.host, this.config.port);
|
||||
let info = await this.gamedig();
|
||||
|
||||
if (DBG) console.log('gs.gamedig', Object.assign({}, info, { players: info?.players.length }));
|
||||
if (DBG) console.log('gs.gamedig %j', Object.assign({}, info, { players: info?.players.length }));
|
||||
if (!info && STEAM_WEB_API_KEY && this.config.appId) {
|
||||
info = await this.steam();
|
||||
if (DBG) console.log('gs.steam', info);
|
||||
@@ -78,7 +100,7 @@ export class GameServer {
|
||||
|
||||
if (info) {
|
||||
if (info.players.length > 0 && DBG) {
|
||||
console.log('gs.players.0', info.players[0]);
|
||||
console.log('gs.players.0 %j', info.players[0]);
|
||||
}
|
||||
|
||||
this.online = true;
|
||||
@@ -94,10 +116,17 @@ export class GameServer {
|
||||
async gamedig(): Promise<Info | null> {
|
||||
try {
|
||||
const res = await query({
|
||||
type: this.config.type,
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
type: this.config.type,
|
||||
}) as qRes;
|
||||
givenPortOnly: this.config.givenPortOnly,
|
||||
requestRules: this.config.requestRules,
|
||||
guildId: this.config.guildId,
|
||||
login: this.config.login,
|
||||
password: this.config.password,
|
||||
teamspeakQueryPort: this.config.teamspeakQueryPort,
|
||||
token: this.config.token
|
||||
} as QueryOptions) as qRes;
|
||||
|
||||
const raw = res.raw as { game: string; folder: string; };
|
||||
const game = raw.game || raw.folder || this.config.type;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import { createServer } from 'http';
|
||||
import { URL } from 'url';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { createServer } from 'node:http';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
@@ -30,22 +31,28 @@ interface ApiResponse {
|
||||
}
|
||||
}
|
||||
|
||||
const EXT_MIME: Record<string, string> = {
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'js': 'application/javascript',
|
||||
'json': 'application/json',
|
||||
'png': 'image/png'
|
||||
};
|
||||
|
||||
createServer(async (req, res) => {
|
||||
if (DBG) console.log('DBG: %j %j', (new Date()), req.url);
|
||||
|
||||
const reqUrl = new URL(req.url || '', 'http://localhost');
|
||||
const p = reqUrl.pathname === '/' ? 'index.html' : reqUrl.pathname.slice(1);
|
||||
const p = reqUrl.pathname === '/' ? 'index.html' : path.normalize(reqUrl.pathname).slice(1);
|
||||
const ext = path.extname(p).slice(1);
|
||||
|
||||
if (['index.html', 'main.css', 'main.js'].includes(p)) {
|
||||
if (ext in EXT_MIME && !p.includes('/') && !p.includes('\\')) {
|
||||
if (SECRET !== '') {
|
||||
let ct = 'text/html';
|
||||
if (p === 'main.css') ct = 'text/css';
|
||||
else if (p === 'main.js') ct = 'text/javascript';
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': ct,
|
||||
'Content-Type': EXT_MIME[ext] || 'plain/text',
|
||||
'Cache-Control': 'max-age=' + String(CACHE_MAX_AGE)
|
||||
});
|
||||
fs.createReadStream('./public/' + p).pipe(res);
|
||||
fs.createReadStream(path.resolve('./public/', p)).pipe(res);
|
||||
} else {
|
||||
res.end('Configure the `SECRET` env var to enable the web UI!');
|
||||
}
|
||||
|
||||
@@ -20,15 +20,28 @@ interface TelegramConfig {
|
||||
|
||||
export interface GameServerConfig {
|
||||
name: string;
|
||||
type: Type;
|
||||
host: string;
|
||||
port: number;
|
||||
appId?: number;//0
|
||||
updateIntervalMinutes?: number;//5
|
||||
graphHistoryHours?: number;//12
|
||||
timezoneOffset?: number;//0
|
||||
discord?: DiscordConfig[],
|
||||
discord?: DiscordConfig[];
|
||||
telegram?: TelegramConfig[];
|
||||
|
||||
// node-gamedig stuff
|
||||
type: Type;
|
||||
host: string;
|
||||
port: number;
|
||||
givenPortOnly?: boolean;
|
||||
requestRules?: boolean;
|
||||
// * Discord
|
||||
guildId?: string;
|
||||
// * Nadeo
|
||||
login?: string;
|
||||
password?: string;
|
||||
// * Teamspeak 3
|
||||
teamspeakQueryPort?: number;
|
||||
// * Terraria
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const adapter = new JSONFile<GameServerConfig[]>(DATA_PATH + 'default.config.json');
|
||||
|
||||