├── .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 | 
14 |
15 |
16 |
17 |
18 | Discord embed in german
19 |
20 | 
21 |
22 |
23 |
24 |
25 | Terminal output (NodeJS)
26 |
27 | 
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 |
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 | }
--------------------------------------------------------------------------------