├── .github └── workflows │ └── docker-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── SETTINGS.md ├── config.example-de.json ├── config.example-en.json ├── docker-compose.yml ├── misc ├── files │ ├── example-economy.xml │ ├── example-savegame.xml │ └── example-stats.xml └── images │ ├── readme │ ├── bot_terminal.png │ ├── discord_de.png │ └── discord_en.png │ └── socials │ ├── GitHub Repository Header.afphoto │ └── GitHub Repository Header.png ├── package.json ├── pnpm-lock.yaml ├── source ├── Interfaces │ ├── Configuration │ │ ├── IApplicationConfiguration.ts │ │ ├── IConfiguration.ts │ │ ├── IDiscordConfiguration.ts │ │ ├── ITranslation.ts │ │ ├── ITranslationCommon.ts │ │ └── ITranslationDiscordEmbed.ts │ └── Feed │ │ ├── IMod.ts │ │ └── IPlayer.ts ├── Main.ts ├── Schema │ ├── ServerStats.d.ts │ └── ServerStats.json └── Services │ ├── Configuration.ts │ ├── DiscordEmbed.ts │ ├── Logging.ts │ ├── ServerStatusFeed.ts │ └── VersionChecker.ts └── tsconfig.json /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | jobs: 16 | build-and-push: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | uses: docker/metadata-action@v5 35 | with: 36 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@v5 40 | with: 41 | context: . 42 | push: true 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | .idea 4 | .ddev 5 | config.json 6 | config.prod.json 7 | config.test.json 8 | config.dev.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | LABEL name="LS25-Discord-Bot" 3 | LABEL authors="Dennis Heinrich" 4 | 5 | # Copy the source files 6 | WORKDIR /app 7 | COPY . /app 8 | RUN npm install pnpm -g 9 | RUN pnpm install 10 | RUN pnpm run build 11 | 12 | ## Simplyfy the rm commands 13 | RUN rm -rf .ddev/ source/ misc/ .git .gitignore config.example-en.json Dockerfile docker-compose.yml README.md 14 | 15 | CMD ["npm", "run", "start-only"] 16 | ENTRYPOINT ["npm", "run", "start-only"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 - Dennis Heinrich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Farming Simulator 25 - Discord Bot 2 | 3 | This bot periodically updates a Discord channel with stats from a Farming Simulator 25 server. 4 | It posts the server name, password, time, and player count. Written in Node.js, it uses the 5 | discord.js library to interact with Discord and fetches server stats via the XML feed 6 | (accessible through the server's web interface). The update interval is configurable. 7 | 8 | ## Screenshots 9 | 10 |
11 | Discord embed in english 12 | 13 | ![discord_en.png](misc%2Fimages%2Freadme%2Fdiscord_en.png) 14 | 15 |
16 | 17 |
18 | Discord embed in german 19 | 20 | ![discord_de.png](misc%2Fimages%2Freadme%2Fdiscord_de.png) 21 | 22 |
23 | 24 |
25 | Terminal output (NodeJS) 26 | 27 | ![bot_terminal.png](misc%2Fimages%2Freadme%2Fbot_terminal.png) 28 | 29 |
30 | 31 | ## Requirements 32 | 33 | - **Node.js**: Required if you want to run the bot without Docker. 34 | - **NPM**: Required if you want to run the bot without Docker. 35 | - **Docker (optional)**: Use Docker if you prefer running the bot in a containerized environment. 36 | 37 | --- 38 | 39 | ## Installation Guide 40 | 41 | ### Step 1: Create a Discord Bot 42 | 43 | 1. Open the [Discord Developer Portal](https://discord.com/developers/applications). 44 | 2. Click on `New Application` and give your application a name. 45 | 3. Navigate to the `Bot` section in the left menu and click on `Add Bot`. 46 | 4. Copy the bot token by clicking `Copy` (you'll need this later). 47 | 5. Go to the `OAuth2` > `URL Generator` section in the left menu. 48 | 6. Under "Scopes," select `bot`, and under "Bot Permissions," select `Administrator`. 49 | 7. Copy the generated URL to invite the bot to your Discord server. 50 | - The URL should look like this: 51 | `https://discord.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot&permissions=8` 52 | 53 | --- 54 | 55 | ### Step 2: Configure the Bot 56 | 57 | 1. Clone the repository to your server 58 | 2. Locate the configuration files: 59 | - Use either 60 | - `config.example-de.json` (for German) 61 | - `config.example-en.json` (for English) 62 | - Rename the chosen file to `config.json`. 63 | 3. Open `config.json` and fill in the required fields: 64 | - Refer to `SETTINGS.md` for detailed descriptions of each field. 65 | - Fields marked with `(*)` are important to check; other fields can be left empty for default values. 66 | 67 | --- 68 | 69 | ## Running the Bot 70 | 71 | ### Option 1: Run Inside a Docker Container (Recommended) 72 | 73 | 1. Navigate to the root directory of the cloned repository. 74 | 2. Build and start the container: 75 | 76 | ```bash 77 | docker-compose up -d --build 78 | ``` 79 | 80 | 3. The bot should now be running and posting server stats to the specified Discord channel. 81 | 82 | ### Option 2: Run Without Docker (Using Node.js) 83 | 84 | 1. Navigate to the root directory of the cloned repository. 85 | 2. Install dependencies: 86 | 87 | ```bash 88 | npm install 89 | ``` 90 | 91 | 3. Start the bot: 92 | 93 | ```bash 94 | npm start 95 | ``` 96 | 97 | 4. The bot should now be running and posting server stats to the specified Discord channel. 98 | - Note: Closing the terminal will stop the bot. Use a process manager like [PM2](https://pm2.io/) to keep it running. 99 | -------------------------------------------------------------------------------- /SETTINGS.md: -------------------------------------------------------------------------------- 1 | # Settings and configuration 2 | 3 | These are the settings that can be configured in the `config.json` file. The file is located in the root directory of the project. All 4 | fields marked with `(*)` are required to be checked, or leave empty for default values. 5 | 6 | | **- Key -** | **- Description -** | 7 | |----------------------------------------------|---------------------------------------------------------------------------| 8 | | (*) application.serverPassword | The password to join the server (or leave empty) | 9 | | (*) application.serverStatsUrl | The feed URL to the server stats (from the web interface from the server) | 10 | | (*) application.serverMapUrl | The feed URL to the server map (from the web interface from the server) | 11 | | (*) application.updateIntervalSeconds | The interval in seconds to update the server stats | 12 | | (*) discord.channelId | The channel id where the bot should post the server stats | 13 | | (*) discord.botToken | The bot token from the Discord Developer Portal | 14 | | translation.discordEmbed.title | The title of the Discord embed | 15 | | translation.discordEmbed.descriptionOnline | The description when the server is online | 16 | | translation.discordEmbed.descriptionOffline | The description when the server is offline | 17 | | translation.discordEmbed.descriptionUnknown | The description when the server status is unknown | 18 | | translation.discordEmbed.titleServerName | The title of the server name | 19 | | translation.discordEmbed.titleServerPassword | The title of the server password | 20 | | translation.discordEmbed.titleServerTime | The title of the server time | 21 | | translation.discordEmbed.titlePlayerCount | The title of the player count | 22 | | translation.discordEmbed.noPlayersOnline | The message when no players are online | 23 | | translation.discordEmbed.titleServerMap | The title of the server map | 24 | | translation.discordEmbed.titleServerMods | The title of the server mods | 25 | | translation.common.monthJanuary | The month January in the language of the server | 26 | | translation.common.monthFebruary | The month February in the language of the server | 27 | | translation.common.monthMarch | The month March in the language of the server | 28 | | translation.common.monthApril | The month April in the language of the server | 29 | | translation.common.monthMay | The month May in the language of the server | 30 | | translation.common.monthJune | The month June in the language of the server | 31 | | translation.common.monthJuly | The month July in the language of the server | 32 | | translation.common.monthAugust | The month August in the language of the server | 33 | | translation.common.monthSeptember | The month September in the language of the server | 34 | | translation.common.monthOctober | The month October in the language of the server | 35 | | translation.common.monthNovember | The month November in the language of the server | 36 | | translation.common.monthDecember | The month December in the language of the server | 37 | -------------------------------------------------------------------------------- /config.example-de.json: -------------------------------------------------------------------------------- 1 | { 2 | "application": { 3 | "serverPassword": "TypeMyServerPasswordHere", 4 | "serverStatsUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats.xml", 5 | "serverMapUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats-map.jpg", 6 | "updateIntervalSeconds": 30 7 | }, 8 | "discord": { 9 | "channelId": "DiscordChannelId_12345", 10 | "botToken": "DiscordSecretBotToken_XYZ" 11 | }, 12 | "translation": { 13 | "discordEmbed": { 14 | "title": "LS25 Server Status", 15 | "descriptionOnline": "Der Server ist online", 16 | "descriptionOffline": "Der Server ist offline", 17 | "descriptionUnknown": "Serverdaten werden abgerufen", 18 | "titleServerName": "Server-Name:", 19 | "titleServerMap": "Server-Karte:", 20 | "titleServerMods": "Server-Mods:", 21 | "titleServerPassword": "Server-Passwort:", 22 | "titleServerTime": "Server-Zeit:", 23 | "titlePlayerCount": "Spieler online:", 24 | "noPlayersOnline": "Keine Spieler online" 25 | }, 26 | "common": { 27 | "monthJanuary": "Januar", 28 | "monthFebruary": "Februar", 29 | "monthMarch": "März", 30 | "monthApril": "April", 31 | "monthMay": "Mai", 32 | "monthJune": "Juni", 33 | "monthJuly": "Juli", 34 | "monthAugust": "August", 35 | "monthSeptember": "September", 36 | "monthOctober": "Oktober", 37 | "monthNovember": "November", 38 | "monthDecember": "Dezember" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /config.example-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "application": { 3 | "serverPassword": "TypeMyServerPasswordHere", 4 | "serverStatsUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats.xml", 5 | "serverMapUrl": "http://1.1.1.1:8080/feed/dedicated-server-stats-map.jpg", 6 | "updateIntervalSeconds": 30 7 | }, 8 | "discord": { 9 | "channelId": "DiscordChannelId_12345", 10 | "botToken": "DiscordSecretBotToken_XYZ" 11 | }, 12 | "translation": { 13 | "discordEmbed": { 14 | "title": "Server Status", 15 | "descriptionOnline": "Server is online", 16 | "descriptionOffline": "Server is offline", 17 | "descriptionUnknown": "Server status fetching", 18 | "titleServerName": "Server name", 19 | "titleServerMap": "Server map", 20 | "titleServerMods": "Server mods", 21 | "titleServerPassword": "Server password", 22 | "titleServerTime": "Server time", 23 | "titlePlayerCount": "Players online", 24 | "noPlayersOnline": "No players online" 25 | }, 26 | "common": { 27 | "monthJanuary": "January", 28 | "monthFebruary": "February", 29 | "monthMarch": "March", 30 | "monthApril": "April", 31 | "monthMay": "May", 32 | "monthJune": "June", 33 | "monthJuly": "July", 34 | "monthAugust": "August", 35 | "monthSeptember": "September", 36 | "monthOctober": "October", 37 | "monthNovember": "November", 38 | "monthDecember": "December" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | ls25bot: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: ls25bot 9 | restart: always 10 | volumes: 11 | - ./config.json:/app/config.json 12 | 13 | restart: 14 | image: docker:cli 15 | restart: unless-stopped 16 | container_name: ls25bot-restart 17 | volumes: 18 | - /var/run/docker.sock:/var/run/docker.sock 19 | entrypoint: [ "/bin/sh","-c" ] 20 | command: 21 | - | 22 | echo "Restarting ls25bot container is running" 23 | while true; do 24 | sleep 7200 # Each 2 hours 25 | echo "Restarting ls25bot container at $(date)" 26 | docker restart ls25bot 27 | done -------------------------------------------------------------------------------- /misc/files/example-savegame.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Server Name 5 | 2024-11-12 6 | MapUS 7 | Riverbend Springs 8 | 18.11.2024 9 | 2024-11-18 10 | 1000000 11 | 0 12 | NORMAL 13 | false 14 | false 15 | true 16 | true 17 | true 18 | false 19 | false 20 | 2 21 | 1 22 | true 23 | true 24 | false 25 | true 26 | true 27 | true 28 | 2 29 | false 30 | false 31 | false 32 | 1 33 | 1 34 | 4 35 | 1 36 | 2 37 | 2 38 | 2 39 | 2 40 | 2 41 | 1 42 | 1 43 | 1 44 | 1 45 | 1 46 | 1 47 | 1 48 | VISUALS_ONLY 49 | 4 50 | 5.000000 51 | 60.000000 52 | 53 | 54 | 00000000000000000000 55 | 56 | 57 | 58 | 59 | 60 | 61 | 233766 62 | 6849.572754 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /misc/files/example-stats.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Player 1 5 | Player 2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | MacDon Pack 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | -------------------------------------------------------------------------------- /misc/images/readme/bot_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/4e6668c2e5b630fb08c767d29c60e52aa2d673e1/misc/images/readme/bot_terminal.png -------------------------------------------------------------------------------- /misc/images/readme/discord_de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/4e6668c2e5b630fb08c767d29c60e52aa2d673e1/misc/images/readme/discord_de.png -------------------------------------------------------------------------------- /misc/images/readme/discord_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/4e6668c2e5b630fb08c767d29c60e52aa2d673e1/misc/images/readme/discord_en.png -------------------------------------------------------------------------------- /misc/images/socials/GitHub Repository Header.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/4e6668c2e5b630fb08c767d29c60e52aa2d673e1/misc/images/socials/GitHub Repository Header.afphoto -------------------------------------------------------------------------------- /misc/images/socials/GitHub Repository Header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/4e6668c2e5b630fb08c767d29c60e52aa2d673e1/misc/images/socials/GitHub Repository Header.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ls25-discord-bot", 3 | "version": "0.1.8", 4 | "description": "A simple discord bot for farming simulator 25", 5 | "main": "source/Main.ts", 6 | "scripts": { 7 | "start": "npx tsc && node build/Main.js", 8 | "start-only": "node build/Main.js", 9 | "build": "npx tsc", 10 | "schema": "npx json2ts -i source/Schema/ServerStats.json -o ./source/Schema/ServerStats.d.ts --unreachableDefinitions", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "ls25", 15 | "fs25", 16 | "farming", 17 | "simulator", 18 | "landwirtschafts", 19 | "simulator" 20 | ], 21 | "author": "Dennis Heinrich", 22 | "license": "proprietary", 23 | "devDependencies": { 24 | "@types/node": "^22.9.0", 25 | "json-schema-to-typescript": "^15.0.3", 26 | "typescript": "^5.6.3" 27 | }, 28 | "dependencies": { 29 | "discord.js": "^14.16.3", 30 | "fast-xml-parser": "^4.5.0", 31 | "winston": "^3.17.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | discord.js: 12 | specifier: ^14.16.3 13 | version: 14.16.3 14 | fast-xml-parser: 15 | specifier: ^4.5.0 16 | version: 4.5.0 17 | winston: 18 | specifier: ^3.17.0 19 | version: 3.17.0 20 | devDependencies: 21 | '@types/node': 22 | specifier: ^22.9.0 23 | version: 22.10.1 24 | json-schema-to-typescript: 25 | specifier: ^15.0.3 26 | version: 15.0.3 27 | typescript: 28 | specifier: ^5.6.3 29 | version: 5.7.2 30 | 31 | packages: 32 | 33 | '@apidevtools/json-schema-ref-parser@11.7.2': 34 | resolution: {integrity: sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==} 35 | engines: {node: '>= 16'} 36 | 37 | '@colors/colors@1.6.0': 38 | resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} 39 | engines: {node: '>=0.1.90'} 40 | 41 | '@dabh/diagnostics@2.0.3': 42 | resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} 43 | 44 | '@discordjs/builders@1.9.0': 45 | resolution: {integrity: sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==} 46 | engines: {node: '>=18'} 47 | 48 | '@discordjs/collection@1.5.3': 49 | resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} 50 | engines: {node: '>=16.11.0'} 51 | 52 | '@discordjs/collection@2.1.1': 53 | resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} 54 | engines: {node: '>=18'} 55 | 56 | '@discordjs/formatters@0.5.0': 57 | resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} 58 | engines: {node: '>=18'} 59 | 60 | '@discordjs/rest@2.4.0': 61 | resolution: {integrity: sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==} 62 | engines: {node: '>=18'} 63 | 64 | '@discordjs/util@1.1.1': 65 | resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} 66 | engines: {node: '>=18'} 67 | 68 | '@discordjs/ws@1.1.1': 69 | resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} 70 | engines: {node: '>=16.11.0'} 71 | 72 | '@jsdevtools/ono@7.1.3': 73 | resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} 74 | 75 | '@sapphire/async-queue@1.5.5': 76 | resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} 77 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 78 | 79 | '@sapphire/shapeshift@4.0.0': 80 | resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} 81 | engines: {node: '>=v16'} 82 | 83 | '@sapphire/snowflake@3.5.3': 84 | resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} 85 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 86 | 87 | '@types/json-schema@7.0.15': 88 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 89 | 90 | '@types/lodash@4.17.13': 91 | resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} 92 | 93 | '@types/node@22.10.1': 94 | resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} 95 | 96 | '@types/triple-beam@1.3.5': 97 | resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} 98 | 99 | '@types/ws@8.5.13': 100 | resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} 101 | 102 | '@vladfrangu/async_event_emitter@2.4.6': 103 | resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} 104 | engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 105 | 106 | argparse@2.0.1: 107 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 108 | 109 | async@3.2.6: 110 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 111 | 112 | color-convert@1.9.3: 113 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 114 | 115 | color-name@1.1.3: 116 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 117 | 118 | color-name@1.1.4: 119 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 120 | 121 | color-string@1.9.1: 122 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 123 | 124 | color@3.2.1: 125 | resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} 126 | 127 | colorspace@1.1.4: 128 | resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} 129 | 130 | discord-api-types@0.37.100: 131 | resolution: {integrity: sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==} 132 | 133 | discord-api-types@0.37.83: 134 | resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} 135 | 136 | discord-api-types@0.37.97: 137 | resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==} 138 | 139 | discord.js@14.16.3: 140 | resolution: {integrity: sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==} 141 | engines: {node: '>=18'} 142 | 143 | enabled@2.0.0: 144 | resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} 145 | 146 | fast-deep-equal@3.1.3: 147 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 148 | 149 | fast-xml-parser@4.5.0: 150 | resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} 151 | hasBin: true 152 | 153 | fdir@6.4.2: 154 | resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} 155 | peerDependencies: 156 | picomatch: ^3 || ^4 157 | peerDependenciesMeta: 158 | picomatch: 159 | optional: true 160 | 161 | fecha@4.2.3: 162 | resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} 163 | 164 | fn.name@1.1.0: 165 | resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} 166 | 167 | inherits@2.0.4: 168 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 169 | 170 | is-arrayish@0.3.2: 171 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 172 | 173 | is-extglob@2.1.1: 174 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 175 | engines: {node: '>=0.10.0'} 176 | 177 | is-glob@4.0.3: 178 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 179 | engines: {node: '>=0.10.0'} 180 | 181 | is-stream@2.0.1: 182 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 183 | engines: {node: '>=8'} 184 | 185 | js-yaml@4.1.0: 186 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 187 | hasBin: true 188 | 189 | json-schema-to-typescript@15.0.3: 190 | resolution: {integrity: sha512-iOKdzTUWEVM4nlxpFudFsWyUiu/Jakkga4OZPEt7CGoSEsAsUgdOZqR6pcgx2STBek9Gm4hcarJpXSzIvZ/hKA==} 191 | engines: {node: '>=16.0.0'} 192 | hasBin: true 193 | 194 | kuler@2.0.0: 195 | resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} 196 | 197 | lodash.snakecase@4.1.1: 198 | resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} 199 | 200 | lodash@4.17.21: 201 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 202 | 203 | logform@2.7.0: 204 | resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} 205 | engines: {node: '>= 12.0.0'} 206 | 207 | magic-bytes.js@1.10.0: 208 | resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} 209 | 210 | minimist@1.2.8: 211 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 212 | 213 | ms@2.1.3: 214 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 215 | 216 | one-time@1.0.0: 217 | resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} 218 | 219 | picomatch@4.0.2: 220 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 221 | engines: {node: '>=12'} 222 | 223 | prettier@3.4.1: 224 | resolution: {integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==} 225 | engines: {node: '>=14'} 226 | hasBin: true 227 | 228 | readable-stream@3.6.2: 229 | resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 230 | engines: {node: '>= 6'} 231 | 232 | safe-buffer@5.2.1: 233 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 234 | 235 | safe-stable-stringify@2.5.0: 236 | resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 237 | engines: {node: '>=10'} 238 | 239 | simple-swizzle@0.2.2: 240 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 241 | 242 | stack-trace@0.0.10: 243 | resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} 244 | 245 | string_decoder@1.3.0: 246 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 247 | 248 | strnum@1.0.5: 249 | resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} 250 | 251 | text-hex@1.0.0: 252 | resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} 253 | 254 | tinyglobby@0.2.10: 255 | resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} 256 | engines: {node: '>=12.0.0'} 257 | 258 | triple-beam@1.4.1: 259 | resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} 260 | engines: {node: '>= 14.0.0'} 261 | 262 | ts-mixer@6.0.4: 263 | resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} 264 | 265 | tslib@2.8.1: 266 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 267 | 268 | typescript@5.7.2: 269 | resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} 270 | engines: {node: '>=14.17'} 271 | hasBin: true 272 | 273 | undici-types@6.20.0: 274 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 275 | 276 | undici@6.19.8: 277 | resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} 278 | engines: {node: '>=18.17'} 279 | 280 | util-deprecate@1.0.2: 281 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 282 | 283 | winston-transport@4.9.0: 284 | resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} 285 | engines: {node: '>= 12.0.0'} 286 | 287 | winston@3.17.0: 288 | resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} 289 | engines: {node: '>= 12.0.0'} 290 | 291 | ws@8.18.0: 292 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 293 | engines: {node: '>=10.0.0'} 294 | peerDependencies: 295 | bufferutil: ^4.0.1 296 | utf-8-validate: '>=5.0.2' 297 | peerDependenciesMeta: 298 | bufferutil: 299 | optional: true 300 | utf-8-validate: 301 | optional: true 302 | 303 | snapshots: 304 | 305 | '@apidevtools/json-schema-ref-parser@11.7.2': 306 | dependencies: 307 | '@jsdevtools/ono': 7.1.3 308 | '@types/json-schema': 7.0.15 309 | js-yaml: 4.1.0 310 | 311 | '@colors/colors@1.6.0': {} 312 | 313 | '@dabh/diagnostics@2.0.3': 314 | dependencies: 315 | colorspace: 1.1.4 316 | enabled: 2.0.0 317 | kuler: 2.0.0 318 | 319 | '@discordjs/builders@1.9.0': 320 | dependencies: 321 | '@discordjs/formatters': 0.5.0 322 | '@discordjs/util': 1.1.1 323 | '@sapphire/shapeshift': 4.0.0 324 | discord-api-types: 0.37.97 325 | fast-deep-equal: 3.1.3 326 | ts-mixer: 6.0.4 327 | tslib: 2.8.1 328 | 329 | '@discordjs/collection@1.5.3': {} 330 | 331 | '@discordjs/collection@2.1.1': {} 332 | 333 | '@discordjs/formatters@0.5.0': 334 | dependencies: 335 | discord-api-types: 0.37.97 336 | 337 | '@discordjs/rest@2.4.0': 338 | dependencies: 339 | '@discordjs/collection': 2.1.1 340 | '@discordjs/util': 1.1.1 341 | '@sapphire/async-queue': 1.5.5 342 | '@sapphire/snowflake': 3.5.3 343 | '@vladfrangu/async_event_emitter': 2.4.6 344 | discord-api-types: 0.37.97 345 | magic-bytes.js: 1.10.0 346 | tslib: 2.8.1 347 | undici: 6.19.8 348 | 349 | '@discordjs/util@1.1.1': {} 350 | 351 | '@discordjs/ws@1.1.1': 352 | dependencies: 353 | '@discordjs/collection': 2.1.1 354 | '@discordjs/rest': 2.4.0 355 | '@discordjs/util': 1.1.1 356 | '@sapphire/async-queue': 1.5.5 357 | '@types/ws': 8.5.13 358 | '@vladfrangu/async_event_emitter': 2.4.6 359 | discord-api-types: 0.37.83 360 | tslib: 2.8.1 361 | ws: 8.18.0 362 | transitivePeerDependencies: 363 | - bufferutil 364 | - utf-8-validate 365 | 366 | '@jsdevtools/ono@7.1.3': {} 367 | 368 | '@sapphire/async-queue@1.5.5': {} 369 | 370 | '@sapphire/shapeshift@4.0.0': 371 | dependencies: 372 | fast-deep-equal: 3.1.3 373 | lodash: 4.17.21 374 | 375 | '@sapphire/snowflake@3.5.3': {} 376 | 377 | '@types/json-schema@7.0.15': {} 378 | 379 | '@types/lodash@4.17.13': {} 380 | 381 | '@types/node@22.10.1': 382 | dependencies: 383 | undici-types: 6.20.0 384 | 385 | '@types/triple-beam@1.3.5': {} 386 | 387 | '@types/ws@8.5.13': 388 | dependencies: 389 | '@types/node': 22.10.1 390 | 391 | '@vladfrangu/async_event_emitter@2.4.6': {} 392 | 393 | argparse@2.0.1: {} 394 | 395 | async@3.2.6: {} 396 | 397 | color-convert@1.9.3: 398 | dependencies: 399 | color-name: 1.1.3 400 | 401 | color-name@1.1.3: {} 402 | 403 | color-name@1.1.4: {} 404 | 405 | color-string@1.9.1: 406 | dependencies: 407 | color-name: 1.1.4 408 | simple-swizzle: 0.2.2 409 | 410 | color@3.2.1: 411 | dependencies: 412 | color-convert: 1.9.3 413 | color-string: 1.9.1 414 | 415 | colorspace@1.1.4: 416 | dependencies: 417 | color: 3.2.1 418 | text-hex: 1.0.0 419 | 420 | discord-api-types@0.37.100: {} 421 | 422 | discord-api-types@0.37.83: {} 423 | 424 | discord-api-types@0.37.97: {} 425 | 426 | discord.js@14.16.3: 427 | dependencies: 428 | '@discordjs/builders': 1.9.0 429 | '@discordjs/collection': 1.5.3 430 | '@discordjs/formatters': 0.5.0 431 | '@discordjs/rest': 2.4.0 432 | '@discordjs/util': 1.1.1 433 | '@discordjs/ws': 1.1.1 434 | '@sapphire/snowflake': 3.5.3 435 | discord-api-types: 0.37.100 436 | fast-deep-equal: 3.1.3 437 | lodash.snakecase: 4.1.1 438 | tslib: 2.8.1 439 | undici: 6.19.8 440 | transitivePeerDependencies: 441 | - bufferutil 442 | - utf-8-validate 443 | 444 | enabled@2.0.0: {} 445 | 446 | fast-deep-equal@3.1.3: {} 447 | 448 | fast-xml-parser@4.5.0: 449 | dependencies: 450 | strnum: 1.0.5 451 | 452 | fdir@6.4.2(picomatch@4.0.2): 453 | optionalDependencies: 454 | picomatch: 4.0.2 455 | 456 | fecha@4.2.3: {} 457 | 458 | fn.name@1.1.0: {} 459 | 460 | inherits@2.0.4: {} 461 | 462 | is-arrayish@0.3.2: {} 463 | 464 | is-extglob@2.1.1: {} 465 | 466 | is-glob@4.0.3: 467 | dependencies: 468 | is-extglob: 2.1.1 469 | 470 | is-stream@2.0.1: {} 471 | 472 | js-yaml@4.1.0: 473 | dependencies: 474 | argparse: 2.0.1 475 | 476 | json-schema-to-typescript@15.0.3: 477 | dependencies: 478 | '@apidevtools/json-schema-ref-parser': 11.7.2 479 | '@types/json-schema': 7.0.15 480 | '@types/lodash': 4.17.13 481 | is-glob: 4.0.3 482 | js-yaml: 4.1.0 483 | lodash: 4.17.21 484 | minimist: 1.2.8 485 | prettier: 3.4.1 486 | tinyglobby: 0.2.10 487 | 488 | kuler@2.0.0: {} 489 | 490 | lodash.snakecase@4.1.1: {} 491 | 492 | lodash@4.17.21: {} 493 | 494 | logform@2.7.0: 495 | dependencies: 496 | '@colors/colors': 1.6.0 497 | '@types/triple-beam': 1.3.5 498 | fecha: 4.2.3 499 | ms: 2.1.3 500 | safe-stable-stringify: 2.5.0 501 | triple-beam: 1.4.1 502 | 503 | magic-bytes.js@1.10.0: {} 504 | 505 | minimist@1.2.8: {} 506 | 507 | ms@2.1.3: {} 508 | 509 | one-time@1.0.0: 510 | dependencies: 511 | fn.name: 1.1.0 512 | 513 | picomatch@4.0.2: {} 514 | 515 | prettier@3.4.1: {} 516 | 517 | readable-stream@3.6.2: 518 | dependencies: 519 | inherits: 2.0.4 520 | string_decoder: 1.3.0 521 | util-deprecate: 1.0.2 522 | 523 | safe-buffer@5.2.1: {} 524 | 525 | safe-stable-stringify@2.5.0: {} 526 | 527 | simple-swizzle@0.2.2: 528 | dependencies: 529 | is-arrayish: 0.3.2 530 | 531 | stack-trace@0.0.10: {} 532 | 533 | string_decoder@1.3.0: 534 | dependencies: 535 | safe-buffer: 5.2.1 536 | 537 | strnum@1.0.5: {} 538 | 539 | text-hex@1.0.0: {} 540 | 541 | tinyglobby@0.2.10: 542 | dependencies: 543 | fdir: 6.4.2(picomatch@4.0.2) 544 | picomatch: 4.0.2 545 | 546 | triple-beam@1.4.1: {} 547 | 548 | ts-mixer@6.0.4: {} 549 | 550 | tslib@2.8.1: {} 551 | 552 | typescript@5.7.2: {} 553 | 554 | undici-types@6.20.0: {} 555 | 556 | undici@6.19.8: {} 557 | 558 | util-deprecate@1.0.2: {} 559 | 560 | winston-transport@4.9.0: 561 | dependencies: 562 | logform: 2.7.0 563 | readable-stream: 3.6.2 564 | triple-beam: 1.4.1 565 | 566 | winston@3.17.0: 567 | dependencies: 568 | '@colors/colors': 1.6.0 569 | '@dabh/diagnostics': 2.0.3 570 | async: 3.2.6 571 | is-stream: 2.0.1 572 | logform: 2.7.0 573 | one-time: 1.0.0 574 | readable-stream: 3.6.2 575 | safe-stable-stringify: 2.5.0 576 | stack-trace: 0.0.10 577 | triple-beam: 1.4.1 578 | winston-transport: 4.9.0 579 | 580 | ws@8.18.0: {} 581 | -------------------------------------------------------------------------------- /source/Interfaces/Configuration/IApplicationConfiguration.ts: -------------------------------------------------------------------------------- 1 | export default interface IApplicationConfiguration { 2 | serverStatsUrl: string; 3 | serverMapUrl: string; 4 | updateIntervalSeconds: number; 5 | serverPassword: string; 6 | } -------------------------------------------------------------------------------- /source/Interfaces/Configuration/IConfiguration.ts: -------------------------------------------------------------------------------- 1 | import IDiscordConfiguration from "./IDiscordConfiguration"; 2 | import IApplicationConfiguration from "./IApplicationConfiguration"; 3 | import ITranslation from "./ITranslation"; 4 | 5 | export default interface IConfiguration { 6 | discord: IDiscordConfiguration; 7 | application: IApplicationConfiguration; 8 | translation: ITranslation; 9 | } -------------------------------------------------------------------------------- /source/Interfaces/Configuration/IDiscordConfiguration.ts: -------------------------------------------------------------------------------- 1 | export default interface IDiscordConfiguration { 2 | channelId: string; 3 | botToken: string; 4 | } -------------------------------------------------------------------------------- /source/Interfaces/Configuration/ITranslation.ts: -------------------------------------------------------------------------------- 1 | import ITranslationDiscordEmbed from "./ITranslationDiscordEmbed"; 2 | import ITranslationCommon from "./ITranslationCommon"; 3 | 4 | export default interface ITranslation { 5 | discordEmbed: ITranslationDiscordEmbed; 6 | common: ITranslationCommon; 7 | } -------------------------------------------------------------------------------- /source/Interfaces/Configuration/ITranslationCommon.ts: -------------------------------------------------------------------------------- 1 | export default interface ITranslationCommon { 2 | monthJanuary: string; 3 | monthFebruary: string; 4 | monthMarch: string; 5 | monthApril: string; 6 | monthMay: string; 7 | monthJune: string; 8 | monthJuly: string; 9 | monthAugust: string; 10 | monthSeptember: string; 11 | monthOctober: string; 12 | monthNovember: string; 13 | monthDecember: string; 14 | } -------------------------------------------------------------------------------- /source/Interfaces/Configuration/ITranslationDiscordEmbed.ts: -------------------------------------------------------------------------------- 1 | export default interface ITranslationDiscordEmbed { 2 | title: string; 3 | descriptionOnline: string; 4 | descriptionOffline: string; 5 | descriptionUnknown: string; 6 | titleServerName: string; 7 | titleServerMap: string; 8 | titleServerMods: string; 9 | titleServerPassword: string; 10 | titleServerTime: string; 11 | titlePlayerCount: string; 12 | noPlayersOnline: string; 13 | } -------------------------------------------------------------------------------- /source/Interfaces/Feed/IMod.ts: -------------------------------------------------------------------------------- 1 | export default interface IMod { 2 | name: string; 3 | author: string; 4 | version: string; 5 | } -------------------------------------------------------------------------------- /source/Interfaces/Feed/IPlayer.ts: -------------------------------------------------------------------------------- 1 | export default interface IPlayer { 2 | username: string; 3 | isAdministrator: boolean; 4 | sessionTime: number; 5 | isUsed: boolean; 6 | } -------------------------------------------------------------------------------- /source/Main.ts: -------------------------------------------------------------------------------- 1 | import {Client, IntentsBitField} from 'discord.js'; 2 | import Configuration from "./Services/Configuration"; 3 | import Logging from "./Services/Logging"; 4 | import DiscordService from "./Services/DiscordEmbed"; 5 | import VersionChecker from './Services/VersionChecker'; 6 | 7 | // Create a new logger instance and configuration instance 8 | const appLogger = Logging.getLogger(); 9 | const appConfig: Configuration = new Configuration(); 10 | 11 | // Log the application start and version 12 | const packageJson = require('../package.json'); 13 | appLogger.info(`Starting | App: ${packageJson.name} | Version: ${packageJson.version}`); 14 | appLogger.info(`----------------------------------------------------`); 15 | 16 | /** 17 | * Check if the configuration is valid and exit the application if it is not 18 | */ 19 | if(!appConfig.isConfigurationValid()) { 20 | appLogger.error("Configuration is not valid. Exiting application."); 21 | process.exit(1); 22 | } 23 | 24 | /** 25 | * Check the version of the bot and log if it is up to date 26 | */ 27 | const versionChecker = new VersionChecker(); 28 | versionChecker.checkVersionIsUpdated().then((isUpToDate: boolean): void => { 29 | if (!isUpToDate) { 30 | appLogger.warn(`====================================================`); 31 | appLogger.warn(`====================================================`); 32 | appLogger.warn(`The bot is not up to date. Please update it soon.`); 33 | appLogger.warn(`Use the command 'git pull && docker compose up -d --build' to update the bot.`); 34 | appLogger.warn(`====================================================`); 35 | appLogger.warn(`====================================================`); 36 | 37 | } else { 38 | appLogger.info(`The bot is up to date. No update needed.`); 39 | } 40 | }); 41 | 42 | /** 43 | * Create a new discord client instance 44 | */ 45 | const discordClient = new Client({ 46 | intents: [IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMessages] 47 | }); 48 | 49 | /** 50 | * Start the discord client and log in 51 | * After that create a new DiscordService instance to start the server stats feed 52 | */ 53 | discordClient.login(appConfig.discord.botToken).then(() => { 54 | appLogger.info(`Login successful to discord with token`); 55 | }); 56 | 57 | /** 58 | * Start the DiscordService and restart it if an error occurred 59 | */ 60 | async function startDiscordService(): Promise { 61 | try { 62 | new DiscordService(discordClient); 63 | } catch (exception) { 64 | appLogger.error(`Restarting the discord service, an error occurred`, exception); 65 | startDiscordService(); 66 | } 67 | } 68 | 69 | discordClient.on('ready', () => { 70 | appLogger.info(`Discord client ready. Logged in as ${discordClient.user?.username}!`); 71 | startDiscordService(); 72 | }); 73 | -------------------------------------------------------------------------------- /source/Schema/ServerStats.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * This file was automatically generated by json-schema-to-typescript. 4 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 5 | * and run json-schema-to-typescript to regenerate this file. 6 | */ 7 | 8 | export interface ServerStats { 9 | Server: { 10 | game: string; 11 | version: string; 12 | name: string; 13 | mapName: string; 14 | dayTime: number; 15 | mapOverviewFilename: string; 16 | mapSize: number; 17 | Slots: { 18 | capacity: number; 19 | numUsed: number; 20 | Player: { 21 | isUsed: boolean; 22 | isAdmin: boolean; 23 | uptime: number; 24 | x: number; 25 | y: number; 26 | z: number; 27 | _text: string; 28 | [k: string]: unknown; 29 | }; 30 | [k: string]: unknown; 31 | }; 32 | Vehicles: { 33 | Vehicle: { 34 | name: string; 35 | category: string; 36 | type: string; 37 | x: number; 38 | y: number; 39 | z: number; 40 | fillTypes: string; 41 | fillLevels: number; 42 | [k: string]: unknown; 43 | }; 44 | [k: string]: unknown; 45 | }; 46 | Mods: { 47 | Mod: { 48 | name: string; 49 | author: string; 50 | version: string; 51 | hash: string; 52 | _text: string; 53 | [k: string]: unknown; 54 | }; 55 | [k: string]: unknown; 56 | }; 57 | Farmlands: { 58 | Farmland: { 59 | name: string; 60 | id: number; 61 | owner: number; 62 | area: number; 63 | x: number; 64 | z: number; 65 | [k: string]: unknown; 66 | }; 67 | [k: string]: unknown; 68 | }; 69 | Fields: { 70 | Field: { 71 | id: number; 72 | x: number; 73 | z: number; 74 | isOwned: boolean; 75 | [k: string]: unknown; 76 | }; 77 | [k: string]: unknown; 78 | }; 79 | [k: string]: unknown; 80 | }; 81 | [k: string]: unknown; 82 | } 83 | -------------------------------------------------------------------------------- /source/Schema/ServerStats.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "Server": { 6 | "type": "object", 7 | "properties": { 8 | "game": { "type": "string" }, 9 | "version": { "type": "string" }, 10 | "name": { "type": "string" }, 11 | "mapName": { "type": "string" }, 12 | "dayTime": { "type": "integer" }, 13 | "mapOverviewFilename": { "type": "string" }, 14 | "mapSize": { "type": "integer" }, 15 | "Slots": { 16 | "type": "object", 17 | "properties": { 18 | "capacity": { "type": "integer" }, 19 | "numUsed": { "type": "integer" }, 20 | "Player": { 21 | "type": "object", 22 | "properties": { 23 | "isUsed": { "type": "boolean" }, 24 | "isAdmin": { "type": "boolean" }, 25 | "uptime": { "type": "integer" }, 26 | "x": { "type": "number" }, 27 | "y": { "type": "number" }, 28 | "z": { "type": "number" }, 29 | "_text": { "type": "string" } 30 | }, 31 | "required": ["isUsed", "isAdmin", "uptime", "x", "y", "z", "_text"] 32 | } 33 | }, 34 | "required": ["capacity", "numUsed", "Player"] 35 | }, 36 | "Vehicles": { 37 | "type": "object", 38 | "properties": { 39 | "Vehicle": { 40 | "type": "object", 41 | "properties": { 42 | "name": { "type": "string" }, 43 | "category": { "type": "string" }, 44 | "type": { "type": "string" }, 45 | "x": { "type": "number" }, 46 | "y": { "type": "number" }, 47 | "z": { "type": "number" }, 48 | "fillTypes": { "type": "string" }, 49 | "fillLevels": { "type": "number" } 50 | }, 51 | "required": ["name", "category", "type", "x", "y", "z", "fillTypes", "fillLevels"] 52 | } 53 | }, 54 | "required": ["Vehicle"] 55 | }, 56 | "Mods": { 57 | "type": "object", 58 | "properties": { 59 | "Mod": { 60 | "type": "object", 61 | "properties": { 62 | "name": { "type": "string" }, 63 | "author": { "type": "string" }, 64 | "version": { "type": "string" }, 65 | "hash": { "type": "string" }, 66 | "_text": { "type": "string" } 67 | }, 68 | "required": ["name", "author", "version", "hash", "_text"] 69 | } 70 | }, 71 | "required": ["Mod"] 72 | }, 73 | "Farmlands": { 74 | "type": "object", 75 | "properties": { 76 | "Farmland": { 77 | "type": "object", 78 | "properties": { 79 | "name": { "type": "string" }, 80 | "id": { "type": "integer" }, 81 | "owner": { "type": "integer" }, 82 | "area": { "type": "integer" }, 83 | "x": { "type": "number" }, 84 | "z": { "type": "number" } 85 | }, 86 | "required": ["name", "id", "owner", "area", "x", "z"] 87 | } 88 | }, 89 | "required": ["Farmland"] 90 | }, 91 | "Fields": { 92 | "type": "object", 93 | "properties": { 94 | "Field": { 95 | "type": "object", 96 | "properties": { 97 | "id": { "type": "integer" }, 98 | "x": { "type": "number" }, 99 | "z": { "type": "number" }, 100 | "isOwned": { "type": "boolean" } 101 | }, 102 | "required": ["id", "x", "z", "isOwned"] 103 | } 104 | }, 105 | "required": ["Field"] 106 | } 107 | }, 108 | "required": ["game", "version", "name", "mapName", "dayTime", "mapOverviewFilename", "mapSize", "Slots", "Vehicles", "Mods", "Farmlands", "Fields"] 109 | } 110 | }, 111 | "required": ["Server"] 112 | } 113 | -------------------------------------------------------------------------------- /source/Services/Configuration.ts: -------------------------------------------------------------------------------- 1 | import IDiscordConfiguration from "../Interfaces/Configuration/IDiscordConfiguration"; 2 | import IApplicationConfiguration from "../Interfaces/Configuration/IApplicationConfiguration"; 3 | import IConfiguration from "../Interfaces/Configuration/IConfiguration"; 4 | import ITranslation from "../Interfaces/Configuration/ITranslation"; 5 | import Logging from "./Logging"; 6 | import {Logger} from "winston"; 7 | 8 | export default class Configuration implements IConfiguration{ 9 | private readonly logger: Logger; 10 | public readonly discord: IDiscordConfiguration; 11 | public readonly application: IApplicationConfiguration; 12 | public readonly translation: ITranslation; 13 | 14 | constructor() { 15 | this.logger = Logging.getLogger(); 16 | try { 17 | let config = require('../../config.json'); 18 | this.discord = config.discord; 19 | this.application = config.application; 20 | this.translation = config.translation; 21 | } catch (exception) { 22 | this.logger.error("Error while loading configuration file, please check if the configuration file exists and is valid."); 23 | process.exit(1); 24 | } 25 | } 26 | 27 | /** 28 | * Returns true if the value is empty or undefined 29 | * @param value 30 | * @private 31 | */ 32 | private isValueEmptyOrUndefined(value: any): boolean { 33 | return value == null || value == "" || value == undefined; 34 | } 35 | 36 | /** 37 | * Returns true if the value is undefined 38 | * @param value 39 | * @private 40 | */ 41 | private isValueUndefined(value: any): boolean { 42 | return value == undefined; 43 | } 44 | 45 | /** 46 | * Validates the discord configuration and returns true if the configuration is valid 47 | * @private 48 | */ 49 | private validateDiscordConfiguration(): boolean { 50 | return !(this.isValueEmptyOrUndefined(this.discord?.botToken) || this.isValueEmptyOrUndefined(this.discord?.channelId)); 51 | } 52 | 53 | /** 54 | * Validates the application configuration and returns true if the configuration is valid 55 | * @private 56 | */ 57 | private validateApplicationConfiguration(): boolean { 58 | return !( 59 | this.isValueUndefined(this.application?.serverPassword) 60 | || this.isValueEmptyOrUndefined(this.application?.serverStatsUrl) 61 | || this.isValueEmptyOrUndefined(this.application?.serverMapUrl) 62 | || this.isValueEmptyOrUndefined(this.application?.updateIntervalSeconds) 63 | ); 64 | } 65 | 66 | /** 67 | * Validates the translation configuration and returns true if the configuration is valid 68 | * @private 69 | */ 70 | private validateTranslationConfiguration(): boolean { 71 | return !( 72 | this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.title) 73 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.descriptionOnline) 74 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.descriptionOffline) 75 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.descriptionUnknown) 76 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titleServerName) 77 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titleServerPassword) 78 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titleServerTime) 79 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titleServerMap) 80 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titleServerMods) 81 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.titlePlayerCount) 82 | || this.isValueEmptyOrUndefined(this?.translation?.discordEmbed?.noPlayersOnline) 83 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthJanuary) 84 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthFebruary) 85 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthMarch) 86 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthApril) 87 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthMay) 88 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthJune) 89 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthJuly) 90 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthAugust) 91 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthSeptember) 92 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthOctober) 93 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthNovember) 94 | || this.isValueEmptyOrUndefined(this?.translation?.common?.monthDecember) 95 | ); 96 | } 97 | 98 | /** 99 | * Validates the configuration file and returns true if the configuration is valid 100 | * @returns boolean True if the configuration is valid 101 | */ 102 | public isConfigurationValid(): boolean { 103 | if(!this.validateDiscordConfiguration()) { 104 | this.logger.error("Discord configuration is not valid. Please check your configuration file."); 105 | return false; 106 | } else { 107 | this.logger.info("Discord configuration is valid."); 108 | } 109 | 110 | if(!this.validateApplicationConfiguration()) { 111 | this.logger.error("Application configuration is not valid. Please check your configuration file."); 112 | return false; 113 | } else { 114 | this.logger.info("Application configuration is valid."); 115 | } 116 | 117 | if(!this.validateTranslationConfiguration()) { 118 | this.logger.error("Translation configuration is not valid. Please check your configuration file."); 119 | return false; 120 | } else { 121 | this.logger.info("Translation configuration is valid."); 122 | } 123 | return true; 124 | } 125 | 126 | /** 127 | * Returns the configuration object 128 | */ 129 | public static getConfiguration(): IConfiguration { 130 | return new Configuration(); 131 | } 132 | } -------------------------------------------------------------------------------- /source/Services/DiscordEmbed.ts: -------------------------------------------------------------------------------- 1 | import {Client, EmbedBuilder, Snowflake, TextChannel} from "discord.js"; 2 | import Configuration from "./Configuration"; 3 | import ServerStatusFeed from "./ServerStatusFeed"; 4 | import {Logger} from "winston"; 5 | import Logging from "./Logging"; 6 | 7 | export default class DiscordEmbed { 8 | private appLogger: Logger; 9 | private discordAppClient: Client; 10 | private appConfiguration: Configuration; 11 | private serverStatsFeed: ServerStatusFeed; 12 | private firstMessageId: Snowflake | null = null; 13 | 14 | public constructor(discordAppClient: Client) { 15 | this.appLogger = Logging.getLogger(); 16 | this.discordAppClient = discordAppClient; 17 | this.appConfiguration = new Configuration(); 18 | this.serverStatsFeed = new ServerStatusFeed(); 19 | 20 | (async () => { 21 | // Delete all messages in the channel 22 | await this.deleteAllMessages(); 23 | // Start the update loop, which updates the discord embed every x seconds itself 24 | await this.updateDiscordEmbed(); 25 | })(); 26 | } 27 | 28 | /** 29 | * Update the discord embed with the server status, player list and server time 30 | * This method is called every x seconds to update the discord embed. 31 | * @private 32 | */ 33 | private async updateDiscordEmbed(): Promise { 34 | try { 35 | await this.serverStatsFeed.updateServerFeed(); 36 | if(this.serverStatsFeed.isFetching()) { 37 | this.appLogger.info('Server status feed is still fetching, try again...'); 38 | setTimeout(() => { 39 | this.updateDiscordEmbed(); 40 | }, 1000); 41 | return; 42 | } 43 | this.discordAppClient.channels.fetch(this.appConfiguration.discord.channelId as Snowflake).then(async channel => { 44 | /** 45 | * Send the initial message to the channel (if the first message id is not set) or 46 | * the message is meanwhile deleted 47 | * @param embedMessage 48 | */ 49 | let sendInitialMessage = (embedMessage: EmbedBuilder) => { 50 | // noinspection JSAnnotator 51 | (channel as TextChannel).send({embeds: [embedMessage]}).then(message => { 52 | this.firstMessageId = message.id; 53 | }); 54 | }; 55 | 56 | this.generateEmbedFromStatusFeed(this.serverStatsFeed).then(embedMessage => { 57 | if (this.firstMessageId !== null) { 58 | (channel as TextChannel).messages.fetch(this.firstMessageId).then(message => { 59 | this.appLogger.info(`Message found, editing message with new embed`); 60 | message.edit({embeds: [embedMessage]}); 61 | }).catch(() => { 62 | this.appLogger.warn('Message not found, sending new message'); 63 | sendInitialMessage(embedMessage); 64 | }); 65 | } else { 66 | this.appLogger.info(`No message found, sending new message`); 67 | sendInitialMessage(embedMessage); 68 | } 69 | }); 70 | }); 71 | } catch (exception) { 72 | this.appLogger.error(exception); 73 | } 74 | 75 | setTimeout(() => { 76 | this.updateDiscordEmbed(); 77 | }, this.appConfiguration.application.updateIntervalSeconds * 1000); 78 | } 79 | 80 | /** 81 | * Delete all messages in a text channel to clear the channel 82 | * @private 83 | */ 84 | private async deleteAllMessages(): Promise { 85 | let textChannel = this.discordAppClient.channels.cache.get(this.appConfiguration.discord.channelId as Snowflake) as TextChannel; 86 | this.appLogger.info(`Deleting all messages in discord text channel ${textChannel.id}`); 87 | textChannel.messages.fetch().then(messages => { 88 | messages.forEach(message => { 89 | message.delete(); 90 | }); 91 | }); 92 | return true; 93 | } 94 | 95 | /** 96 | * Truncates a string at a given length 97 | * @param text The input text to truncate 98 | * @param maxLength The allowed characters until truncation 99 | * @returns The truncated string 100 | */ 101 | private async truncateText(text: string, maxLength = 1024): Promise { 102 | return text.length > maxLength ? text.slice(0, maxLength - 3) + '...' : text; 103 | } 104 | 105 | /** 106 | * Send server stats embed in a channel 107 | * @param serverStats 108 | */ 109 | private async generateEmbedFromStatusFeed(serverStats: ServerStatusFeed): Promise { 110 | let embed = new EmbedBuilder(); 111 | let config = this.appConfiguration; 112 | 113 | embed.setTitle(config.translation.discordEmbed.title); 114 | if (!serverStats.isOnline()) { 115 | embed.setColor(0xCA0000); 116 | embed.setDescription(config.translation.discordEmbed.descriptionOffline); 117 | } else if (serverStats.isFetching()) { 118 | embed.setDescription(config.translation.discordEmbed.descriptionUnknown); 119 | } else { 120 | embed.setColor(0x00CA00); 121 | embed.setDescription(config.translation.discordEmbed.descriptionOnline); 122 | embed.setTimestamp(new Date()); 123 | embed.setThumbnail(config.application.serverMapUrl); 124 | 125 | let playerListString: string; 126 | let playerListTitleString = `${config.translation.discordEmbed.titlePlayerCount} (${serverStats.getPlayerCount()??0}/${serverStats.getMaxPlayerCount()??0}):`; 127 | 128 | if(serverStats.getPlayerList().length === 0) { 129 | playerListString = config.translation.discordEmbed.noPlayersOnline; 130 | } else { 131 | playerListString = serverStats.getPlayerList().map(p => p.username).join(', '); 132 | } 133 | 134 | let serverPassword = config.application.serverPassword; 135 | if(config.application.serverPassword == "") { 136 | serverPassword = "-/-"; 137 | } 138 | 139 | let serverMods = serverStats.getServerMods(); 140 | let serverModsText = "-/-"; 141 | if(serverMods.length > 0) { 142 | serverModsText = await this.truncateText(serverMods.map(mod => `${mod.name}`).join(', ')); 143 | } 144 | 145 | // @ts-ignore 146 | embed.addFields( 147 | {name: config.translation.discordEmbed.titleServerName, value: serverStats.getServerName()}, 148 | {name: config.translation.discordEmbed.titleServerPassword, value: serverPassword}, 149 | {name: config.translation.discordEmbed.titleServerTime, value: serverStats.getServerTime()}, 150 | {name: config.translation.discordEmbed.titleServerMap, value: serverStats.getServerMap()}, 151 | {name: config.translation.discordEmbed.titleServerMods, value: serverModsText}, 152 | { 153 | name: playerListTitleString, 154 | value: playerListString 155 | }, 156 | ); 157 | } 158 | return embed; 159 | } 160 | } -------------------------------------------------------------------------------- /source/Services/Logging.ts: -------------------------------------------------------------------------------- 1 | import winston, {Logger} from "winston"; 2 | 3 | export default class Logging { 4 | public static getLogger(): Logger { 5 | return winston.createLogger({ 6 | level: 'info', 7 | format: winston.format.combine( 8 | winston.format.timestamp({ 9 | format: 'YYYY-MM-DD HH:mm:ss' 10 | }), 11 | winston.format.colorize(), 12 | winston.format.simple(), 13 | winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`) 14 | ), 15 | transports: [ 16 | new winston.transports.Console(), 17 | ] 18 | }); 19 | } 20 | } -------------------------------------------------------------------------------- /source/Services/ServerStatusFeed.ts: -------------------------------------------------------------------------------- 1 | import {ServerStats} from "../Schema/ServerStats"; 2 | import Configuration from "./Configuration"; 3 | import {XMLParser} from "fast-xml-parser"; 4 | import Logging from "./Logging"; 5 | import IPlayer from "../Interfaces/Feed/IPlayer"; 6 | import IConfiguration from "../Interfaces/Configuration/IConfiguration"; 7 | import IMod from "../Interfaces/Feed/IMod"; 8 | 9 | export const CONNECTION_REFUSED = 'ECONNREFUSED'; 10 | export const NOT_FOUND = 'ENOTFOUND'; 11 | 12 | export default class ServerStatusFeed { 13 | private _serverStats: ServerStats | null = null; 14 | private _isOnline: boolean = false; 15 | private _isFetching: boolean = false; 16 | 17 | constructor() { 18 | } 19 | 20 | /** 21 | * Returns the fetching status of the server stats feed 22 | * @returns {boolean} The fetching status of the server stats feed 23 | */ 24 | public isFetching(): boolean { 25 | return this._isFetching; 26 | } 27 | 28 | /** 29 | * Get the server stats object 30 | * @returns {ServerStats | null} The server stats object or null if the server is offline or fetching 31 | * @private 32 | */ 33 | private getServerStats(): ServerStats | null { 34 | if(this._isOnline && !this._isFetching && this._serverStats) { 35 | return this._serverStats; 36 | } 37 | return null; 38 | } 39 | 40 | /** 41 | * Update the server feed from the server status feed url 42 | * @returns {Promise} The server stats object or null if the fetch failed 43 | */ 44 | public async updateServerFeed(): Promise { 45 | this._isFetching = true; 46 | Logging.getLogger().info(`Fetching server status from feed url`); 47 | await fetch(Configuration.getConfiguration().application.serverStatsUrl) 48 | .then( 49 | r => r.text() 50 | ).then( 51 | (response) => { 52 | // Set online status to true 53 | this._isOnline = true; 54 | 55 | // Parse the XML response 56 | const parsedFeed = new XMLParser({ignoreAttributes: false, attributeNamePrefix: ''}).parse(response) as ServerStats; 57 | Logging.getLogger().info(`Server status feed successful received`); 58 | this._serverStats = parsedFeed; 59 | } 60 | ).catch( 61 | (reason) => { 62 | // Set online status to false 63 | this._isOnline = false; 64 | 65 | // Handle different error codes 66 | switch (reason.cause.code) { 67 | case CONNECTION_REFUSED: 68 | Logging.getLogger().error(`Connection refused to server status feed`); 69 | break; 70 | case NOT_FOUND: 71 | Logging.getLogger().error(`Server status feed not found`); 72 | break; 73 | default: 74 | Logging.getLogger().error(`Error fetching server status feed`); 75 | break; 76 | } 77 | return null; 78 | }) 79 | .finally(() => { 80 | // Set fetching status to false after fetching is done or failed 81 | this._isFetching = false; 82 | }); 83 | return this._serverStats; 84 | } 85 | 86 | /** 87 | * Returns the online status of the server 88 | * @returns {boolean} The online status of the server 89 | */ 90 | public isOnline(): boolean { 91 | return this._isOnline; 92 | } 93 | 94 | /** 95 | * Returns the server name 96 | * @returns {string} The server name 97 | */ 98 | public getServerName(): string { 99 | return this.getServerStats()?.Server.name; 100 | } 101 | 102 | /** 103 | * Returns the server map name 104 | * @returns {string} The server map name 105 | */ 106 | public getServerMap(): string { 107 | return this.getServerStats()?.Server.mapName; 108 | } 109 | 110 | /** 111 | * Returns the server time in decimal format 112 | * @returns {number} The server time in decimal format 113 | */ 114 | public getServerTimeDecimal(): number { 115 | let dayTime = this.getServerStats()?.Server.dayTime; 116 | if (dayTime === undefined) { 117 | return 0; 118 | } 119 | return dayTime / (60 * 60 * 1000) + 0.0001; 120 | } 121 | 122 | /** 123 | * Get the server mods from the server stats feed 124 | * @returns {IMod[]} The server mods as an array of IMod objects 125 | */ 126 | public getServerMods(): IMod[] { 127 | let modList = this.getServerStats()?.Server?.Mods?.Mod; 128 | if(modList === undefined || !Array.isArray(modList) || modList == null) { 129 | return []; 130 | } 131 | return modList.map((mod: any) => { 132 | return { 133 | name: mod['#text'], 134 | author: mod.author, 135 | version: mod.version 136 | } as IMod; 137 | }); 138 | } 139 | 140 | /** 141 | * Returns the server time in the format HH:MM 142 | * @returns {string} The server time in the format HH:MM 143 | */ 144 | public getServerTime(): string { 145 | let decimalTime = this.getServerTimeDecimal(); 146 | if(decimalTime === 0) { 147 | return "00:00"; 148 | } 149 | let hours = Math.floor(decimalTime); 150 | let minutes = Math.floor((decimalTime - hours) * 60); 151 | let hoursString = hours.toString(); 152 | let minutesString = minutes.toString(); 153 | if(hoursString.length === 1) { 154 | hoursString = `0${hoursString}`; 155 | } 156 | if(minutesString.length === 1) { 157 | minutesString = `0${minutesString}`; 158 | } 159 | return `${hoursString}:${minutesString}`; 160 | } 161 | 162 | /** 163 | * Returns the server player count 164 | * @returns {number | null | undefined} The server player count 165 | */ 166 | public getPlayerCount(): number | null | undefined { 167 | return this.getServerStats()?.Server?.Slots?.numUsed; 168 | } 169 | 170 | /** 171 | * Returns the server player count 172 | * @returns {number | null | undefined} The server player count 173 | */ 174 | public getMaxPlayerCount(): number | null | undefined { 175 | return this.getServerStats()?.Server?.Slots?.capacity; 176 | } 177 | 178 | /** 179 | * Returns the player list from the server stats feed 180 | * @returns {IPlayer[]} The online player list as an array of IPlayer objects 181 | */ 182 | public getPlayerList(): IPlayer[] { 183 | let mappedPlayers: IPlayer[]; 184 | let returnPlayers: IPlayer[] = []; 185 | let playerList = this.getServerStats()?.Server.Slots.Player; 186 | if (Array.isArray(playerList)) { 187 | mappedPlayers = playerList.map((player) => { 188 | return { 189 | username: player['#text'], 190 | isAdministrator: player.isAdmin === 'true', 191 | sessionTime: parseInt(player.uptime), 192 | isUsed: player.isUsed === 'true', 193 | } as IPlayer; 194 | }); 195 | } else { 196 | mappedPlayers = []; 197 | } 198 | 199 | // Filter out player slots that are not used 200 | mappedPlayers.forEach((player) => { 201 | if(player.isUsed) { 202 | returnPlayers.push(player); 203 | } 204 | }); 205 | 206 | return returnPlayers; 207 | } 208 | } -------------------------------------------------------------------------------- /source/Services/VersionChecker.ts: -------------------------------------------------------------------------------- 1 | export default class VersionChecker { 2 | private readonly localPackageVersion: string; 3 | private readonly versionUrl: string = "https://raw.githubusercontent.com/cloudmaker97/FS25-Discord-Bot/refs/heads/main/package.json"; 4 | 5 | constructor() { 6 | this.localPackageVersion = require('../../package.json').version; 7 | } 8 | 9 | /** 10 | * Check if the version of the bot is up to date 11 | */ 12 | public async checkVersionIsUpdated(): Promise { 13 | const latestVersion = await this.getLatestReleasedVersion(); 14 | return this.isNewerVersion(latestVersion, this.localPackageVersion); 15 | } 16 | 17 | /** 18 | * Get the latest released version of the bot from the github repository 19 | */ 20 | public async getLatestReleasedVersion(): Promise { 21 | const response = await fetch(this.versionUrl); 22 | const latestPackage = await response.text(); 23 | const latestVersion = JSON.parse(latestPackage)?.version; 24 | return latestVersion; 25 | } 26 | 27 | /** 28 | * Check if the latest version is newer than the current version 29 | */ 30 | public isNewerVersion(latestVersion: string, currentVersion: string) { 31 | const v1Parts: number[] = latestVersion.split('.').map(Number); 32 | const v2Parts: number[] = currentVersion.split('.').map(Number); 33 | for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { 34 | const part1 = v1Parts[i] || 0; 35 | const part2 = v2Parts[i] || 0; 36 | if (part1 > part2) return false; 37 | if (part1 < part2) return true; 38 | } 39 | return true; 40 | } 41 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "allowJs": true, 5 | "outDir": "build", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "skipLibCheck": true, 11 | "module": "commonjs", 12 | "rootDir": "source", 13 | "target": "es2016", 14 | "lib": ["es6"], 15 | } 16 | } --------------------------------------------------------------------------------