138 Commits

Author SHA1 Message Date
4ccd2e207a Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.16.0 2024-10-23 14:01:18 +00:00
Lee
0bc614ce39 Merge pull request 'Update dependency com.influxdb:influxdb-client-java to v7.2.0' (#44) from renovate/com.influxdb-influxdb-client-java-7.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m36s
Reviewed-on: #44
2024-08-13 17:28:18 +00:00
Lee
499c54c8cf Merge pull request 'Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.14.0' (#46) from renovate/io.sentry-sentry-spring-boot-starter-jakarta-7.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #46
2024-08-13 17:28:09 +00:00
Lee
41f7ca07b0 Merge pull request 'Update dependency com.influxdb:influxdb-spring to v7.2.0' (#45) from renovate/com.influxdb-influxdb-spring-7.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #45
2024-08-13 17:26:51 +00:00
5ec61940ac Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.14.0 2024-08-13 09:01:06 +00:00
6665e8a655 Update dependency com.influxdb:influxdb-spring to v7.2.0 2024-08-12 08:01:06 +00:00
07562eb94d Update dependency com.influxdb:influxdb-client-java to v7.2.0 2024-08-12 08:01:04 +00:00
Lee
a78adf67c7 Merge pull request 'Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.13.0' (#43) from renovate/io.sentry-sentry-spring-boot-starter-jakarta-7.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m13s
Reviewed-on: #43
2024-07-31 15:59:25 +00:00
fc1f51da75 Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.13.0 2024-07-31 10:00:30 +00:00
c796875d8c increase the timeout for mojang servers
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 3m12s
2024-07-30 22:06:01 +01:00
82fb2a3d23 Merge remote-tracking branch 'origin/master'
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 3m15s
2024-07-30 22:00:30 +01:00
2e326bb7be oopsie 2024-07-30 22:00:07 +01:00
Lee
c5bf941c54 Merge pull request 'Update dependency de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring3x to v4.16.1' (#22) from renovate/de.flapdoodle.embed-de.flapdoodle.embed.mongo.spring3x-4.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m0s
Reviewed-on: #22
2024-07-30 20:55:26 +00:00
Lee
0eb965a26d Merge pull request 'Update dependency org.springframework.boot:spring-boot-starter-parent to v3.3.2' (#42) from renovate/org.springframework.boot-spring-boot-starter-parent-3.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m58s
Reviewed-on: #42
2024-07-30 20:52:18 +00:00
Lee
5481c9302c Merge pull request 'Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.12.1' (#21) from renovate/io.sentry-sentry-spring-boot-starter-jakarta-7.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #21
2024-07-30 20:52:12 +00:00
b7834ab389 change how the mojang server status' are fetched
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m9s
2024-07-30 21:04:37 +01:00
bb651bd88b Merge remote-tracking branch 'origin/master'
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 3m47s
2024-07-30 20:52:09 +01:00
2b017f9ef7 fix mojang status endpoint 2024-07-30 20:49:36 +01:00
d83391de33 Update dependency org.springframework.boot:spring-boot-starter-parent to v3.3.2 2024-07-26 12:03:27 +00:00
76bef70473 Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.12.1 2024-07-25 13:00:32 +00:00
4aa5b0a90d Update dependency de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring3x to v4.16.1 2024-07-18 16:00:22 +00:00
Lee
6a44618ae9 Merge pull request 'Update dependency org.codehaus.plexus:plexus-archiver to v4.10.0' (#20) from renovate/org.codehaus.plexus-plexus-archiver-4.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m12s
Reviewed-on: #20
2024-07-06 18:48:57 +00:00
146d053af8 Update dependency org.codehaus.plexus:plexus-archiver to v4.10.0 2024-07-06 08:00:23 +00:00
Lee
796146c039 Merge pull request 'Update dependency de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring3x to v4.15.0' (#10) from renovate/de.flapdoodle.embed-de.flapdoodle.embed.mongo.spring3x-4.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m44s
Reviewed-on: #10
2024-07-06 07:34:23 +00:00
Lee
00c83d9ae3 Merge pull request 'Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.11.0' (#11) from renovate/io.sentry-sentry-spring-boot-starter-jakarta-7.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #11
2024-07-06 07:34:16 +00:00
Lee
3bbab24e45 Merge pull request 'Update dependency com.influxdb:influxdb-client-java to v7.1.0' (#12) from renovate/com.influxdb-influxdb-client-java-7.x into master
Some checks are pending
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Waiting to run
Reviewed-on: #12
2024-07-06 07:34:08 +00:00
Lee
5034a11e63 Merge pull request 'Update dependency com.influxdb:influxdb-spring to v7.1.0' (#13) from renovate/com.influxdb-influxdb-spring-7.x into master
Some checks are pending
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Waiting to run
Reviewed-on: #13
2024-07-06 07:34:01 +00:00
Lee
3d11c65678 Merge pull request 'Update s4u/setup-maven-action action to v1.14.0' (#14) from renovate/s4u-setup-maven-action-1.x into master
Some checks are pending
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Waiting to run
Reviewed-on: #14
2024-07-06 07:33:54 +00:00
Lee
fd3da02159 Merge pull request 'Update dependency com.google.code.gson:gson to v2.11.0' (#15) from renovate/com.google.code.gson-gson-2.x into master
Some checks are pending
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Waiting to run
Reviewed-on: #15
2024-07-06 07:33:46 +00:00
Lee
0bdaefe4a2 Merge pull request 'Update dependency org.springframework.boot:spring-boot-starter-parent to v3.3.1' (#16) from renovate/spring-boot into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #16
2024-07-06 07:33:38 +00:00
Lee
ba167b4e56 Merge pull request 'Update maven Docker tag to v3.9.8' (#17) from renovate/maven-3.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #17
2024-07-06 07:33:18 +00:00
Lee
493e7ce4c0 Merge pull request 'Update dependency org.projectlombok:lombok to v1.18.34' (#18) from renovate/org.projectlombok-lombok-1.x into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
Reviewed-on: #18
2024-07-06 07:33:08 +00:00
Lee
7f501431b1 Merge pull request 'Update dependency org.springdoc:springdoc-openapi-starter-webmvc-ui to v2.6.0' (#19) from renovate/org.springdoc-springdoc-openapi-starter-webmvc-ui-2.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 3m35s
Reviewed-on: #19
2024-07-06 07:28:56 +00:00
fa92791b56 Update dependency de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring3x to v4.15.0 2024-07-04 18:00:28 +00:00
6750773640 Update dependency io.sentry:sentry-spring-boot-starter-jakarta to v7.11.0 2024-07-01 17:00:30 +00:00
cecc6bc94f Update dependency org.springdoc:springdoc-openapi-starter-webmvc-ui to v2.6.0 2024-06-30 19:00:29 +00:00
20db1c1aff Update s4u/setup-maven-action action to v1.14.0 2024-06-29 08:00:30 +00:00
e62e7f0fc2 Update dependency org.projectlombok:lombok to v1.18.34 2024-06-28 01:00:20 +00:00
c9ed681204 Update maven Docker tag to v3.9.8 2024-06-27 18:00:21 +00:00
c6642e85fe Update dependency org.springframework.boot:spring-boot-starter-parent to v3.3.1 2024-06-20 12:00:34 +00:00
9dfbd1af47 Update dependency com.google.code.gson:gson to v2.11.0 2024-05-19 20:01:05 +00:00
c8629e4f27 Update dependency com.influxdb:influxdb-spring to v7.1.0 2024-05-17 10:01:10 +00:00
a6209c45ff Update dependency com.influxdb:influxdb-client-java to v7.1.0 2024-05-17 10:01:07 +00:00
ee5b1f12d8 Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m16s
2024-04-22 22:07:52 +01:00
ff79372ead don't throw an error on geo lookup error 2024-04-22 22:06:11 +01:00
Lee
f0e1490463 Merge pull request 'Update dependency org.codehaus.plexus:plexus-archiver to v4.9.2' (#9) from renovate/org.codehaus.plexus-plexus-archiver-4.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m15s
Reviewed-on: #9
2024-04-21 23:10:18 +00:00
9196ec3578 Update dependency org.codehaus.plexus:plexus-archiver to v4.9.2 2024-04-21 23:00:39 +00:00
a8558578f2 use ip not hostname for getting geolocation
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m51s
2024-04-21 23:42:33 +01:00
67efda71d2 don't return maxmind data if it failed to load
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m22s
2024-04-21 23:33:40 +01:00
2ad5556041 add check if licence is in the config for maxmind
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 30s
2024-04-21 23:30:58 +01:00
3faf2d3319 add location to the server response
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 57s
2024-04-21 23:27:44 +01:00
bf992713dc switch to springboot sentry
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m50s
2024-04-21 18:50:57 +01:00
23e240fce1 fix sentry depend
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m1s
2024-04-21 18:44:44 +01:00
cca45057f0 impl sentry
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-21 18:41:53 +01:00
beda7fa230 fix websocket metrics
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m7s
2024-04-21 18:37:21 +01:00
d394c21f69 fix docs link 2024-04-21 13:50:23 +01:00
7df4fda744 remove mojang pinger debug
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m1s
2024-04-21 03:08:15 +01:00
f85ed49545 fix italics
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m57s
2024-04-20 21:17:25 +01:00
69833bf560 add italics to the server preview renderer
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m52s
2024-04-20 21:14:00 +01:00
6693fc6793 work pls
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m4s
2024-04-20 21:01:18 +01:00
a6ea3ab143 work pls
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m33s
2024-04-20 20:49:23 +01:00
f96e5d5426 work pls
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 1m17s
2024-04-20 20:47:29 +01:00
e360ad4446 work pls
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 58s
2024-04-20 20:43:45 +01:00
c913816447 work pls
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-20 20:41:24 +01:00
5dccce9fc5 maybe font man will show up at my door
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 39s
2024-04-20 20:39:39 +01:00
3cd5e32118 maybe font man will show up at my door
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 38s
2024-04-20 20:38:05 +01:00
5ca707bef1 update maven - might fix, who knows
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m2s
2024-04-20 20:34:00 +01:00
6fc02cd906 pls fix it
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 42s
2024-04-20 20:31:02 +01:00
f4a9d7c31c pls fix it
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m56s
2024-04-20 20:26:31 +01:00
7127794152 install fontconfig - docker
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 39s
2024-04-20 20:24:46 +01:00
f664406299 install fontconfig - docker
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 37s
2024-04-20 20:23:04 +01:00
c5b5b3b105 pls work
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m6s
2024-04-20 20:10:41 +01:00
b5fa470801 fix preview tests
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m5s
2024-04-20 20:00:47 +01:00
0854c9e76a change cachecontrol for server previews to be lower 2024-04-20 20:00:22 +01:00
17803410bd fix preview route path
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 39s
2024-04-20 19:58:50 +01:00
92fe2b28e3 move package
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m12s
2024-04-20 19:54:38 +01:00
eae027af84 add server preview tests
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m9s
2024-04-20 19:45:54 +01:00
d2ae4b4cc5 add server preview renderer
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 38s
2024-04-20 19:37:58 +01:00
ff58b1756a is this even used anymore?
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m10s
2024-04-20 14:31:49 +01:00
543aff2a04 send metrics to client when they connect
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m56s
2024-04-20 14:23:43 +01:00
b666e5a8b7 Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m17s
2024-04-20 14:18:55 +01:00
ab3ed0511f cleanup metrics websocket 2024-04-20 14:17:17 +01:00
Lee
bf44c9bc0d Update README.md 2024-04-20 01:13:22 +00:00
cf8e27f039 add topics for logging
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m53s
2024-04-19 23:24:51 +01:00
e5935c6696 fix metric saving for running in tests
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m10s
2024-04-19 22:42:32 +01:00
5871c64582 cleanup application.yml 2024-04-19 22:26:17 +01:00
1cfcce4806 etags attempt #2
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m52s
2024-04-19 21:00:08 +01:00
46d4a53b11 fix cache control, oops
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m47s
2024-04-19 20:51:33 +01:00
d0cfd03ad9 impl etags
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m56s
2024-04-19 20:46:30 +01:00
4dc263961d add cache control to endpoints
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m0s
2024-04-19 20:33:12 +01:00
8a8c6b542a add debug
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m1s
2024-04-19 18:10:25 +01:00
5b8017e403 add debug
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m44s
2024-04-19 18:06:04 +01:00
ad83e270b6 maybe fix
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m51s
2024-04-19 18:00:22 +01:00
aa69970ec7 woop! fix timeouts and use hostnames for mojang api and session server
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m58s
2024-04-19 17:52:16 +01:00
7ecaf8c580 lower timeout
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m51s
2024-04-19 17:31:20 +01:00
8a985b52b8 update mojang endpoint to include the name of the service
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m18s
2024-04-19 17:21:10 +01:00
03c679d25c pls work part 2
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m54s
2024-04-18 23:49:09 +01:00
6096764905 Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m46s
2024-04-18 23:39:38 +01:00
984dd8bfdc pls fix? 2024-04-18 23:38:42 +01:00
Lee
806620afff Merge pull request 'Configure Renovate' (#7) from renovate/configure into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m45s
Reviewed-on: #7
2024-04-18 20:16:52 +00:00
0b6c752441 Add renovate.json 2024-04-18 20:16:25 +00:00
Lee
f2d6200bd4 Delete renovate.json
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s
2024-04-18 20:13:23 +00:00
Lee
bee6f2d52d Merge pull request 'Configure Renovate' (#6) from renovate/configure into master
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 43s
Reviewed-on: #6
2024-04-18 20:02:23 +00:00
4cd345cb8f Add renovate.json 2024-04-18 20:01:54 +00:00
Lee
2e90f8b041 Delete renovate.json
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-18 20:01:30 +00:00
Lee
5f764d16dc Merge pull request 'Update dependency org.springframework.boot:spring-boot-starter-parent to v3.2.5' (#5) from renovate/org.springframework.boot-spring-boot-starter-parent-3.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m56s
Reviewed-on: #5
2024-04-18 19:49:23 +00:00
4fa94c7264 Update dependency org.springframework.boot:spring-boot-starter-parent to v3.2.5 2024-04-18 19:00:35 +00:00
7a5b42e9d7 maybe fix metrics randomly not writing to influx
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m51s
2024-04-18 17:38:14 +01:00
547fa075f3 fix hostname stored
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m50s
2024-04-18 17:10:23 +01:00
8dcde443ee store hostname instead of query for unique server lookup total
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m38s
2024-04-18 17:08:07 +01:00
ba699f5305 store uuid instead of query for unique player lookup total 2024-04-18 17:07:55 +01:00
b3e560d1e2 fix the fixy
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m59s
2024-04-18 16:47:48 +01:00
cb9181010a maybe
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 1m4s
2024-04-18 16:38:08 +01:00
046df7fd1f maybe
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 2m1s
2024-04-18 16:28:09 +01:00
3ac4bfe2ee maybe
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m54s
2024-04-18 15:47:05 +01:00
f037f3f9e7 pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 18s
2024-04-18 14:26:25 +01:00
04b99715c1 pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 20s
2024-04-18 14:25:12 +01:00
4e5258d74c pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 25s
2024-04-18 14:22:10 +01:00
6e336bb879 wow
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 21s
2024-04-18 14:20:55 +01:00
daf3770b73 pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 28s
2024-04-18 14:07:18 +01:00
02eb9e2a2e pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 23s
2024-04-18 14:05:42 +01:00
aa87d2f374 pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 23s
2024-04-18 14:03:52 +01:00
1eb540380e pls work
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 50s
2024-04-18 13:58:28 +01:00
b0bdf6e800 add version to embedded mongo for tests
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 1m0s
2024-04-18 13:56:31 +01:00
597c3850e3 fix tests
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 23s
2024-04-18 13:55:02 +01:00
ce3067ee0e switch to mongo for metric storage
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 54s
2024-04-18 13:34:43 +01:00
1c685ca414 switch to mongo for metric storage
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 1m1s
2024-04-18 13:31:22 +01:00
c91a4afdf9 oopise, actually register unique server requests metric
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m23s
2024-04-18 13:19:49 +01:00
4fd66dffd3 switch to unique player and server lookups not the total
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 26s
2024-04-18 13:17:41 +01:00
b93c7f68fb Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m35s
2024-04-18 12:41:38 +01:00
d9ebbfe99e update metrics websocket json response 2024-04-18 12:37:46 +01:00
Lee
c58ceac5e4 Merge pull request 'Configure Renovate' (#4) from renovate/configure into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m45s
Reviewed-on: #4
2024-04-18 11:03:07 +00:00
e2d97ae417 Add renovate.json 2024-04-18 11:02:07 +00:00
Lee
d764242aed Delete renovate.json
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Has been cancelled
2024-04-18 11:01:50 +00:00
Lee
ebe5e763fa Merge pull request 'Update dependency org.springdoc:springdoc-openapi-starter-webmvc-ui to v2.5.0' (#3) from renovate/org.springdoc-springdoc-openapi-starter-webmvc-ui-2.x into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 2m16s
Reviewed-on: #3
2024-04-18 08:36:28 +00:00
7e436c917a Update dependency org.springdoc:springdoc-openapi-starter-webmvc-ui to v2.5.0 2024-04-18 08:36:01 +00:00
Lee
3069e9c9e8 Merge pull request 'Configure Renovate' (#2) from renovate/configure into master
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m48s
Reviewed-on: #2
2024-04-18 08:34:14 +00:00
036d8439ba Add renovate.json 2024-04-18 08:33:01 +00:00
76 changed files with 1339 additions and 455 deletions

View File

@ -27,7 +27,7 @@ jobs:
# Setup Java and Maven
- name: Set up JDK and Maven
uses: s4u/setup-maven-action@v1.12.0
uses: s4u/setup-maven-action@v1.14.0
with:
java-version: ${{ matrix.java-version }}
distribution: "zulu"

3
.gitignore vendored
View File

@ -29,3 +29,6 @@ git.properties
pom.xml.versionsBackup
application.yml
target/
### MaxMind GeoIP2
data/

View File

@ -1,4 +1,8 @@
FROM maven:3.8.5-openjdk-17-slim
FROM maven:3.9.8-eclipse-temurin-17-alpine
RUN apk --update --upgrade --no-cache add fontconfig ttf-freefont font-noto terminus-font \
&& fc-cache -f \
&& fc-list | sort
# Set the working directory
WORKDIR /home/container
@ -17,4 +21,4 @@ ENV PORT=80
ENV ENVIRONMENT=production
# Run the jar file
CMD ["java", "-jar", "target/Minecraft-Utilities.jar"]
CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true

View File

@ -1,7 +1,3 @@
# Minecraft Utilities API
# Minecraft Utilities - Backend
Wrapper for the Minecraft APIs to make them easier to use.
## Usage
View the [documentation](https://api.mcutils.xyz/swagger-ui/index.html) or visit the [website](https://api.mcutils.xyz) for more information.
See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.

45
pom.xml
View File

@ -17,7 +17,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
@ -83,17 +83,23 @@
<artifactId>jedis</artifactId>
</dependency>
<!-- MongoDB for data storage -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- Libraries -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -123,17 +129,24 @@
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<!-- Sentry -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.16.0</version>
</dependency>
<!-- InfluxDB Metrics -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-spring</artifactId>
<version>7.0.0</version>
<version>7.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>7.0.0</version>
<version>7.2.0</version>
</dependency>
<!-- DNS Lookup -->
@ -148,10 +161,24 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
<version>2.6.0</version>
<scope>compile</scope>
</dependency>
<!-- GeoIP - IP Lookups -->
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>4.2.0</version>
</dependency>
<!-- Archive Utilities -->
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
<version>4.10.0</version>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -164,6 +191,12 @@
<version>1.4.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
<version>4.16.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View File

@ -12,17 +12,14 @@ import java.net.http.HttpClient;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Log4j2
@Log4j2(topic = "Main")
@SpringBootApplication
public class Main {
public static final Gson GSON = new GsonBuilder()
.setDateFormat("MM-dd-yyyy HH:mm:ss")
.create();
public static final Gson GSON = new GsonBuilder()
.setDateFormat("MM-dd-yyyy HH:mm:ss")
.create();
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
public static final ExecutorService EXECUTOR_POOL = Executors.newFixedThreadPool(8);
@SneakyThrows
public static void main(String[] args) {

View File

@ -0,0 +1,30 @@
package xyz.mcutils.backend.common;
import lombok.Getter;
import lombok.experimental.UtilityClass;
@UtilityClass
public final class AppConfig {
/**
* Is the app running in a production environment?
*/
@Getter
private static final boolean production;
static { // Are we running on production?
String env = System.getenv("ENVIRONMENT");
production = env != null && (env.equals("production"));
}
/**
* Is the app running in a test environment?
*/
@Getter
private static boolean isRunningTest = true;
static {
try {
Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
} catch (ClassNotFoundException e) {
isRunningTest = false;
}
}
}

View File

@ -3,6 +3,7 @@ package xyz.mcutils.backend.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@ -93,4 +94,19 @@ public final class ColorUtils {
return builder.toString();
}
/**
* Gets a {@link Color} from a Minecraft color code.
*
* @param colorCode the color code to get the color from
* @return the color
*/
public static Color getMinecraftColor(char colorCode) {
String color = COLOR_MAP.getOrDefault(colorCode, null);
if (color == null) {
throw new IllegalArgumentException("Invalid color code: " + colorCode);
}
return Color.decode(color);
}
}

View File

@ -0,0 +1,28 @@
package xyz.mcutils.backend.common;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
@Log4j2(topic = "Fonts")
public class Fonts {
public static final Font MINECRAFT;
public static final Font MINECRAFT_BOLD;
public static final Font MINECRAFT_ITALIC;
static {
InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
try {
MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
MINECRAFT_ITALIC = MINECRAFT.deriveFont(Font.ITALIC);
} catch (FontFormatException | IOException e) {
log.error("Failed to load Minecraft font", e);
throw new RuntimeException("Failed to load Minecraft font", e);
}
}
}

View File

@ -8,21 +8,23 @@ import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
@Log4j2
@Log4j2(topic = "Image Utils")
public class ImageUtils {
/**
* Scale the given image to the provided size.
* Scale the given image to the provided scale.
*
* @param image the image to scale
* @param size the size to scale the image to
* @param scale the scale to scale the image to
* @return the scaled image
*/
public static BufferedImage resize(BufferedImage image, double size) {
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB);
public static BufferedImage resize(BufferedImage image, double scale) {
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaled.createGraphics();
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null);
graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
graphics.dispose();
return scaled;
}
@ -56,4 +58,21 @@ public class ImageUtils {
throw new Exception("Failed to convert image to bytes", e);
}
}
/**
* Convert a base64 string to an image.
*
* @param base64 the base64 string to convert
* @return the image
*/
@SneakyThrows
public static BufferedImage base64ToImage(String base64) {
String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
try {
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
} catch (Exception e) {
throw new Exception("Failed to convert base64 to image", e);
}
}
}

View File

@ -0,0 +1,85 @@
package xyz.mcutils.backend.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@ToString
public enum MojangServer {
SESSION("Session Server", "https://sessionserver.mojang.com"),
API("Mojang API", "https://api.mojang.com"),
TEXTURES("Textures Server", "https://textures.minecraft.net"),
ASSETS("Assets Server", "https://assets.mojang.com"),
LIBRARIES("Libraries Server", "https://libraries.minecraft.net"),
SERVICES("Minecraft Services", "https://api.minecraftservices.com");
private static final long STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
/**
* The name of this server.
*/
@NonNull private final String name;
/**
* The endpoint of this service.
*/
@NonNull private final String endpoint;
/**
* Ping this service and get the status of it.
*
* @return the service status
*/
@NonNull
public Status getStatus() {
try {
InetAddress address = InetAddress.getByName(endpoint.substring(8));
long before = System.currentTimeMillis();
if (address.isReachable((int) STATUS_TIMEOUT)) {
// The time it took to reach the host is 75% of
// the timeout, consider it to be degraded.
if ((System.currentTimeMillis() - before) > STATUS_TIMEOUT * 0.75D) {
return Status.DEGRADED;
}
return Status.ONLINE;
}
} catch (UnknownHostException ex) {
ex.printStackTrace();
} catch (IOException ignored) {
// We can safely ignore any errors, we're simply checking
// if the host is reachable, if it's not, then it's offline.
}
return Status.OFFLINE;
}
/**
* The status of a service.
*/
public enum Status {
/**
* The service is online and accessible.
*/
ONLINE,
/**
* The service is online, but is experiencing degraded performance.
*/
DEGRADED,
/**
* The service is offline and inaccessible.
*/
OFFLINE
}
}

View File

@ -12,7 +12,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
@UtilityClass @Log4j2
@UtilityClass @Log4j2(topic = "Player Utils")
public class PlayerUtils {
/**

View File

@ -0,0 +1,14 @@
package xyz.mcutils.backend.common.renderer;
import java.awt.image.BufferedImage;
public abstract class Renderer<T> {
/**
* Renders the object to the specified size.
*
* @param input The object to render.
* @param size The size to render the object to.
*/
public abstract BufferedImage render(T input, int size);
}

View File

@ -11,7 +11,7 @@ import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@Log4j2
@Log4j2(topic = "Skin Renderer")
public abstract class SkinRenderer<T extends ISkinPart> {
/**

View File

@ -0,0 +1,176 @@
package xyz.mcutils.backend.common.renderer.impl.server;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.ColorUtils;
import xyz.mcutils.backend.common.Fonts;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.Renderer;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.service.ServerService;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@Log4j2
public class ServerPreviewRenderer extends Renderer<MinecraftServer> {
public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
private static BufferedImage SERVER_BACKGROUND;
private static BufferedImage PING_ICON;
static {
try {
SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
} catch (Exception ex) {
log.error("Failed to load server preview assets", ex);
}
}
private final int fontSize = Fonts.MINECRAFT.getSize();
private final int width = 560;
private final int height = 64 + 3 + 3;
private final int padding = 3;
@Override
public BufferedImage render(MinecraftServer server, int size) {
BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
BufferedImage favicon = getServerFavicon(server);
BufferedImage background = SERVER_BACKGROUND;
// Create the graphics for drawing
Graphics2D graphics = texture.createGraphics();
// Set up the font
graphics.setFont(Fonts.MINECRAFT);
// Draw the background
for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
graphics.drawImage(background, backgroundX, backgroundY, null);
}
}
int y = fontSize + 1;
int x = 64 + 8;
int initialX = x; // Store the initial value of x
// Draw the favicon
graphics.drawImage(favicon, padding, padding, null);
// Draw the server hostname
graphics.setColor(Color.WHITE);
graphics.drawString(server.getHostname(), x, y);
// Draw the server motd
y += fontSize + (padding * 2);
for (String line : server.getMotd().getRaw()) {
int index = 0;
int colorIndex = line.indexOf("§");
while (colorIndex != -1) {
// Draw text before color code
String textBeforeColor = line.substring(index, colorIndex);
graphics.drawString(textBeforeColor, x, y);
// Calculate width of text before color code
int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
// Move x position to after the drawn text
x += textWidth;
// Set color based on color code
char colorCode = Character.toLowerCase(line.charAt(colorIndex + 1));
// Set the color and font style
switch (colorCode) {
case 'l': graphics.setFont(Fonts.MINECRAFT_BOLD);
case 'o': graphics.setFont(Fonts.MINECRAFT_ITALIC);
default: {
try {
graphics.setFont(Fonts.MINECRAFT);
Color color = ColorUtils.getMinecraftColor(colorCode);
graphics.setColor(color);
} catch (Exception ignored) {
// Unknown color, can ignore the error
}
}
}
// Move index to after the color code
index = colorIndex + 2;
// Find next color code
colorIndex = line.indexOf("§", index);
}
// Draw remaining text
String remainingText = line.substring(index);
graphics.drawString(remainingText, x, y);
// Move to the next line
y += fontSize + padding;
// Reset x position for the next line
x = initialX; // Reset x to its initial value
}
// Ensure the font is reset
graphics.setFont(Fonts.MINECRAFT);
// Render the ping
BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
x = width - pingIcon.getWidth() - padding;
graphics.drawImage(pingIcon, x, padding, null);
// Reset the y position
y = fontSize + 1;
// Render the player count
MinecraftServer.Players players = server.getPlayers();
String playersOnline = players.getOnline() + "";
String playersMax = players.getMax() + "";
// Calculate the width of each player count element
int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
int slashWidth = graphics.getFontMetrics().stringWidth("/");
int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
// Calculate the total width of the player count string
int totalWidth = maxWidth + slashWidth + onlineWidth;
// Calculate the starting x position
int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
// Render the player count elements
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersOnline, startX, y);
startX += onlineWidth;
graphics.setColor(Color.DARK_GRAY);
graphics.drawString("/", startX, y);
startX += slashWidth;
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersMax, startX, y);
return ImageUtils.resize(texture, (double) size / width);
}
/**
* Get the favicon of a server.
*
* @param server the server to get the favicon of
* @return the server favicon
*/
public BufferedImage getServerFavicon(MinecraftServer server) {
String favicon = null;
// Get the server favicon
if (server instanceof JavaMinecraftServer javaServer) {
if (javaServer.getFavicon() != null) {
favicon = javaServer.getFavicon().getBase64();
}
}
// Fallback to the default server icon
if (favicon == null) {
favicon = ServerService.DEFAULT_SERVER_ICON;
}
return ImageUtils.base64ToImage(favicon);
}
}

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -11,7 +11,7 @@ import xyz.mcutils.backend.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;
@AllArgsConstructor @Getter @Log4j2
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Body")
public class BodyRenderer extends SkinRenderer<ISkinPart.Custom> {
public static final BodyRenderer INSTANCE = new BodyRenderer();

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart;

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -10,7 +10,7 @@ import xyz.mcutils.backend.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;
@AllArgsConstructor @Getter @Log4j2
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Square")
public class SquareRenderer extends SkinRenderer<ISkinPart.Vanilla> {
public static final SquareRenderer INSTANCE = new SquareRenderer();

View File

@ -6,13 +6,15 @@ import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Getter @Log4j2
@Getter @Log4j2(topic = "Config")
@Configuration
public class Config {
public static Config INSTANCE;
@ -23,18 +25,17 @@ public class Config {
@Value("${public-url}")
private String webPublicUrl;
/**
* Whether the server is in production mode.
*/
private boolean production = false;
@PostConstruct
public void onInitialize() {
INSTANCE = this;
}
String environmentProperty = environment.getProperty("ENVIRONMENT", "development");
production = environmentProperty.equalsIgnoreCase("production"); // Set the production mode
log.info("Server is running in {} mode", production ? "production" : "development");
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
@Bean

View File

@ -0,0 +1,15 @@
package xyz.mcutils.backend.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@Configuration
@EnableMongoRepositories(basePackages = "xyz.mcutils.backend.repository.mongo")
public class MongoConfig {
@Autowired
void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
mappingMongoConverter.setMapKeyDotReplacement("-DOT");
}
}

View File

@ -8,12 +8,14 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
/**
* @author Braydon
*/
@Configuration
@Log4j2(topic = "Redis")
@EnableRedisRepositories(basePackages = "xyz.mcutils.backend.repository.redis")
public class RedisConfig {
/**
* The Redis server host.

View File

@ -1,22 +0,0 @@
package xyz.mcutils.backend.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import xyz.mcutils.backend.service.MetricService;
import xyz.mcutils.backend.websocket.MetricsWebSocketHandler;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MetricService metricService;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MetricsWebSocketHandler(metricService), "/websocket/metrics").setAllowedOrigins("*");
}
}

View File

@ -2,7 +2,9 @@ package xyz.mcutils.backend.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@ -10,17 +12,21 @@ import org.springframework.web.bind.annotation.RestController;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
import xyz.mcutils.backend.service.MojangService;
@RestController
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
public class MojangController {
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
public class MojangController {
@Autowired
private MojangService mojangService;
@ResponseBody
@GetMapping(value = "/status")
public CachedEndpointStatus getStatus() {
return mojangService.getMojangApiStatus();
public ResponseEntity<?> getStatus() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
.body(Map.of("endpoints", mojangService.getMojangServerStatus()));
}
}

View File

@ -16,11 +16,10 @@ import xyz.mcutils.backend.service.PlayerService;
import java.util.concurrent.TimeUnit;
@RestController
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
@RequestMapping(value = "/player/")
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
public class PlayerController {
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
private final PlayerService playerService;
@Autowired
@ -32,16 +31,22 @@ public class PlayerController {
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getPlayer(
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
CachedPlayer player = playerService.getPlayer(id);
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body(playerService.getPlayer(id));
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.body(player);
}
@ResponseBody
@GetMapping(value = "/uuid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public CachedPlayerName getPlayerUuid(
public ResponseEntity<CachedPlayerName> getPlayerUuid(
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
return playerService.usernameToUuid(id);
CachedPlayerName player = playerService.usernameToUuid(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePublic())
.body(player);
}
@GetMapping(value = "/{part}/{id}")
@ -57,7 +62,7 @@ public class PlayerController {
// Return the part image
return ResponseEntity.ok()
.cacheControl(cacheControl)
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
.body(playerService.getSkinPart(player, part, overlays, size).getBytes());

View File

@ -3,6 +3,7 @@ package xyz.mcutils.backend.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@ -12,10 +13,11 @@ import xyz.mcutils.backend.service.MojangService;
import xyz.mcutils.backend.service.ServerService;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
@RequestMapping(value = "/server/")
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
public class ServerController {
private final ServerService serverService;
@ -29,31 +31,56 @@ public class ServerController {
@ResponseBody
@GetMapping(value = "/{platform}/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
public CachedMinecraftServer getServer(
public ResponseEntity<CachedMinecraftServer> getServer(
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname) {
return serverService.getServer(platform, hostname);
CachedMinecraftServer server = serverService.getServer(platform, hostname);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
.body(server);
}
@ResponseBody
@GetMapping(value = "/icon/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<?> getServerIcon(
public ResponseEntity<byte[]> getServerIcon(
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
byte[] favicon = serverService.getServerFavicon(hostname);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
.body(serverService.getServerFavicon(hostname));
.body(favicon);
}
@ResponseBody
@GetMapping(value = "/{platform}/preview/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<byte[]> getServerPreview(
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download,
@Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) {
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
CachedMinecraftServer server = serverService.getServer(platform, hostname);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
.contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
.body(serverService.getServerPreview(server, platform, size));
}
@ResponseBody
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getServerBlockedStatus(
@Parameter(description = "The hostname of the server", example = "aetheria.cc") @PathVariable String hostname) {
return ResponseEntity.ok(Map.of(
"blocked", mojangService.isServerBlocked(hostname)
));
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.body(Map.of(
"blocked", mojangService.isServerBlocked(hostname)
));
}
}

View File

@ -1,6 +1,7 @@
package xyz.mcutils.backend.exception;
import io.micrometer.common.lang.NonNull;
import io.sentry.Sentry;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
}
if (status == null) { // Fallback to 500
status = HttpStatus.INTERNAL_SERVER_ERROR;
Sentry.captureException(ex); // Capture the exception with Sentry
}
return new ResponseEntity<>(new ErrorResponse(status, message), status);
}

View File

@ -1,19 +1,22 @@
package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
import xyz.mcutils.backend.model.mojang.EndpointStatus;
import xyz.mcutils.backend.common.MojangServer;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Setter @Getter @ToString
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
public class CachedEndpointStatus extends CachedResponse implements Serializable {
@ -26,12 +29,21 @@ public class CachedEndpointStatus extends CachedResponse implements Serializable
/**
* The endpoint cache.
*/
@JsonUnwrapped
private final EndpointStatus value;
private final List<Map<String, Object>> endpoints;
public CachedEndpointStatus(@NonNull String id, EndpointStatus value) {
public CachedEndpointStatus(@NonNull String id, Map<MojangServer, MojangServer.Status> mojangServers) {
super(Cache.defaultCache());
this.id = id;
this.value = value;
this.endpoints = new ArrayList<>();
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
MojangServer server = entry.getKey();
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("name", server.getName());
serverStatus.put("endpoint", server.getEndpoint());
serverStatus.put("status", entry.getValue().name());
endpoints.add(serverStatus);
}
}
}

View File

@ -2,7 +2,10 @@ package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
@ -13,8 +16,7 @@ import java.io.Serializable;
/**
* @author Braydon
*/
@Setter @Getter @ToString
@NoArgsConstructor
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
public class CachedMinecraftServer extends CachedResponse implements Serializable {
/**

View File

@ -2,8 +2,8 @@ package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@ -18,8 +18,7 @@ import java.util.UUID;
*
* @author Braydon
*/
@Setter @Getter
@NoArgsConstructor
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
public class CachedPlayer extends CachedResponse implements Serializable {
/**

View File

@ -1,8 +1,9 @@
package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
@ -12,8 +13,8 @@ import java.util.UUID;
/**
* @author Braydon
*/
@Getter
@ToString
@Setter
@Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "playerName", timeToLive = 60L * 60L * 6) // 6 hours (in seconds)
public class CachedPlayerName extends CachedResponse {
/**

View File

@ -1,15 +1,11 @@
package xyz.mcutils.backend.model.cache;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@Setter
@Getter
@AllArgsConstructor
@Setter @Getter @EqualsAndHashCode
@RedisHash(value = "playerSkinPart", timeToLive = 60L * 60L) // 1 hour (in seconds)
public class CachedPlayerSkinPart {

View File

@ -0,0 +1,21 @@
package xyz.mcutils.backend.model.cache;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@AllArgsConstructor
@Setter @Getter @EqualsAndHashCode
@RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds)
public class CachedServerPreview {
/**
* The ID of the server preview
*/
@Id @NonNull private String id;
/**
* The server preview bytes
*/
private byte[] bytes;
}

View File

@ -1,13 +1,10 @@
package xyz.mcutils.backend.model.dns;
import io.micrometer.common.lang.NonNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
@Setter @Getter
@NoArgsConstructor @AllArgsConstructor
@Setter @Getter @EqualsAndHashCode
public abstract class DNSRecord {
/**
* The type of this record.

View File

@ -1,19 +1,32 @@
package xyz.mcutils.backend.model.mojang;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.util.Map;
@AllArgsConstructor
@Getter
@RequiredArgsConstructor
@Getter @Setter @EqualsAndHashCode
public class EndpointStatus {
/**
* The list of endpoints and their status.
* The name of the service.
*/
private final Map<String, Status> endpoints;
private final String name;
/**
* The hostname of the service.
*/
private final String hostname;
/**
* The status of the service.
*/
private Status status;
/**
* Statuses for the endpoint.
*/
public enum Status {
/**
* The service is online and operational.

View File

@ -2,9 +2,11 @@ package xyz.mcutils.backend.model.player;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter @AllArgsConstructor
@AllArgsConstructor
@Getter @EqualsAndHashCode
public class Cape {
/**

View File

@ -1,6 +1,7 @@
package xyz.mcutils.backend.model.player;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import xyz.mcutils.backend.common.Tuple;
@ -10,7 +11,8 @@ import xyz.mcutils.backend.model.token.MojangProfileToken;
import java.util.UUID;
@Getter @AllArgsConstructor @NoArgsConstructor
@AllArgsConstructor @NoArgsConstructor
@Getter @EqualsAndHashCode
public class Player {
/**

View File

@ -1,14 +1,14 @@
package xyz.mcutils.backend.model.response;
import io.micrometer.common.lang.NonNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.http.HttpStatus;
import java.util.Date;
@Getter
@ToString
@Getter @ToString @EqualsAndHashCode
public class ErrorResponse {
/**
* The status code of this error.

View File

@ -32,8 +32,8 @@ public final class BedrockMinecraftServer extends MinecraftServer {
private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records,
@NonNull Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd,
@NonNull GameMode gamemode) {
super(hostname, ip, port, records, motd, players);
@NonNull GameMode gamemode, GeoLocation location) {
super(hostname, ip, port, records, motd, players, location);
this.id = id;
this.edition = edition;
this.version = version;
@ -53,12 +53,12 @@ public final class BedrockMinecraftServer extends MinecraftServer {
* @return the Bedrock Minecraft server
*/
@NonNull
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull String token) {
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull String token) {
String[] split = token.split(";"); // Split the token
Edition edition = Edition.valueOf(split[0]);
Version version = new Version(Integer.parseInt(split[2]), split[3]);
Players players = new Players(Integer.parseInt(split[4]), Integer.parseInt(split[5]), null);
MOTD motd = MOTD.create(split[1] + "\n" + split[7]);
MOTD motd = MOTD.create(hostname, Platform.BEDROCK, split[1] + "\n" + split[7]);
GameMode gameMode = new GameMode(split[8], split.length > 9 ? Integer.parseInt(split[9]) : -1);
return new BedrockMinecraftServer(
split[6],
@ -70,7 +70,8 @@ public final class BedrockMinecraftServer extends MinecraftServer {
version,
players,
motd,
gameMode
gameMode,
location
);
}

View File

@ -14,7 +14,7 @@ import xyz.mcutils.backend.model.token.JavaServerStatusToken;
/**
* @author Braydon
*/
@Setter @Getter
@Setter @Getter @EqualsAndHashCode(callSuper = false)
public final class JavaMinecraftServer extends MinecraftServer {
/**
@ -65,10 +65,10 @@ public final class JavaMinecraftServer extends MinecraftServer {
*/
private boolean mojangBlocked;
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, DNSRecord[] records,
@NonNull Version version, Favicon favicon, ForgeModInfo modInfo, ForgeData forgeData,
boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
super(hostname, ip, port, records, motd, players);
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, GeoLocation location,
DNSRecord[] records, @NonNull Version version, Favicon favicon, ForgeModInfo modInfo,
ForgeData forgeData, boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
super(hostname, ip, port, records, motd, players, location);
this.version = version;
this.favicon = favicon;
this.modInfo = modInfo;
@ -88,7 +88,7 @@ public final class JavaMinecraftServer extends MinecraftServer {
* @return the Java Minecraft server
*/
@NonNull
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, @NonNull JavaServerStatusToken token) {
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull JavaServerStatusToken token) {
String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null;
if (motdString == null) { // Not a string motd, convert from Json
motdString = new TextComponent(ComponentSerializer.parse(Main.GSON.toJson(token.getDescription()))).toLegacyText();
@ -97,8 +97,9 @@ public final class JavaMinecraftServer extends MinecraftServer {
hostname,
ip,
port,
MinecraftServer.MOTD.create(motdString),
MinecraftServer.MOTD.create(hostname, Platform.JAVA, motdString),
token.getPlayers(),
location,
records,
token.getVersion().detailedCopy(),
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)),

View File

@ -1,11 +1,10 @@
package xyz.mcutils.backend.model.server;
import com.maxmind.geoip2.model.CityResponse;
import io.micrometer.common.lang.NonNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.*;
import xyz.mcutils.backend.common.ColorUtils;
import xyz.mcutils.backend.config.Config;
import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
import xyz.mcutils.backend.service.pinger.impl.BedrockMinecraftServerPinger;
@ -18,7 +17,7 @@ import java.util.UUID;
* @author Braydon
*/
@AllArgsConstructor
@Getter @Setter
@Getter @Setter @EqualsAndHashCode
public class MinecraftServer {
/**
@ -51,6 +50,11 @@ public class MinecraftServer {
*/
private final Players players;
/**
* The location of the server.
*/
private final GeoLocation location;
/**
* A platform a Minecraft
* server can operate on.
@ -97,6 +101,11 @@ public class MinecraftServer {
*/
private final String[] html;
/**
* The URL to the server preview image.
*/
private final String preview;
/**
* Create a new MOTD from a raw string.
*
@ -104,12 +113,14 @@ public class MinecraftServer {
* @return the new motd
*/
@NonNull
public static MOTD create(@NonNull String raw) {
public static MOTD create(@NonNull String hostname, @NonNull Platform platform, @NonNull String raw) {
String[] rawLines = raw.split("\n"); // The raw lines
return new MOTD(
rawLines,
Arrays.stream(rawLines).map(ColorUtils::stripColor).toArray(String[]::new),
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new)
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new),
Config.INSTANCE.getWebPublicUrl() + "/server/%s/preview/%s".formatted(
platform.name().toLowerCase(),hostname)
);
}
}
@ -150,4 +161,54 @@ public class MinecraftServer {
@NonNull private final String name;
}
}
/**
* The location of the server.
*/
@AllArgsConstructor @Getter
public static class GeoLocation {
/**
* The country of the server.
*/
private final String country;
/**
* The region of the server.
*/
private final String region;
/**
* The city of the server.
*/
private final String city;
/**
* The latitude of the server.
*/
private final double latitude;
/**
* The longitude of the server.
*/
private final double longitude;
/**
* Gets the location of the server from Maxmind.
*
* @param response the response from Maxmind
* @return the location of the server
*/
public static GeoLocation fromMaxMind(CityResponse response) {
if (response == null) {
return null;
}
return new GeoLocation(
response.getCountry().getName(),
response.getMostSpecificSubdivision().getName(),
response.getCity().getName(),
response.getLocation().getLatitude(),
response.getLocation().getLongitude()
);
}
}
}

View File

@ -3,9 +3,9 @@ package xyz.mcutils.backend.model.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import xyz.mcutils.backend.common.renderer.SkinRenderer;
import xyz.mcutils.backend.common.renderer.impl.BodyRenderer;
import xyz.mcutils.backend.common.renderer.impl.IsometricHeadRenderer;
import xyz.mcutils.backend.common.renderer.impl.SquareRenderer;
import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer;
import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer;
import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer;
import java.awt.image.BufferedImage;

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
@ -18,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor @NoArgsConstructor
@Getter @Log4j2
@Getter @Log4j2(topic = "Skin") @EqualsAndHashCode
public class Skin {
/**
* The URL for the skin

View File

@ -1,13 +0,0 @@
package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
/**
* A cache repository for {@link CachedEndpointStatus}'s.
*
* @author Braydon
*/
@Repository
public interface EndpointStatusRepository extends CrudRepository<CachedEndpointStatus, String> { }

View File

@ -1,11 +0,0 @@
package xyz.mcutils.backend.repository;
import org.springframework.data.repository.CrudRepository;
import xyz.mcutils.backend.service.metric.Metric;
/**
* A repository for {@link Metric}s.
*
* @author Braydon
*/
public interface MetricsRepository extends CrudRepository<Metric<?>, String> { }

View File

@ -0,0 +1,11 @@
package xyz.mcutils.backend.repository.mongo;
import org.springframework.data.mongodb.repository.MongoRepository;
import xyz.mcutils.backend.service.metric.Metric;
/**
* A repository for {@link Metric}s.
*
* @author Braydon
*/
public interface MetricsRepository extends MongoRepository<Metric<?>, String> { }

View File

@ -1,7 +1,6 @@
package xyz.mcutils.backend.repository;
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
/**
@ -9,5 +8,4 @@ import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
*
* @author Braydon
*/
@Repository
public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { }

View File

@ -1,7 +1,6 @@
package xyz.mcutils.backend.repository;
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayer;
import java.util.UUID;
@ -11,5 +10,4 @@ import java.util.UUID;
*
* @author Braydon
*/
@Repository
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }

View File

@ -1,7 +1,6 @@
package xyz.mcutils.backend.repository;
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayerName;
/**
@ -13,5 +12,4 @@ import xyz.mcutils.backend.model.cache.CachedPlayerName;
*
* @author Braydon
*/
@Repository
public interface PlayerNameCacheRepository extends CrudRepository<CachedPlayerName, String> { }

View File

@ -1,7 +1,6 @@
package xyz.mcutils.backend.repository;
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart;
/**
@ -11,5 +10,4 @@ import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart;
* player skin part by it's id.
* </p>
*/
@Repository
public interface PlayerSkinPartCacheRepository extends CrudRepository<CachedPlayerSkinPart, String> { }

View File

@ -0,0 +1,9 @@
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import xyz.mcutils.backend.model.cache.CachedServerPreview;
/**
* A cache repository for server previews.
*/
public interface ServerPreviewCacheRepository extends CrudRepository<CachedServerPreview, String> { }

View File

@ -0,0 +1,125 @@
package xyz.mcutils.backend.service;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CityResponse;
import io.sentry.Sentry;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.Main;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
@Service
@Log4j2(topic = "MaxMind Service")
public class MaxMindService {
/**
* The MaxMind database.
*/
private static DatabaseReader database;
/**
* The location of the MaxMind database.
*/
private final String databaseName = "maxmind.mmdb";
/**
* The MaxMind license key.
*/
private final String maxMindLicense;
public MaxMindService(@Value("${maxmind.license}") String maxMindLicense) {
this.maxMindLicense = maxMindLicense;
if (maxMindLicense.isBlank()) {
log.error("The MaxMind license key is not set, please set it in the configuration and try again, disabling the MaxMind service...");
return;
}
File databaseFile = loadDatabase();
try {
database = new DatabaseReader.Builder(databaseFile).build();
log.info("Loaded the MaxMind database from '{}'", databaseFile.getAbsolutePath());
} catch (Exception ex) {
log.error("Failed to load the MaxMind database, please check the configuration and try again", ex);
System.exit(1);
}
}
/**
* Lookup the GeoIP information for the ip.
*
* @param ip The query to lookup
* @return The GeoIP information
*/
public static CityResponse lookup(String ip) {
if (database == null) { // The database isn't loaded, return null
return null;
}
try {
return database.city(InetAddress.getByName(ip));
} catch (IOException | GeoIp2Exception e) {
log.error("Failed to lookup the GeoIP information for '{}'", ip, e);
Sentry.captureException(e);
return null;
}
}
@SneakyThrows
private File loadDatabase() {
File database = new File("data", databaseName);
if (database.exists()) {
return database;
}
// Ensure the parent directories exist
database.getParentFile().mkdirs();
String downloadUrl = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz";
HttpResponse<Path> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder()
.uri(URI.create(downloadUrl.formatted(maxMindLicense)))
.build(), HttpResponse.BodyHandlers.ofFile(Files.createTempFile("maxmind", ".tar.gz")));
Path downloadedFile = response.body();
File tempDir = Files.createTempDirectory("maxmind").toFile();
TarGZipUnArchiver archiver = new TarGZipUnArchiver();
archiver.setSourceFile(downloadedFile.toFile());
archiver.setDestDirectory(tempDir);
archiver.extract();
File[] files = tempDir.listFiles();
if (files == null || files.length == 0) {
log.error("Failed to extract the MaxMind database");
System.exit(1);
}
// Search for the database file
for (File file : files) {
// The database is in a subdirectory
if (!file.isDirectory()) {
continue;
}
// Get the database file
File databaseFile = new File(file, "GeoLite2-City.mmdb");
if (!databaseFile.exists()) {
log.error("Failed to find the MaxMind database in the extracted files");
continue;
}
Files.copy(databaseFile.toPath(), database.toPath());
}
log.info("Downloaded and extracted the MaxMind database to '{}'", database.getAbsolutePath());
return database;
}
}

View File

@ -6,8 +6,9 @@ import com.influxdb.spring.influx.InfluxDB2AutoConfiguration;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.common.AppConfig;
import xyz.mcutils.backend.common.Timer;
import xyz.mcutils.backend.repository.MetricsRepository;
import xyz.mcutils.backend.repository.mongo.MetricsRepository;
import xyz.mcutils.backend.service.metric.Metric;
import xyz.mcutils.backend.service.metric.metrics.*;
import xyz.mcutils.backend.service.metric.metrics.process.CpuUsageMetric;
@ -19,7 +20,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service @Log4j2
@Service @Log4j2(topic = "Metric Service")
public class MetricService {
/**
* The metrics that are registered.
@ -39,22 +40,35 @@ public class MetricService {
this.influxWriteApi = influxAutoConfiguration.influxDBClient().getWriteApiBlocking();
this.metricsRepository = metricsRepository;
Map<Metric<?>, Boolean> collectorEnabled = new HashMap<>();
// Register the metrics
registerMetric(new TotalRequestsMetric());
registerMetric(new RequestsPerRouteMetric());
registerMetric(new MemoryMetric());
registerMetric(new CpuUsageMetric());
registerMetric(new TotalPlayerLookupsMetric());
registerMetric(new TotalServerLookupsMetric());
registerMetric(new ConnectedSocketsMetric());
registerMetric(new UniquePlayerLookupsMetric());
registerMetric(new UniqueServerLookupsMetric());
// Load the metrics from Redis
loadMetrics();
// please god forgive my sins; this is the worst code I've ever written
for (Metric<?> metric : metrics.values()) {
collectorEnabled.put(metric, metric.isCollector());
}
Timer.scheduleRepeating(() -> {
saveMetrics();
writeToInflux();
}, saveInterval, saveInterval);
if (!AppConfig.isRunningTest()) {
// Load the metrics from Redis
loadMetrics();
for (Map.Entry<Metric<?>, Boolean> entry : collectorEnabled.entrySet()) {
entry.getKey().setCollector(entry.getValue());
}
Timer.scheduleRepeating(() -> {
saveMetrics();
writeToInflux();
}, saveInterval, saveInterval);
}
}
/**
@ -88,8 +102,8 @@ public class MetricService {
*/
public void loadMetrics() {
log.info("Loading metrics");
for (Metric<?> metric : metricsRepository.findAll()) {
metrics.put(metric.getClass(), metric);
for (Metric<?> metric : metrics.values()) {
metricsRepository.findById(metric.getId()).ifPresent(loaded -> metrics.put(loaded.getClass(), loaded));
}
log.info("Loaded {} metrics", metrics.size());
}
@ -101,7 +115,7 @@ public class MetricService {
for (Metric<?> metric : metrics.values()) {
saveMetric(metric);
}
log.info("Saved {} metrics to Redis", metrics.size());
log.info("Saved {} metrics to MongoDB", metrics.size());
}
/**
@ -110,24 +124,32 @@ public class MetricService {
* @param metric the metric to save
*/
private void saveMetric(Metric<?> metric) {
metricsRepository.save(metric); // Save the metric to the repository
try {
metricsRepository.save(metric); // Save the metric to the repository
} catch (Exception e) {
log.error("Failed to save metric to MongoDB", e);
}
}
/**
* Push all metrics to InfluxDB.
*/
private void writeToInflux() {
List<Point> points = new ArrayList<>();
for (Metric<?> metric : metrics.values()) {
if (metric.isCollector()) {
metric.collect();
}
Point point = metric.toPoint();
if (point != null) {
points.add(point);
try {
List<Point> points = new ArrayList<>();
for (Metric<?> metric : metrics.values()) {
if (metric.isCollector()) {
metric.collect();
}
Point point = metric.toPoint();
if (point != null) {
points.add(point);
}
}
influxWriteApi.writePoints(points);
log.info("Wrote {} metrics to Influx", metrics.size());
} catch (Exception e) {
log.error("Failed to write metrics to Influx", e);
}
influxWriteApi.writePoints(points);
log.info("Wrote {} metrics to Influx", metrics.size());
}
}

View File

@ -9,29 +9,23 @@ import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpirationPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.Endpoint;
import xyz.mcutils.backend.common.ExpiringSet;
import xyz.mcutils.backend.common.MojangServer;
import xyz.mcutils.backend.common.WebRequest;
import xyz.mcutils.backend.config.Config;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
import xyz.mcutils.backend.model.mojang.EndpointStatus;
import xyz.mcutils.backend.model.token.MojangProfileToken;
import xyz.mcutils.backend.model.token.MojangUsernameToUuidToken;
import xyz.mcutils.backend.repository.EndpointStatusRepository;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Service @Log4j2 @Getter
@Service
@Log4j2(topic = "Mojang Service")
@Getter
public class MojangService {
/**
@ -53,21 +47,9 @@ public class MojangService {
private static final long FETCH_BLOCKED_SERVERS_INTERVAL = TimeUnit.HOURS.toMillis(1L);
/**
* Information about the Mojang API endpoints.
* The interval to fetch the Mojang server status.
*/
private static final String MOJANG_ENDPOINT_STATUS_KEY = "mojang";
private static final List<Endpoint> MOJANG_ENDPOINTS = List.of(
new Endpoint("https://textures.minecraft.net", List.of(HttpStatus.BAD_REQUEST)),
new Endpoint("https://session.minecraft.net", List.of(HttpStatus.NOT_FOUND)),
new Endpoint("https://libraries.minecraft.net", List.of(HttpStatus.NOT_FOUND)),
new Endpoint("https://assets.mojang.com", List.of(HttpStatus.NOT_FOUND)),
new Endpoint("https://api.minecraftservices.com", List.of(HttpStatus.FORBIDDEN)),
new Endpoint(API_ENDPOINT, List.of(HttpStatus.OK)),
new Endpoint(SESSION_SERVER_ENDPOINT, List.of(HttpStatus.FORBIDDEN))
);
@Autowired
private EndpointStatusRepository mojangEndpointStatusRepository;
private static final long FETCH_MOJANG_SERVERS_STATUS_INTERVAL = TimeUnit.MINUTES.toMillis(1L);
/**
* A list of banned server hashes provided by Mojang.
@ -87,6 +69,11 @@ public class MojangService {
*/
private final ExpiringSet<String> blockedServersCache = new ExpiringSet<>(ExpirationPolicy.CREATED, 10L, TimeUnit.MINUTES);
/**
* The status of the Mojang API.
*/
private final List<Map<String, Object>> mojangServerStatus = new ArrayList<>();
public MojangService() {
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
@ -94,6 +81,33 @@ public class MojangService {
fetchBlockedServers();
}
}, 0L, FETCH_BLOCKED_SERVERS_INTERVAL);
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
log.info("Fetching Mojang Server status...");
Map<MojangServer, MojangServer.Status> mojangServers = new HashMap<>();
Arrays.stream(MojangServer.values()).parallel().forEach(server -> {
log.info("Pinging {}...", server.getEndpoint());
MojangServer.Status status = server.getStatus(); // Retrieve the server status
log.info("Retrieved status of {}: {}", server.getEndpoint(), status.name());
mojangServers.put(server, status); // Cache the server status
});
mojangServerStatus.clear();
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
MojangServer server = entry.getKey();
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("name", server.getName());
serverStatus.put("endpoint", server.getEndpoint());
serverStatus.put("status", entry.getValue().name());
mojangServerStatus.add(serverStatus);
}
log.info("Fetched Mojang Server status for {} endpoints", mojangServers.size());
}
}, 0L, FETCH_MOJANG_SERVERS_STATUS_INTERVAL);
}
/**
@ -104,7 +118,7 @@ public class MojangService {
log.info("Fetching blocked servers from Mojang");
try (
InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream();
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n");
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n")
) {
List<String> hashes = new ArrayList<>();
while (scanner.hasNext()) {
@ -112,6 +126,8 @@ public class MojangService {
}
bannedServerHashes = Collections.synchronizedList(hashes);
log.info("Fetched {} banned server hashes", bannedServerHashes.size());
} catch (IOException e) {
log.error("Failed to fetch blocked servers from Mojang", e);
}
}
@ -182,62 +198,6 @@ public class MojangService {
return blocked;
}
/**
* Gets the status of the Mojang APIs.
*
* @return the status
*/
public CachedEndpointStatus getMojangApiStatus() {
log.info("Getting Mojang API status");
Optional<CachedEndpointStatus> endpointStatus = mojangEndpointStatusRepository.findById(MOJANG_ENDPOINT_STATUS_KEY);
if (endpointStatus.isPresent() && Config.INSTANCE.isProduction()) {
log.info("Got cached Mojang API status");
return endpointStatus.get();
}
// Fetch the status of the Mojang API endpoints
List<CompletableFuture<EndpointStatus.Status>> futures = new ArrayList<>();
for (Endpoint endpoint : MOJANG_ENDPOINTS) {
CompletableFuture<EndpointStatus.Status> future = CompletableFuture.supplyAsync(() -> {
boolean online = false;
long start = System.currentTimeMillis();
ResponseEntity<?> response = WebRequest.head(endpoint.getEndpoint(), String.class);
if (endpoint.getAllowedStatuses().contains(response.getStatusCode())) {
online = true;
}
if (online && System.currentTimeMillis() - start > 1000) { // If the response took longer than 1 second
return EndpointStatus.Status.DEGRADED;
}
return online ? EndpointStatus.Status.ONLINE : EndpointStatus.Status.OFFLINE;
}, Main.EXECUTOR_POOL);
futures.add(future);
}
CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
try {
allFutures.get(5, TimeUnit.SECONDS); // Wait for the futures to complete
} catch (Exception e) {
log.error("Timeout while fetching Mojang API status: {}", e.getMessage());
}
// Process the results
Map<String, EndpointStatus.Status> endpoints = new HashMap<>();
for (int i = 0; i < MOJANG_ENDPOINTS.size(); i++) {
Endpoint endpoint = MOJANG_ENDPOINTS.get(i);
EndpointStatus.Status status = futures.get(i).join();
endpoints.put(endpoint.getEndpoint(), status);
}
log.info("Fetched Mojang API status for {} endpoints", endpoints.size());
CachedEndpointStatus status = new CachedEndpointStatus(
MOJANG_ENDPOINT_STATUS_KEY,
new EndpointStatus(endpoints)
);
mojangEndpointStatusRepository.save(status);
status.getCache().setCached(false);
return status;
}
/**
* Gets the Session Server profile of the
* player with the given UUID.

View File

@ -3,11 +3,7 @@ package xyz.mcutils.backend.service;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.PlayerUtils;
import xyz.mcutils.backend.common.Tuple;
import xyz.mcutils.backend.common.UUIDUtils;
import xyz.mcutils.backend.config.Config;
import xyz.mcutils.backend.common.*;
import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.MojangAPIRateLimitException;
import xyz.mcutils.backend.exception.impl.RateLimitException;
@ -21,17 +17,16 @@ import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.model.skin.Skin;
import xyz.mcutils.backend.model.token.MojangProfileToken;
import xyz.mcutils.backend.model.token.MojangUsernameToUuidToken;
import xyz.mcutils.backend.repository.PlayerCacheRepository;
import xyz.mcutils.backend.repository.PlayerNameCacheRepository;
import xyz.mcutils.backend.repository.PlayerSkinPartCacheRepository;
import xyz.mcutils.backend.service.metric.metrics.TotalPlayerLookupsMetric;
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
import xyz.mcutils.backend.repository.redis.PlayerCacheRepository;
import xyz.mcutils.backend.repository.redis.PlayerNameCacheRepository;
import xyz.mcutils.backend.repository.redis.PlayerSkinPartCacheRepository;
import xyz.mcutils.backend.service.metric.metrics.UniquePlayerLookupsMetric;
import java.awt.image.BufferedImage;
import java.util.Optional;
import java.util.UUID;
@Service @Log4j2
@Service @Log4j2(topic = "Player Service")
public class PlayerService {
private final MojangService mojangAPIService;
@ -65,10 +60,8 @@ public class PlayerService {
uuid = usernameToUuid(id).getUniqueId();
}
((TotalPlayerLookupsMetric) metricService.getMetric(TotalPlayerLookupsMetric.class)).increment(); // Increment the total player lookups
Optional<CachedPlayer> cachedPlayer = playerCacheRepository.findById(uuid);
if (cachedPlayer.isPresent() && Config.INSTANCE.isProduction()) { // Return the cached player if it exists
if (cachedPlayer.isPresent() && AppConfig.isProduction()) { // Return the cached player if it exists
log.info("Player {} is cached", id);
return cachedPlayer.get();
}
@ -90,6 +83,10 @@ public class PlayerService {
)
);
// Add the lookup to the unique player lookups metric
((UniquePlayerLookupsMetric) metricService.getMetric(UniquePlayerLookupsMetric.class))
.addLookup(uuid);
playerCacheRepository.save(player);
player.getCache().setCached(false);
return player;
@ -108,7 +105,7 @@ public class PlayerService {
log.info("Getting UUID from username: {}", username);
String id = username.toUpperCase();
Optional<CachedPlayerName> cachedPlayerName = playerNameCacheRepository.findById(id);
if (cachedPlayerName.isPresent() && Config.INSTANCE.isProduction()) {
if (cachedPlayerName.isPresent() && AppConfig.isProduction()) {
return cachedPlayerName.get();
}
try {
@ -138,12 +135,10 @@ public class PlayerService {
*/
public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) {
if (size > 512) {
log.info("Size {} is too large, setting to 512", size);
size = 512;
throw new BadRequestException("Size cannot be larger than 512");
}
if (size < 32) {
log.info("Size {} is too small, setting to 32", size);
size = 32;
throw new BadRequestException("Size cannot be smaller than 32");
}
ISkinPart part = ISkinPart.getByName(partName); // The skin part to get
@ -158,7 +153,7 @@ public class PlayerService {
Optional<CachedPlayerSkinPart> cache = playerSkinPartCacheRepository.findById(key);
// The skin part is cached
if (cache.isPresent() && Config.INSTANCE.isProduction()) {
if (cache.isPresent() && AppConfig.isProduction()) {
log.info("Skin part {} for player {} is cached", name, player.getUniqueId());
return cache.get();
}

View File

@ -3,20 +3,23 @@ package xyz.mcutils.backend.service;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.common.AppConfig;
import xyz.mcutils.backend.common.DNSUtils;
import xyz.mcutils.backend.common.EnumUtils;
import xyz.mcutils.backend.config.Config;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.impl.server.ServerPreviewRenderer;
import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
import xyz.mcutils.backend.model.cache.CachedServerPreview;
import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.dns.impl.ARecord;
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.repository.MinecraftServerCacheRepository;
import xyz.mcutils.backend.service.metric.Metric;
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository;
import xyz.mcutils.backend.repository.redis.ServerPreviewCacheRepository;
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
import java.net.InetSocketAddress;
import java.util.ArrayList;
@ -24,19 +27,22 @@ import java.util.Base64;
import java.util.List;
import java.util.Optional;
@Service @Log4j2
@Service @Log4j2(topic = "Server Service")
public class ServerService {
private static final String DEFAULT_SERVER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAASFBMVEWwsLBBQUE9PT1JSUlFRUUuLi5MTEyzs7M0NDQ5OTlVVVVQUFAmJia5ubl+fn5zc3PFxcVdXV3AwMCJiYmUlJRmZmbQ0NCjo6OL5p+6AAAFVklEQVRYw+1W67K0KAzkJnIZdRAZ3/9NtzvgXM45dX7st1VbW7XBUVDSdEISRqn/5R+T82/+nsr/XZn/SHm/3x9/ArA/IP8qwPK433d44VubZ/XT6/cJy0L792VZfnDrcRznr86d748u92X5vtaxOe228zcCy+MSMpg/5SwRopsYMv8oigCwngbQhE/rzhwAYMpxnvMvHhgy/8AgByJolzb5pPqEbvtgMBBmtvkbgxKmaaIZ5TyPum6Viue6te241N+s+W6nOlucgjEx6Nay9zZta1XVxejW+Q5ZhhkDS31lgOTegjUBor33CQilbC2GYGy9y9bN8ytevjE4a2stajHDAgAcUkoYwzO6zQi8ZflC+XO0+exiuNa3OQtIJOCk13neUjv7VO7Asu/3LwDFeg37sQtQhy4lAQH6IR9ztca0E3oI5PtDAlJ1tHGplrJ12jjrrXPWYvXsU042Bl/qUr3B9qzPSKaovpvjgglYL2F1x+Zs7gIvpLYuq46wr3H5/RJxyvM6sXOY762oU4YZ3mAz1lpc9O3Y30VJUM/iWhBIib63II/LA4COEMxcSmrH4ddl/wTYe3RIO0vK2VI9wQy6AxRsJpb3AAALvXb6TxvUCYSdOQo5Mh0GySkJc7rB405GUEfzbbl/iFpPoNQVNUQAZG06nkI6RCABRqRA9IimH6Up5Mhybtu2IlewB2Sf6AmQ4ZU9rfBELvyA23Yub6LWWtUBgK3OB79L7FILLDKWd4wpxmMRAMoLQR1ItLoiWUmhFtjptab7LQDgRARliLITLrcBkHNp9VACUH1UDRQEYGuYxzyM9H0mBccQNnCkQ3Q1UHBaO6sNyw0CelEtBGXKSoE+fJWZh5GupyneMIkCOMESAniMAzMreLvuO+pnmBQSp4C+ELCiMSGVLPh7M023SSBAiAA5yPh2m0wigEbWKnw3qDrrscF00cciCATGwNQRAv2YGvyD4Y36QGhqOS4AcABAA88oGvBCRho5H2+UiW6EfyM1L5l8a56rqdvE6lFakc3ScVDOBNBUoFM8c1vgnhAG5VsAqMD6Q9IwwtAkR39iGEQF1ZBxgU+v9UGL6MBQYiTdJllIBtx5y0rixGdAZ1YysbS53TAVy3vf4aabEpt1T0HoB2Eg4Yv5OKNwyHgmNvPKaQAYLG3EIyIqcL6Fj5C2jhXL9EpCdRMROE5nCW3qm1vfR6wYh0HKGG3wY+JgLkUWQ/WMfI8oMvIWMY7aCncNxxpSmHRUCEzDdSR0+dRwIQaMWW1FE0AOGeKkx0OLwYanBK3qfC0BSmIlozkuFcvSkulckoIB2FbHWu0y9gMHsEapMMEoySNUA2RDrduxIqr5POQV2zZ++IBOwVrFO9THrtjU2uWsCMZjxXl88Hmeaz1rPdAqXyJl68F5RTtdvN1aIyYEAMAWJaCMHvon7s23jljlxoKBEgNv6LQ25/rZIQyOdwDO3jLsqE2nbVAil21LxqFpZ2xJ3CFuE33QCo7kfkfO8kpW6gdioxdzZDLOaMMwidzeKD0RxaD7cnHHsu0jVkW5oTwwMGI0lwwA36u2nMY8AKzErLW9JxFiteyzZsAAxY1vPe5Uf68lIDVjV8JZpPfjxbc/QuyRKdAQJaAdIA4tCTht+kQJ1I4nbdjfHxgpTSLyI19pb/iuK7+9YJaZCxEIKj79YZ6uDU8f97878teRN1FzA7OvquSrVKUgk+S6ROpJfA7GpN6RPkx4voshXgu91p7CGHeA+IY8dUUVXwT7PYw12Xsj0Lfh9X4ac9XgKW86cj8bPh8XmyDOD88FLoB+YPXp4YtyB3gBPXu98xeRI2zploVCBQAAAABJRU5ErkJggg==";
public static final String DEFAULT_SERVER_ICON = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAASFBMVEWwsLBBQUE9PT1JSUlFRUUuLi5MTEyzs7M0NDQ5OTlVVVVQUFAmJia5ubl+fn5zc3PFxcVdXV3AwMCJiYmUlJRmZmbQ0NCjo6OL5p+6AAAFVklEQVRYw+1W67K0KAzkJnIZdRAZ3/9NtzvgXM45dX7st1VbW7XBUVDSdEISRqn/5R+T82/+nsr/XZn/SHm/3x9/ArA/IP8qwPK433d44VubZ/XT6/cJy0L792VZfnDrcRznr86d748u92X5vtaxOe228zcCy+MSMpg/5SwRopsYMv8oigCwngbQhE/rzhwAYMpxnvMvHhgy/8AgByJolzb5pPqEbvtgMBBmtvkbgxKmaaIZ5TyPum6Viue6te241N+s+W6nOlucgjEx6Nay9zZta1XVxejW+Q5ZhhkDS31lgOTegjUBor33CQilbC2GYGy9y9bN8ytevjE4a2stajHDAgAcUkoYwzO6zQi8ZflC+XO0+exiuNa3OQtIJOCk13neUjv7VO7Asu/3LwDFeg37sQtQhy4lAQH6IR9ztca0E3oI5PtDAlJ1tHGplrJ12jjrrXPWYvXsU042Bl/qUr3B9qzPSKaovpvjgglYL2F1x+Zs7gIvpLYuq46wr3H5/RJxyvM6sXOY762oU4YZ3mAz1lpc9O3Y30VJUM/iWhBIib63II/LA4COEMxcSmrH4ddl/wTYe3RIO0vK2VI9wQy6AxRsJpb3AAALvXb6TxvUCYSdOQo5Mh0GySkJc7rB405GUEfzbbl/iFpPoNQVNUQAZG06nkI6RCABRqRA9IimH6Up5Mhybtu2IlewB2Sf6AmQ4ZU9rfBELvyA23Yub6LWWtUBgK3OB79L7FILLDKWd4wpxmMRAMoLQR1ItLoiWUmhFtjptab7LQDgRARliLITLrcBkHNp9VACUH1UDRQEYGuYxzyM9H0mBccQNnCkQ3Q1UHBaO6sNyw0CelEtBGXKSoE+fJWZh5GupyneMIkCOMESAniMAzMreLvuO+pnmBQSp4C+ELCiMSGVLPh7M023SSBAiAA5yPh2m0wigEbWKnw3qDrrscF00cciCATGwNQRAv2YGvyD4Y36QGhqOS4AcABAA88oGvBCRho5H2+UiW6EfyM1L5l8a56rqdvE6lFakc3ScVDOBNBUoFM8c1vgnhAG5VsAqMD6Q9IwwtAkR39iGEQF1ZBxgU+v9UGL6MBQYiTdJllIBtx5y0rixGdAZ1YysbS53TAVy3vf4aabEpt1T0HoB2Eg4Yv5OKNwyHgmNvPKaQAYLG3EIyIqcL6Fj5C2jhXL9EpCdRMROE5nCW3qm1vfR6wYh0HKGG3wY+JgLkUWQ/WMfI8oMvIWMY7aCncNxxpSmHRUCEzDdSR0+dRwIQaMWW1FE0AOGeKkx0OLwYanBK3qfC0BSmIlozkuFcvSkulckoIB2FbHWu0y9gMHsEapMMEoySNUA2RDrduxIqr5POQV2zZ++IBOwVrFO9THrtjU2uWsCMZjxXl88Hmeaz1rPdAqXyJl68F5RTtdvN1aIyYEAMAWJaCMHvon7s23jljlxoKBEgNv6LQ25/rZIQyOdwDO3jLsqE2nbVAil21LxqFpZ2xJ3CFuE33QCo7kfkfO8kpW6gdioxdzZDLOaMMwidzeKD0RxaD7cnHHsu0jVkW5oTwwMGI0lwwA36u2nMY8AKzErLW9JxFiteyzZsAAxY1vPe5Uf68lIDVjV8JZpPfjxbc/QuyRKdAQJaAdIA4tCTht+kQJ1I4nbdjfHxgpTSLyI19pb/iuK7+9YJaZCxEIKj79YZ6uDU8f97878teRN1FzA7OvquSrVKUgk+S6ROpJfA7GpN6RPkx4voshXgu91p7CGHeA+IY8dUUVXwT7PYw12Xsj0Lfh9X4ac9XgKW86cj8bPh8XmyDOD88FLoB+YPXp4YtyB3gBPXu98xeRI2zploVCBQAAAABJRU5ErkJggg==";
private final MojangService mojangService;
private final MetricService metricService;
private final MinecraftServerCacheRepository serverCacheRepository;
private final ServerPreviewCacheRepository serverPreviewCacheRepository;
@Autowired
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository) {
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository,
ServerPreviewCacheRepository serverPreviewCacheRepository) {
this.mojangService = mojangService;
this.metricService = metricService;
this.serverCacheRepository = serverCacheRepository;
this.serverPreviewCacheRepository = serverPreviewCacheRepository;
}
/**
@ -66,11 +72,9 @@ public class ServerService {
String key = "%s-%s:%s".formatted(platformName, hostname, port);
log.info("Getting server: {}:{}", hostname, port);
((TotalServerLookupsMetric) metricService.getMetric(TotalServerLookupsMetric.class)).increment(); // Increment the total server lookups
// Check if the server is cached
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
if (cached.isPresent() && Config.INSTANCE.isProduction()) {
if (cached.isPresent() && AppConfig.isProduction()) {
log.info("Server {}:{} is cached", hostname, port);
return cached.get();
}
@ -102,6 +106,10 @@ public class ServerService {
((JavaMinecraftServer) server.getServer()).setMojangBlocked(mojangService.isServerBlocked(hostname));
}
// Add the server lookup to the unique server lookups metric
((UniqueServerLookupsMetric) metricService.getMetric(UniqueServerLookupsMetric.class))
.addLookup("%s-%s:%s".formatted(platformName, hostname, port));
log.info("Found server: {}:{}", hostname, port);
serverCacheRepository.save(server);
server.getCache().setCached(false);
@ -130,4 +138,39 @@ public class ServerService {
}
return Base64.getDecoder().decode(icon); // Return the decoded favicon
}
/**
* Gets the server list preview image.
*
* @param cachedServer the server to get the preview of
* @param platform the platform of the server
* @param size the size of the preview
* @return the server preview
*/
public byte[] getServerPreview(CachedMinecraftServer cachedServer, String platform, int size) {
if (size > 2048) {
throw new BadRequestException("Size cannot be greater than 2048");
}
if (size < 256) {
throw new BadRequestException("Size cannot be smaller than 256");
}
MinecraftServer server = cachedServer.getServer();
log.info("Getting preview for server: {}:{} (size {})", server.getHostname(), server.getPort(), size);
String key = "%s-%s:%s".formatted(platform, server.getHostname(), server.getPort());
// Check if the server preview is cached
Optional<CachedServerPreview> cached = serverPreviewCacheRepository.findById(key);
if (cached.isPresent() && AppConfig.isProduction()) {
log.info("Server preview for {}:{} is cached", server.getHostname(), server.getPort());
return cached.get().getBytes();
}
long start = System.currentTimeMillis();
byte[] preview = ImageUtils.imageToBytes(ServerPreviewRenderer.INSTANCE.render(server, size));
log.info("Took {}ms to render preview for server: {}:{}", System.currentTimeMillis() - start, server.getHostname(), server.getPort());
CachedServerPreview serverPreview = new CachedServerPreview(key, preview);
serverPreviewCacheRepository.save(serverPreview);
return preview;
}
}

View File

@ -7,11 +7,12 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
@AllArgsConstructor
@Getter @Setter @ToString
@RedisHash(value = "metric")
@Document("metrics")
public abstract class Metric<T> {
/**
* The id of the metric.
@ -27,8 +28,8 @@ public abstract class Metric<T> {
* Should this metric be collected
* before pushing to Influx?
*/
@JsonIgnore
private transient boolean collector;
@Transient @JsonIgnore
private boolean collector;
/**
* Collects the metric.

View File

@ -1,7 +1,7 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
import xyz.mcutils.backend.websocket.MetricsWebSocketHandler;
import xyz.mcutils.backend.websocket.WebSocketManager;
public class ConnectedSocketsMetric extends IntegerMetric {
@ -16,6 +16,6 @@ public class ConnectedSocketsMetric extends IntegerMetric {
@Override
public void collect() {
setValue(MetricsWebSocketHandler.SESSIONS.size());
setValue(WebSocketManager.getTotalConnections());
}
}

View File

@ -1,10 +0,0 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
public class TotalPlayerLookupsMetric extends IntegerMetric {
public TotalPlayerLookupsMetric() {
super("total_player_lookups");
}
}

View File

@ -1,10 +0,0 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
public class TotalServerLookupsMetric extends IntegerMetric {
public TotalServerLookupsMetric() {
super("total_server_lookups");
}
}

View File

@ -0,0 +1,36 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class UniquePlayerLookupsMetric extends IntegerMetric {
private List<String> uniqueLookups = new ArrayList<>();
public UniquePlayerLookupsMetric() {
super("unique_player_lookups");
}
@Override
public boolean isCollector() {
return true;
}
/**
* Adds a lookup to the list of unique lookups.
*
* @param uuid the query that was used to look up a player
*/
public void addLookup(UUID uuid) {
if (!uniqueLookups.contains(uuid.toString())) {
uniqueLookups.add(uuid.toString());
}
}
@Override
public void collect() {
setValue(uniqueLookups.size());
}
}

View File

@ -0,0 +1,36 @@
package xyz.mcutils.backend.service.metric.metrics;
import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
import java.util.ArrayList;
import java.util.List;
public class UniqueServerLookupsMetric extends IntegerMetric {
private List<String> uniqueLookups = new ArrayList<>();
public UniqueServerLookupsMetric() {
super("unique_server_lookups");
}
@Override
public boolean isCollector() {
return true;
}
/**
* Adds a lookup to the list of unique lookups.
*
* @param hostname the query that was used to look up a player
*/
public void addLookup(String hostname) {
hostname = hostname.toLowerCase();
if (!uniqueLookups.contains(hostname)) {
uniqueLookups.add(hostname);
}
}
@Override
public void collect() {
setValue(uniqueLookups.size());
}
}

View File

@ -7,10 +7,15 @@ import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.server.BedrockMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.service.MaxMindService;
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
import java.io.IOException;
import java.net.*;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
/**
* The {@link MinecraftServerPinger} for pinging
@ -50,19 +55,19 @@ public final class BedrockMinecraftServerPinger implements MinecraftServerPinger
unconnectedPong.process(socket);
String response = unconnectedPong.getResponse();
if (response == null) { // No pong response
throw new ResourceNotFoundException("Server didn't respond to ping");
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
}
return BedrockMinecraftServer.create(hostname, ip, port, records, response); // Return the server
return BedrockMinecraftServer.create(hostname, ip, port, records,
MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(ip)), response); // Return the server
} catch (IOException ex ) {
if (ex instanceof UnknownHostException) {
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
throw new BadRequestException("Unknown hostname '%s'".formatted(hostname));
} else if (ex instanceof SocketTimeoutException) {
throw new ResourceNotFoundException(ex);
} else if (ex instanceof SocketException) {
throw new BadRequestException("An error occurred pinging %s:%s".formatted(hostname, port));
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
} else {
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
throw new BadRequestException("An error occurred pinging '%s:%s'".formatted(hostname, port));
}
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
}
return null;
}
}

View File

@ -3,14 +3,15 @@ package xyz.mcutils.backend.service.pinger.impl;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.JavaMinecraftVersion;
import xyz.mcutils.backend.common.ServerUtils;
import xyz.mcutils.backend.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
import xyz.mcutils.backend.common.packet.impl.java.JavaPacketStatusInStart;
import xyz.mcutils.backend.exception.impl.BadRequestException;
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.model.token.JavaServerStatusToken;
import xyz.mcutils.backend.service.MaxMindService;
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
import java.io.DataInputStream;
@ -25,6 +26,13 @@ import java.net.*;
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
private static final int TIMEOUT = 1500; // The timeout for the socket
/**
* Ping the server with the given hostname and port.
*
* @param hostname the hostname of the server
* @param port the port of the server
* @return the server that was pinged
*/
@Override
public JavaMinecraftServer ping(String hostname, String ip, int port, DNSRecord[] records) {
log.info("Pinging {}:{}...", hostname, port);
@ -44,16 +52,18 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream);
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
return JavaMinecraftServer.create(hostname, ip, port, records, token);
return JavaMinecraftServer.create(hostname, ip, port, records,
MinecraftServer.GeoLocation.fromMaxMind(MaxMindService.lookup(ip)), token);
}
} catch (IOException ex) {
if (ex instanceof UnknownHostException) {
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
} else if (ex instanceof ConnectException || ex instanceof SocketTimeoutException) {
throw new ResourceNotFoundException(ex);
throw new ResourceNotFoundException("Server '%s' didn't respond to ping".formatted(hostname));
} else {
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
throw new BadRequestException("An error occurred pinging '%s:%s'".formatted(hostname, port));
}
log.error("An error occurred pinging %s".formatted(ServerUtils.getAddress(hostname, port)), ex);
}
return null;
}
}

View File

@ -1,71 +0,0 @@
package xyz.mcutils.backend.websocket;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.Timer;
import xyz.mcutils.backend.model.metric.WebsocketMetrics;
import xyz.mcutils.backend.service.MetricService;
import xyz.mcutils.backend.service.metric.metrics.TotalPlayerLookupsMetric;
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
import xyz.mcutils.backend.service.metric.metrics.TotalServerLookupsMetric;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Log4j2(topic = "WebSocket/Metrics")
public class MetricsWebSocketHandler extends TextWebSocketHandler {
private final long interval = TimeUnit.SECONDS.toMillis(5);
public static final List<WebSocketSession> SESSIONS = new ArrayList<>();
private final MetricService metricService;
public MetricsWebSocketHandler(MetricService metricService) {
this.metricService = metricService;
Timer.scheduleRepeating(() -> {
for (WebSocketSession session : SESSIONS) {
sendMetrics(session);
}
}, interval, interval);
}
/**
* Sends the metrics to the client.
*
* @param session the session to send the metrics to
*/
private void sendMetrics(WebSocketSession session) {
try {
WebsocketMetrics metrics = new WebsocketMetrics(Map.of(
"totalRequests", metricService.getMetric(TotalRequestsMetric.class).getValue(),
"totalServerLookups", metricService.getMetric(TotalServerLookupsMetric.class).getValue(),
"totalPlayerLookups", metricService.getMetric(TotalPlayerLookupsMetric.class).getValue()
));
session.sendMessage(new TextMessage(Main.GSON.toJson(metrics)));
} catch (Exception e) {
log.error("An error occurred while sending metrics to the client", e);
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
log.info("WebSocket connection established with session id: {}", session.getId());
sendMetrics(session);
SESSIONS.add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, @NotNull CloseStatus status) {
log.info("WebSocket connection closed with session id: {}", session.getId());
SESSIONS.remove(session);
}
}

View File

@ -0,0 +1,62 @@
package xyz.mcutils.backend.websocket;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor @Getter @Log4j2(topic = "WebSocket")
public abstract class WebSocket extends TextWebSocketHandler {
/**
* The sessions that are connected to the WebSocket.
*/
private final List<WebSocketSession> sessions = new ArrayList<>();
/**
* The path of the WebSocket.
* <p>
* Example: /websocket/metrics
* </p>
*/
public final String path;
/**
* Sends a message to the client.
*
* @param session the session to send the message to
* @param message the message to send
* @throws IOException if an error occurs while sending the message
*/
public void sendMessage(WebSocketSession session, String message) throws IOException {
session.sendMessage(new TextMessage(message));
}
/**
* Called when a session connects to the WebSocket.
*
* @param session the session that connected
*/
abstract public void onSessionConnect(WebSocketSession session);
@Override
public final void afterConnectionEstablished(@NotNull WebSocketSession session) {
this.sessions.add(session);
log.info("Connection established: {}", session.getId());
this.onSessionConnect(session);
}
@Override
public final void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus status) {
this.sessions.remove(session);
log.info("Connection closed: {}", session.getId());
}
}

View File

@ -0,0 +1,51 @@
package xyz.mcutils.backend.websocket;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import xyz.mcutils.backend.service.MetricService;
import xyz.mcutils.backend.websocket.impl.MetricsWebSocket;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableWebSocket
public class WebSocketManager implements WebSocketConfigurer {
private static final List<WebSocket> WEB_SOCKETS = new ArrayList<>();
private final MetricService metricService;
@Autowired
public WebSocketManager(MetricService metricService) {
this.metricService = metricService;
}
@Override
public void registerWebSocketHandlers(@NotNull WebSocketHandlerRegistry registry) {
registerWebSocket(registry, new MetricsWebSocket(metricService));
}
/**
* Registers a WebSocket.
*
* @param registry the registry to register the WebSocket on
* @param webSocket the WebSocket to register
*/
private void registerWebSocket(WebSocketHandlerRegistry registry, WebSocket webSocket) {
registry.addHandler(webSocket, webSocket.getPath()).setAllowedOrigins("*");
WEB_SOCKETS.add(webSocket);
}
/**
* Gets the total amount of connections.
*
* @return the total amount of connections
*/
public static int getTotalConnections() {
return WEB_SOCKETS.stream().mapToInt(webSocket -> webSocket.getSessions().size()).sum();
}
}

View File

@ -0,0 +1,53 @@
package xyz.mcutils.backend.websocket.impl;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.socket.WebSocketSession;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.Timer;
import xyz.mcutils.backend.service.MetricService;
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
import xyz.mcutils.backend.service.metric.metrics.UniquePlayerLookupsMetric;
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
import xyz.mcutils.backend.websocket.WebSocket;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Log4j2(topic = "WebSocket/Metrics")
public class MetricsWebSocket extends WebSocket {
private final long interval = TimeUnit.SECONDS.toMillis(5);
private final MetricService metricService;
public MetricsWebSocket(MetricService metricService) {
super("/websocket/metrics");
this.metricService = metricService;
Timer.scheduleRepeating(() -> {
for (WebSocketSession session : this.getSessions()) {
sendMetrics(session);
}
}, interval, interval);
}
@Override
public void onSessionConnect(WebSocketSession session) {
sendMetrics(session);
}
/**
* Sends the metrics to the client.
*
* @param session the session to send the metrics to
*/
private void sendMetrics(WebSocketSession session) {
try {
this.sendMessage(session, Main.GSON.toJson(Map.of(
"totalRequests", metricService.getMetric(TotalRequestsMetric.class).getValue(),
"uniqueServerLookups", metricService.getMetric(UniqueServerLookupsMetric.class).getValue(),
"uniquePlayerLookups", metricService.getMetric(UniquePlayerLookupsMetric.class).getValue()
)));
} catch (Exception e) {
log.error("An error occurred while sending metrics to the client", e);
}
}
}

View File

@ -17,17 +17,23 @@ spring:
database: 1
auth: "" # Leave blank for no auth
# Disable default metrics
management:
endpoints:
web:
exposure:
exclude:
- "*"
influx:
metrics:
export:
enabled: false
# MongoDB - This is used for general data storage
mongodb:
uri: mongodb://localhost:27017
database: test
port: 27017
# Sentry Configuration
sentry:
dsn: ""
# The URL of the API
public-url: http://localhost
# MaxMind Configuration
# This is used for IP Geolocation
maxmind:
license: ""
# InfluxDB Configuration
influx:
@ -36,4 +42,22 @@ influx:
org: org
bucket: bucket
public-url: http://localhost
management:
# Disable all actuator endpoints
endpoints:
web:
exposure:
exclude:
- "*"
# Disable default metrics
influx:
metrics:
export:
enabled: false
# Set the embedded MongoDB version
de:
flapdoodle:
mongodb:
embedded:
version: 7.0.8

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

View File

@ -1,4 +1,4 @@
package cc.fascinated.config;
package xyz.mcutils.backend.test.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@ -16,7 +16,8 @@ import java.io.IOException;
*/
@TestConfiguration
public class TestRedisConfig {
@NonNull private final RedisServer server;
@NonNull
private final RedisServer server;
public TestRedisConfig() throws IOException {
server = new RedisServer(); // Construct the mock server

View File

@ -1,18 +1,18 @@
package xyz.mcutils.backend.tests;
package xyz.mcutils.backend.test.tests;
import cc.fascinated.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import xyz.mcutils.backend.test.config.TestRedisConfig;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(classes = { TestRedisConfig.class })
@AutoConfigureMockMvc
@SpringBootTest(classes = TestRedisConfig.class)
class MojangControllerTests {
@Autowired

View File

@ -1,20 +1,22 @@
package xyz.mcutils.backend.tests;
package xyz.mcutils.backend.test.tests;
import cc.fascinated.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.test.config.TestRedisConfig;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(classes = { TestRedisConfig.class })
@AutoConfigureMockMvc
@SpringBootTest(classes = TestRedisConfig.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PlayerControllerTests {
private final String testPlayerUuid = "eeab5f8a-18dd-4d58-af78-2b3c4543da48";

View File

@ -1,19 +1,21 @@
package xyz.mcutils.backend.tests;
package xyz.mcutils.backend.test.tests;
import cc.fascinated.config.TestRedisConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import xyz.mcutils.backend.test.config.TestRedisConfig;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(classes = { TestRedisConfig.class })
@AutoConfigureMockMvc
@SpringBootTest(classes = TestRedisConfig.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ServerControllerTests {
private final String testServer = "play.hypixel.net";
@ -54,4 +56,11 @@ class ServerControllerTests {
.andExpect(status().isOk())
.andExpect(jsonPath("$.blocked").value(false));
}
@Test
public void ensureServerPreviewLookupSuccess() throws Exception {
mockMvc.perform(get("/server/java/preview/" + testServer)
.contentType(MediaType.IMAGE_PNG))
.andExpect(status().isOk());
}
}