commit c290cf6cdcf022d0fe75bf07282e02b37b6017b4 Author: Dennis Heinrich Date: Fri Nov 15 21:26:20 2024 +0100 First commit of the bot; Created documentation in README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3be0636 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +/build/ +.idea +.ddev +config.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf3d70a --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Farming Simulator 25 - Discord Bot +## Create a Discord bot + +1. Open the [Discord Developer Portal](https://discord.com/developers/applications) +2. Click on `New Application` +3. Name your application +4. Click on `Bot` in the left menu +6. Click on `Copy` to copy the bot token +7. Click on `Installation` in the left menu +8. Add the Permission 'Administrator' to the bot guild installation (bottom of the page) +9. Click on `Copy` to copy the URL to install the bot to a guild +10. Your installation link url should look like this: `https://discord.com/oauth2/authorize?client_id=CLIENT_ID` + +## Install the bot + +1. Clone the repository +2. Move the `config.example.json` to `config.json` + 1. serverPassword: The password to the server + 2. serverStatsUrl: The feed URL to the server stats (from the web interface from the server) + 3. serverMapUrl: The feed URL to the server map (from the web interface from the server) + 4. guildId: The guild id where the bot should be installed + 5. channelId: The channel id where the bot should post the server stats + 6. botToken: The bot token from the Discord Developer Portal +3. Add the bot token to the `config.json` +4. Add the prefix to the `config.json` +4. Run `npm install` +5. Run `npm start` diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..743f2a4 --- /dev/null +++ b/config.example.json @@ -0,0 +1,13 @@ +{ + "application": { + "serverPassword": "TypeMyServerPasswordHere", + "serverStatsUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats.xml", + "serverMapUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats-map.jpg", + "updateIntervalSeconds": 30 + }, + "discord": { + "guildId": "DiscordServerId_12345", + "channelId": "DiscordChannelId_12345", + "botToken": "DiscordSecretBotToken_XYZ" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5806dd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,825 @@ +{ + "name": "ls25discord", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ls25discord", + "version": "0.0.1", + "license": "proprietary", + "dependencies": { + "discord.js": "^14.16.3", + "fast-xml-parser": "^4.5.0", + "json-schema-to-typescript": "^15.0.3", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.9.0.tgz", + "integrity": "sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.5.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "0.37.97", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.37.97", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", + "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", + "license": "MIT" + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.5.0.tgz", + "integrity": "sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "0.37.97" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.37.97", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", + "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", + "license": "MIT" + }, + "node_modules/@discordjs/rest": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.0.tgz", + "integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "0.37.97", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.37.97", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", + "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", + "license": "MIT" + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz", + "integrity": "sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.3.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.37.83", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.83.tgz", + "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==", + "license": "MIT" + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.100", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz", + "integrity": "sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.16.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.16.3.tgz", + "integrity": "sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.9.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.5.0", + "@discordjs/rest": "^2.4.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "1.1.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "0.37.100", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.3.tgz", + "integrity": "sha512-iOKdzTUWEVM4nlxpFudFsWyUiu/Jakkga4OZPEt7CGoSEsAsUgdOZqR6pcgx2STBek9Gm4hcarJpXSzIvZ/hKA==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "license": "MIT" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..84a2659 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "ls25discord", + "version": "0.0.1", + "description": "A simple discord bot for farming simulator 25", + "main": "source/Main.ts", + "scripts": { + "start": "npx tsc && node build/Main.js", + "schema": "npx json2ts -i source/Schema/ServerStats.json -o ./source/Schema/ServerStats.d.ts --unreachableDefinitions", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "ls25", + "fs25", + "farming", + "simulator", + "landwirtschafts", + "simulator" + ], + "author": "Dennis Heinrich", + "license": "proprietary", + "devDependencies": { + "@types/node": "^22.9.0", + "typescript": "^5.6.3" + }, + "dependencies": { + "discord.js": "^14.16.3", + "fast-xml-parser": "^4.5.0", + "json-schema-to-typescript": "^15.0.3", + "winston": "^3.17.0" + } +} diff --git a/source/Interfaces/Configuration/IApplicationConfiguration.ts b/source/Interfaces/Configuration/IApplicationConfiguration.ts new file mode 100644 index 0000000..8b7333b --- /dev/null +++ b/source/Interfaces/Configuration/IApplicationConfiguration.ts @@ -0,0 +1,6 @@ +export default interface IApplicationConfiguration { + serverStatsUrl: string; + serverMapUrl: string; + updateIntervalSeconds: number; + serverPassword: string; +} \ No newline at end of file diff --git a/source/Interfaces/Configuration/IConfiguration.ts b/source/Interfaces/Configuration/IConfiguration.ts new file mode 100644 index 0000000..ae36948 --- /dev/null +++ b/source/Interfaces/Configuration/IConfiguration.ts @@ -0,0 +1,7 @@ +import IDiscordConfiguration from "./IDiscordConfiguration"; +import IApplicationConfiguration from "./IApplicationConfiguration"; + +export default interface IConfiguration { + discord: IDiscordConfiguration; + application: IApplicationConfiguration; +} \ No newline at end of file diff --git a/source/Interfaces/Configuration/IDiscordConfiguration.ts b/source/Interfaces/Configuration/IDiscordConfiguration.ts new file mode 100644 index 0000000..63b9d6d --- /dev/null +++ b/source/Interfaces/Configuration/IDiscordConfiguration.ts @@ -0,0 +1,5 @@ +export default interface IDiscordConfiguration { + guildId: string; + channelId: string; + botToken: string; +} \ No newline at end of file diff --git a/source/Interfaces/Feed/IPlayer.ts b/source/Interfaces/Feed/IPlayer.ts new file mode 100644 index 0000000..de2d84b --- /dev/null +++ b/source/Interfaces/Feed/IPlayer.ts @@ -0,0 +1,6 @@ +export default interface IPlayer { + username: string; + isAdministrator: boolean; + sessionTime: number; + isUsed: boolean; +} \ No newline at end of file diff --git a/source/Main.ts b/source/Main.ts new file mode 100644 index 0000000..d7e89b6 --- /dev/null +++ b/source/Main.ts @@ -0,0 +1,95 @@ +import {Client, EmbedBuilder, IntentsBitField, Snowflake, TextChannel} from 'discord.js'; +import Configuration from "./Services/Configuration"; +import Logging from "./Services/Logging"; +import ServerStatsFeed from "./Services/ServerStatsFeed"; + +const appLogger = Logging.getLogger(); +const appConfig: Configuration = new Configuration(); +const appClient = new Client({ + intents: [IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMessages] +}); +appClient.login(appConfig.discord.botToken); + +/** + * Delete all messages in a text channel + * @param textChannel + */ +function deleteAllMessages(textChannel: TextChannel) { + appLogger.info(`Deleting all messages in channel ${textChannel.id}`); + textChannel.messages.fetch().then(messages => { + messages.forEach(message => { + message.delete(); + }); + }); +} + +/** + * Send server stats embed in a channel + * @param serverStats + */ +async function generateEmbedFromStatusFeed(serverStats: ServerStatsFeed): Promise { + let embed = new EmbedBuilder(); + embed.setTitle('Server Status'); + if (!serverStats.isOnline()) { + embed.setDescription('Der Server ist aktuell offline.'); + } else if (serverStats.isFetching()) { + embed.setDescription('Der Serverstatus wird aktuell abgefragt...'); + } else { + embed.setDescription(`Der Server ist aktuell ${serverStats.isOnline() ? 'online' : 'offline'}`); + embed.setTimestamp(); + embed.setThumbnail(Configuration.getConfiguration().application.serverMapUrl); + + let playerListString: string = ''; + if(serverStats.getPlayerList().length === 0) { + playerListString = 'Keine Spieler online'; + } else { + playerListString = serverStats.getPlayerList().map(p => p.username).join(', '); + } + + // @ts-ignore + embed.addFields( + {name: 'Name:', value: serverStats.getServerName()}, + {name: 'Passwort:', value: appConfig.application.serverPassword}, + {name: 'Uhrzeit im Spiel:', value: serverStats.getServerTime()}, + { + name: `Spieler online (${serverStats.getPlayerCount()}/${serverStats.getMaxPlayerCount()}):`, + value: playerListString + }, + ); + } + return embed; +} + +appClient.on('ready', () => { + appLogger.info(`Discord client ready. Logged in as ${appClient.user?.username}!`); + const serverStats = new ServerStatsFeed(); + + // Fetch channel and delete all messages + appClient.channels.fetch(appConfig.discord.channelId as Snowflake).then(channel => { + let textChannel = channel as TextChannel; + deleteAllMessages(textChannel); + }); + + // Start fetching server stats + (async () => { + await serverStats.updateServerFeed(); + let firstMessageId: any = null; + setInterval(async () => { + await serverStats.updateServerFeed(); + appClient.channels.fetch(appConfig.discord.channelId as Snowflake).then(async channel => { + generateEmbedFromStatusFeed(serverStats).then(embedMessage => { + console.log(embedMessage); + if (firstMessageId !== null) { + (channel as TextChannel).messages.fetch(firstMessageId).then(message => { + message.edit({embeds: [embedMessage]}); + }); + } else { + (channel as TextChannel).send({embeds: [embedMessage]}).then(message => { + firstMessageId = message.id; + }) + } + }); + }) + }, appConfig.application.updateIntervalSeconds * 1000); + })(); +}); diff --git a/source/Schema/ServerStats.d.ts b/source/Schema/ServerStats.d.ts new file mode 100644 index 0000000..ad636b6 --- /dev/null +++ b/source/Schema/ServerStats.d.ts @@ -0,0 +1,82 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface ServerStats { + Server: { + game: string; + version: string; + name: string; + mapName: string; + dayTime: number; + mapOverviewFilename: string; + mapSize: number; + Slots: { + capacity: number; + numUsed: number; + Player: { + isUsed: boolean; + isAdmin: boolean; + uptime: number; + x: number; + y: number; + z: number; + _text: string; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + Vehicles: { + Vehicle: { + name: string; + category: string; + type: string; + x: number; + y: number; + z: number; + fillTypes: string; + fillLevels: number; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + Mods: { + Mod: { + name: string; + author: string; + version: string; + hash: string; + _text: string; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + Farmlands: { + Farmland: { + name: string; + id: number; + owner: number; + area: number; + x: number; + z: number; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + Fields: { + Field: { + id: number; + x: number; + z: number; + isOwned: boolean; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + [k: string]: unknown; + }; + [k: string]: unknown; +} diff --git a/source/Schema/ServerStats.json b/source/Schema/ServerStats.json new file mode 100644 index 0000000..2ff02f2 --- /dev/null +++ b/source/Schema/ServerStats.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "Server": { + "type": "object", + "properties": { + "game": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "mapName": { "type": "string" }, + "dayTime": { "type": "integer" }, + "mapOverviewFilename": { "type": "string" }, + "mapSize": { "type": "integer" }, + "Slots": { + "type": "object", + "properties": { + "capacity": { "type": "integer" }, + "numUsed": { "type": "integer" }, + "Player": { + "type": "object", + "properties": { + "isUsed": { "type": "boolean" }, + "isAdmin": { "type": "boolean" }, + "uptime": { "type": "integer" }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "z": { "type": "number" }, + "_text": { "type": "string" } + }, + "required": ["isUsed", "isAdmin", "uptime", "x", "y", "z", "_text"] + } + }, + "required": ["capacity", "numUsed", "Player"] + }, + "Vehicles": { + "type": "object", + "properties": { + "Vehicle": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "category": { "type": "string" }, + "type": { "type": "string" }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "z": { "type": "number" }, + "fillTypes": { "type": "string" }, + "fillLevels": { "type": "number" } + }, + "required": ["name", "category", "type", "x", "y", "z", "fillTypes", "fillLevels"] + } + }, + "required": ["Vehicle"] + }, + "Mods": { + "type": "object", + "properties": { + "Mod": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "author": { "type": "string" }, + "version": { "type": "string" }, + "hash": { "type": "string" }, + "_text": { "type": "string" } + }, + "required": ["name", "author", "version", "hash", "_text"] + } + }, + "required": ["Mod"] + }, + "Farmlands": { + "type": "object", + "properties": { + "Farmland": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "id": { "type": "integer" }, + "owner": { "type": "integer" }, + "area": { "type": "integer" }, + "x": { "type": "number" }, + "z": { "type": "number" } + }, + "required": ["name", "id", "owner", "area", "x", "z"] + } + }, + "required": ["Farmland"] + }, + "Fields": { + "type": "object", + "properties": { + "Field": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "x": { "type": "number" }, + "z": { "type": "number" }, + "isOwned": { "type": "boolean" } + }, + "required": ["id", "x", "z", "isOwned"] + } + }, + "required": ["Field"] + } + }, + "required": ["game", "version", "name", "mapName", "dayTime", "mapOverviewFilename", "mapSize", "Slots", "Vehicles", "Mods", "Farmlands", "Fields"] + } + }, + "required": ["Server"] +} diff --git a/source/Services/Configuration.ts b/source/Services/Configuration.ts new file mode 100644 index 0000000..83f8bd9 --- /dev/null +++ b/source/Services/Configuration.ts @@ -0,0 +1,18 @@ +import IDiscordConfiguration from "../Interfaces/Configuration/IDiscordConfiguration"; +import IApplicationConfiguration from "../Interfaces/Configuration/IApplicationConfiguration"; +import IConfiguration from "../Interfaces/Configuration/IConfiguration"; + +export default class Configuration implements IConfiguration{ + public readonly discord: IDiscordConfiguration; + public readonly application: IApplicationConfiguration; + + constructor() { + let config = require('../../config.json'); + this.discord = config.discord; + this.application = config.application; + } + + public static getConfiguration(): IConfiguration { + return new Configuration(); + } +} \ No newline at end of file diff --git a/source/Services/Logging.ts b/source/Services/Logging.ts new file mode 100644 index 0000000..4263dd6 --- /dev/null +++ b/source/Services/Logging.ts @@ -0,0 +1,16 @@ +import winston, {Logger} from "winston"; + +export default class Logging { + public static getLogger(): Logger { + return winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple(), + ), + transports: [ + new winston.transports.Console(), + ] + }); + } +} \ No newline at end of file diff --git a/source/Services/ServerStatsFeed.ts b/source/Services/ServerStatsFeed.ts new file mode 100644 index 0000000..976bbd7 --- /dev/null +++ b/source/Services/ServerStatsFeed.ts @@ -0,0 +1,171 @@ +import {ServerStats} from "../Schema/ServerStats"; +import Configuration from "./Configuration"; +import {XMLParser} from "fast-xml-parser"; +import Logging from "./Logging"; +import IPlayer from "../Interfaces/Feed/IPlayer"; + +export const CONNECTION_REFUSED = 'ECONNREFUSED'; +export const NOT_FOUND = 'ENOTFOUND'; + +export default class ServerStatsFeed { + private _serverStats: ServerStats | null = null; + private _isOnline: boolean = false; + private _isFetching: boolean = false; + + constructor() { + } + + public isFetching(): boolean { + return this._isFetching; + } + + private getServerStats(): ServerStats | null { + if(this._isOnline && !this._isFetching && this._serverStats) { + return this._serverStats; + } + return null; + } + + public async updateServerFeed(): Promise { + this._isFetching = true; + Logging.getLogger().info(`Fetching server stats from feed url`); + await fetch(Configuration.getConfiguration().application.serverStatsUrl) + .then( + r => r.text() + ).then( + (response) => { + // Set online status to true + this._isOnline = true; + + // Parse the XML response + const parsedFeed = new XMLParser({ignoreAttributes: false, attributeNamePrefix: ''}).parse(response) as ServerStats; + Logging.getLogger().info(`Server stats received`); + this._serverStats = parsedFeed; + } + ).catch( + (reason) => { + // Set online status to false + this._isOnline = false; + + // Handle different error codes + switch (reason.cause.code) { + case CONNECTION_REFUSED: + Logging.getLogger().error(`Connection refused to server stats feed`); + break; + case NOT_FOUND: + Logging.getLogger().error(`Server stats feed not found`); + break; + default: + Logging.getLogger().error(`Error fetching server stats`); + break; + } + return null; + }) + .finally(() => { + // Set fetching status to false after fetching is done or failed + this._isFetching = false; + }); + return this._serverStats; + } + + public isOnline(): boolean { + return this._isOnline; + } + + /** + * Returns the server name + * @returns {string} The server name + */ + public getServerName(): string { + return this.getServerStats()?.Server.name; + } + + /** + * Returns the server map name + * @returns {string} The server map name + */ + public getServerMap(): string { + return this.getServerStats()?.Server.map; + } + + /** + * Returns the server time in decimal format + * @returns {number} The server time in decimal format + */ + public getServerTimeDecimal(): number { + let dayTime = this.getServerStats()?.Server.dayTime; + if (dayTime === undefined) { + return 0; + } + return dayTime / (60 * 60 * 1000) + 0.0001; + } + + /** + * Returns the server time in the format HH:MM + * @returns {string} The server time in the format HH:MM + */ + public getServerTime(): string { + let decimalTime = this.getServerTimeDecimal(); + if(decimalTime === 0) { + return "00:00"; + } + let hours = Math.floor(decimalTime); + let minutes = Math.floor((decimalTime - hours) * 60); + let hoursString = hours.toString(); + let minutesString = minutes.toString(); + if(hoursString.length === 1) { + hoursString = `0${hoursString}`; + } + if(minutesString.length === 1) { + minutesString = `0${minutesString}`; + } + return `${hoursString}:${minutesString}`; + } + + /** + * Returns the server player count + * @returns {number} The server player count + */ + public getPlayerCount(): number { + return this.getServerStats()?.Server.Slots.numUsed; + } + + /** + * Returns the server player count + * @returns {number} The server player count + */ + public getMaxPlayerCount(): number { + return this.getServerStats()?.Server.Slots.capacity; + } + + /** + * Returns the player list from the server stats feed + * @returns {IPlayer[]} The online player list as an array of IPlayer objects + */ + public getPlayerList(): IPlayer[] { + let mappedPlayers: IPlayer[]; + let returnPlayers: IPlayer[] = []; + let playerList = this.getServerStats()?.Server.Slots.Player; + if (Array.isArray(playerList)) { + mappedPlayers = playerList.map((player) => { + return { + username: player['#text'], + isAdministrator: player.isAdmin === 'true', + sessionTime: parseInt(player.uptime), + isUsed: player.isUsed === 'true', + } as IPlayer; + }); + } else { + mappedPlayers = []; + } + + // Filter out player slots that are not used + mappedPlayers.forEach((player) => { + if(player.isUsed) { + returnPlayers.push(player); + } + }); + + return returnPlayers; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f0feaa5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "allowJs": true, + "outDir": "build", + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "module": "commonjs", + "rootDir": "source", + "target": "es2016", + "lib": ["es6"], + } +} \ No newline at end of file