Compare commits

..

307 Commits

Author SHA1 Message Date
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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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 2024-07-30 22:06:01 +01:00
82fb2a3d23 Merge remote-tracking branch 'origin/master' 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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#21
2024-07-30 20:52:12 +00:00
b7834ab389 change how the mojang server status' are fetched 2024-07-30 21:04:37 +01:00
bb651bd88b Merge remote-tracking branch 'origin/master' 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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#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 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
Reviewed-on: MinecraftUtilities/Backend#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 2024-04-21 23:42:33 +01:00
67efda71d2 don't return maxmind data if it failed to load 2024-04-21 23:33:40 +01:00
2ad5556041 add check if licence is in the config for maxmind 2024-04-21 23:30:58 +01:00
3faf2d3319 add location to the server response 2024-04-21 23:27:44 +01:00
bf992713dc switch to springboot sentry 2024-04-21 18:50:57 +01:00
23e240fce1 fix sentry depend 2024-04-21 18:44:44 +01:00
cca45057f0 impl sentry 2024-04-21 18:41:53 +01:00
beda7fa230 fix websocket metrics 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 2024-04-21 03:08:15 +01:00
f85ed49545 fix italics 2024-04-20 21:17:25 +01:00
69833bf560 add italics to the server preview renderer 2024-04-20 21:14:00 +01:00
6693fc6793 work pls 2024-04-20 21:01:18 +01:00
a6ea3ab143 work pls 2024-04-20 20:49:23 +01:00
f96e5d5426 work pls 2024-04-20 20:47:29 +01:00
e360ad4446 work pls 2024-04-20 20:43:45 +01:00
c913816447 work pls 2024-04-20 20:41:24 +01:00
5dccce9fc5 maybe font man will show up at my door 2024-04-20 20:39:39 +01:00
3cd5e32118 maybe font man will show up at my door 2024-04-20 20:38:05 +01:00
5ca707bef1 update maven - might fix, who knows 2024-04-20 20:34:00 +01:00
6fc02cd906 pls fix it 2024-04-20 20:31:02 +01:00
f4a9d7c31c pls fix it 2024-04-20 20:26:31 +01:00
7127794152 install fontconfig - docker 2024-04-20 20:24:46 +01:00
f664406299 install fontconfig - docker 2024-04-20 20:23:04 +01:00
c5b5b3b105 pls work 2024-04-20 20:10:41 +01:00
b5fa470801 fix preview tests 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 2024-04-20 19:58:50 +01:00
92fe2b28e3 move package 2024-04-20 19:54:38 +01:00
eae027af84 add server preview tests 2024-04-20 19:45:54 +01:00
d2ae4b4cc5 add server preview renderer 2024-04-20 19:37:58 +01:00
ff58b1756a is this even used anymore? 2024-04-20 14:31:49 +01:00
543aff2a04 send metrics to client when they connect 2024-04-20 14:23:43 +01:00
b666e5a8b7 Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend 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 2024-04-19 23:24:51 +01:00
e5935c6696 fix metric saving for running in tests 2024-04-19 22:42:32 +01:00
5871c64582 cleanup application.yml 2024-04-19 22:26:17 +01:00
1cfcce4806 etags attempt #2 2024-04-19 21:00:08 +01:00
46d4a53b11 fix cache control, oops 2024-04-19 20:51:33 +01:00
d0cfd03ad9 impl etags 2024-04-19 20:46:30 +01:00
4dc263961d add cache control to endpoints 2024-04-19 20:33:12 +01:00
8a8c6b542a add debug 2024-04-19 18:10:25 +01:00
5b8017e403 add debug 2024-04-19 18:06:04 +01:00
ad83e270b6 maybe fix 2024-04-19 18:00:22 +01:00
aa69970ec7 woop! fix timeouts and use hostnames for mojang api and session server 2024-04-19 17:52:16 +01:00
7ecaf8c580 lower timeout 2024-04-19 17:31:20 +01:00
8a985b52b8 update mojang endpoint to include the name of the service 2024-04-19 17:21:10 +01:00
03c679d25c pls work part 2 2024-04-18 23:49:09 +01:00
6096764905 Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend 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
Reviewed-on: MinecraftUtilities/Backend#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 2024-04-18 20:13:23 +00:00
Lee
bee6f2d52d Merge pull request 'Configure Renovate' (#6) from renovate/configure into master
Reviewed-on: MinecraftUtilities/Backend#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 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
Reviewed-on: MinecraftUtilities/Backend#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 2024-04-18 17:38:14 +01:00
547fa075f3 fix hostname stored 2024-04-18 17:10:23 +01:00
8dcde443ee store hostname instead of query for unique server lookup total 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 2024-04-18 16:47:48 +01:00
cb9181010a maybe 2024-04-18 16:38:08 +01:00
046df7fd1f maybe 2024-04-18 16:28:09 +01:00
3ac4bfe2ee maybe 2024-04-18 15:47:05 +01:00
f037f3f9e7 pls work 2024-04-18 14:26:25 +01:00
04b99715c1 pls work 2024-04-18 14:25:12 +01:00
4e5258d74c pls work 2024-04-18 14:22:10 +01:00
6e336bb879 wow 2024-04-18 14:20:55 +01:00
daf3770b73 pls work 2024-04-18 14:07:18 +01:00
02eb9e2a2e pls work 2024-04-18 14:05:42 +01:00
aa87d2f374 pls work 2024-04-18 14:03:52 +01:00
1eb540380e pls work 2024-04-18 13:58:28 +01:00
b0bdf6e800 add version to embedded mongo for tests 2024-04-18 13:56:31 +01:00
597c3850e3 fix tests 2024-04-18 13:55:02 +01:00
ce3067ee0e switch to mongo for metric storage 2024-04-18 13:34:43 +01:00
1c685ca414 switch to mongo for metric storage 2024-04-18 13:31:22 +01:00
c91a4afdf9 oopise, actually register unique server requests metric 2024-04-18 13:19:49 +01:00
4fd66dffd3 switch to unique player and server lookups not the total 2024-04-18 13:17:41 +01:00
b93c7f68fb Merge branch 'master' of https://git.fascinated.cc/MinecraftUtilities/Backend 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
Reviewed-on: MinecraftUtilities/Backend#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 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
Reviewed-on: MinecraftUtilities/Backend#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
Reviewed-on: MinecraftUtilities/Backend#2
2024-04-18 08:34:14 +00:00
036d8439ba Add renovate.json 2024-04-18 08:33:01 +00:00
0228f7205d add connected sockets metric 2024-04-18 03:06:39 +01:00
c46443425e add total player and server lookups 2024-04-18 02:26:02 +01:00
9fd84f2e5b send metrics when connecting to the websocket 2024-04-18 01:04:25 +01:00
c5758d38e0 add metrics websocket 2024-04-18 00:25:44 +01:00
cbfaa867a9 fix bedrock invalid server pinging 2024-04-17 20:51:48 +01:00
5fbab2c0ec update website title 2024-04-17 18:50:47 +01:00
3d17798f30 fix cpu metric 2024-04-17 16:37:55 +01:00
a47db6d843 fix tests 2024-04-17 16:37:24 +01:00
ecca157d86 change api format 2024-04-17 16:35:52 +01:00
632d33197d add cpu metric 2024-04-17 01:35:15 +01:00
8f758820e1 update memory metric 2024-04-17 01:02:06 +01:00
d8d4f32006 ah shit, forgot to load the metrics after messing around 2024-04-17 00:59:36 +01:00
7a50dccabd add memory metric 2024-04-17 00:55:41 +01:00
be40a981fe bulk write influx points 2024-04-17 00:04:14 +01:00
08be02ff73 revert extra formatting (fuck html) 2024-04-16 21:14:19 +01:00
37b048c2a9 pls work 2024-04-16 21:01:05 +01:00
b7b788847d support more formatting 2024-04-16 20:58:03 +01:00
924e045a18 support more formatting 2024-04-16 20:53:27 +01:00
bedde3b0eb line to html now workie 2024-04-16 20:47:50 +01:00
45fb517385 maybe bold for html? 2024-04-16 20:44:37 +01:00
5c77b59b90 maybe bold for html? 2024-04-16 20:40:09 +01:00
65aa5b102b maybe bold for html? 2024-04-16 20:36:24 +01:00
2be48c7c30 maybe bold for html? 2024-04-16 20:32:48 +01:00
375a8cc2e6 maybe fix html spacing 2024-04-16 20:23:27 +01:00
2e5c8b1ee0 fix html motd 2024-04-14 21:50:44 +01:00
f3b1104e93 fix html motd 2024-04-14 21:47:25 +01:00
1d597d5d92 fix html motd 2024-04-14 21:39:55 +01:00
5877b100bc update example player 2024-04-14 17:32:23 +01:00
3b946132af remove debug 2024-04-14 17:25:08 +01:00
9453e91892 fix bug with bedrock servers not being parsed properly 2024-04-14 17:23:33 +01:00
a6d06e6873 maybe make building faster in the ci? 2024-04-14 17:13:12 +01:00
2b35fd51b0 add bedrock example for the main page 2024-04-14 17:10:40 +01:00
72ac874b9f update index.html 2024-04-14 17:06:05 +01:00
fa0189c421 don't log so much when saving and writing metrics 2024-04-14 16:57:07 +01:00
cf618b3123 plain html is actually so ugly 2024-04-14 15:56:41 +01:00
1675fe16d2 add favicon to the html 2024-04-14 15:55:04 +01:00
15d9dcbd5d make links clickable and add robots.txt file 2024-04-14 15:49:00 +01:00
1a518b3d00 update the website 2024-04-14 15:43:14 +01:00
1de25a92ba seo stuff 2024-04-14 15:27:13 +01:00
4d0ae5286d add health endpoint 2024-04-14 12:44:05 +01:00
1408cecee3 fix influx 2024-04-14 10:10:03 +01:00
13964e0f65 cleanup 2024-04-14 10:01:35 +01:00
6cea5a1f29 change metric save interval 2024-04-14 10:00:38 +01:00
05d0aa500c fix map metric 2024-04-14 09:48:59 +01:00
8d45ba8cbd fix map metric 2024-04-14 09:42:39 +01:00
e9da32775f add basic metrics impl 2024-04-14 09:34:10 +01:00
fc640fe1a0 update src code url 2024-04-13 21:10:43 +01:00
e067d399c8 remove error page template 2024-04-13 21:08:03 +01:00
69321adb1f fix tests 2024-04-13 21:02:54 +01:00
595ca5b36a Merge branch 'master' of https://git.fascinated.cc/Fascinated/minecraft-helper 2024-04-13 20:58:51 +01:00
88c940431b rename the package 2024-04-13 20:53:31 +01:00
a15326a847 rename the package 2024-04-13 20:44:05 +01:00
Lee
941ffaf6ae Update README.md 2024-04-13 18:34:32 +00:00
b14c969013 update dns records field for minecraft server response 2024-04-13 18:33:41 +01:00
c72bda317e update dns records field for minecraft server response 2024-04-13 18:27:53 +01:00
499fcb6fa5 cleanup cache 2024-04-13 18:15:03 +01:00
edb02c2ba1 cleanup 2024-04-13 18:02:46 +01:00
a1755948c1 cleanup 2024-04-13 17:55:03 +01:00
15885f7e00 cleanup imports 2024-04-13 17:40:28 +01:00
1a0dd8844d fix index and /mojang/status routes 2024-04-13 17:40:03 +01:00
77f787b659 don't return username to uuid id 2024-04-13 17:30:08 +01:00
a966977d82 fix username to uuid cache 2024-04-13 17:29:32 +01:00
bda70b19a8 cleanup imports 2024-04-13 17:21:49 +01:00
e5e3503abc fix tests 2024-04-13 17:21:33 +01:00
b5e8664ad3 fix tests 2024-04-13 17:18:43 +01:00
8216ec7943 clean up cache info 2024-04-13 17:17:13 +01:00
c198339acc put player in a player object in the return json and update the cache information in json responses 2024-04-13 17:10:40 +01:00
2895525412 degraded status if response time is more than 1 second 2024-04-13 16:50:17 +01:00
348edfd1ef add a status instead of a boolean online / offline for the endpoint status 2024-04-13 16:46:45 +01:00
66f5660274 only allow GET on /mojang/status 2024-04-13 16:42:21 +01:00
55c2c95269 only fetch the head on the endpoint status check 2024-04-13 16:30:22 +01:00
77bd9a7c7e cleanup 2024-04-13 16:27:19 +01:00
0b187a852c check the mojang api statuses in parallel 2024-04-13 16:24:49 +01:00
4d4e8557d8 remove unnecessary skin fallback 2024-04-13 16:09:01 +01:00
b708191267 add more endpoints to check the status for 2024-04-13 15:59:44 +01:00
3cf16bd2eb add fallback skins 2024-04-13 15:54:32 +01:00
852f5a8bea fix WebRequest (again) 2024-04-13 15:42:30 +01:00
57601acbb1 cleanup WebRequest 2024-04-13 15:41:08 +01:00
471c3e6e80 fix bug with calling apis 2024-04-13 15:36:20 +01:00
811ea348cf add mojang api status endpoint 2024-04-13 15:34:19 +01:00
0c8f769ee7 cleanup imports 2024-04-13 14:08:12 +01:00
586539d810 move cors to the Config 2024-04-13 14:07:54 +01:00
52d89a6d9f change the cache for player name to uuid to be 6 hours 2024-04-13 14:06:48 +01:00
5381a2887e update blocked servers hourly 2024-04-13 14:03:08 +01:00
4ababb0cb5 remove target dir 2024-04-12 20:17:24 +01:00
4f4a06a4fb update a log 2024-04-12 20:15:17 +01:00
53c50b3a05 don't log as much for request logging 2024-04-12 20:07:25 +01:00
f1dae95a4c fix cache hash for player skin part 2024-04-12 20:05:00 +01:00
f4cf93cf08 fix slim skin models not being recognised 2024-04-12 19:59:59 +01:00
157bdf5e5a cleanup and add player skin format link 2024-04-12 19:56:57 +01:00
4b672de85d add minimum size to player images 2024-04-12 19:52:38 +01:00
55c1ca4139 add skin overlays to all images if it's enabled 2024-04-12 19:50:36 +01:00
4e08955ab9 cleanup 2024-04-12 18:56:25 +01:00
e788ae003f fix skin part tests 2024-04-12 18:50:17 +01:00
a1ad295e0c fix skin part tests 2024-04-12 18:48:13 +01:00
2ea58d8080 make the skin renderer less bad (thanks bray) 2024-04-12 18:46:54 +01:00
83a95fb26c cleanup head renderer 2024-04-11 08:21:53 +01:00
9debdece9e cleanup isometric head 2024-04-11 08:11:21 +01:00
9acb7c259a cleanup isometric head 2024-04-11 08:08:57 +01:00
d9e6becebb fix overlay on player head 2024-04-11 07:45:16 +01:00
654037c8e1 lower size limit 2024-04-11 07:15:12 +01:00
977a1dcbc4 add size limit to player images 2024-04-11 07:14:13 +01:00
bfbaf34b24 fix legacy skins 2024-04-11 07:07:49 +01:00
c8b6f2aad8 implement overlays for skins (skin layers) 2024-04-11 06:54:06 +01:00
9b7b761ffd add working skin layers and rename param for overlay 2024-04-11 06:33:51 +01:00
f63d1cc3ec add head endpoint and finish the body renderer 2024-04-11 06:10:37 +01:00
1a74b0099b rename FlatRenderer to SquareRenderer 2024-04-11 03:58:51 +01:00
4f26110405 add trimmed uuid to player response 2024-04-11 03:57:46 +01:00
6f840654e9 update some logs 2024-04-11 03:54:39 +01:00
e9fa275002 cleanup isometric skin renderer 2024-04-11 03:51:21 +01:00
b682153ebb cleanup isometric skin renderer 2024-04-11 03:48:54 +01:00
a16fda1b53 fix username lookup test 2024-04-11 03:11:09 +01:00
8e5adf337a add isometric head renderer 2024-04-11 03:08:17 +01:00
557c0facb7 cleanup username to uuid endpoint 2024-04-11 00:50:57 +01:00
a11a90f530 cleanup username to uuid endpoint 2024-04-11 00:49:16 +01:00
f2e8360567 allow cors from any domain 2024-04-11 00:33:10 +01:00
944000ab1a fix username to uuid lookup tests 2024-04-11 00:28:54 +01:00
bd09539732 add more username to uuid lookup tests 2024-04-11 00:26:38 +01:00
4a9149e41e add username to uuid lookup test 2024-04-11 00:24:03 +01:00
a3b9cb5e77 fix server example and add an endpoint to get uuid from username 2024-04-11 00:21:36 +01:00
624dcc0be6 add raw profile properties to the player endpoint 2024-04-11 00:15:12 +01:00
0ea69f86f9 add dns records to the server response 2024-04-10 16:21:07 +01:00
d156d2cb3b cleanup tests 2024-04-10 14:44:05 +01:00
8f1bc67596 fix tests 2024-04-10 14:41:26 +01:00
de338fed82 add blocked server test 2024-04-10 14:39:12 +01:00
9d846dec1d changed blocked key blocked server on blocked server endpoint 2024-04-10 14:37:39 +01:00
c9a4e2d4ea add missing fields in ForgeData 2024-04-10 14:35:16 +01:00
b26d5aa67f support 1.7 servers 2024-04-10 14:32:19 +01:00
795c97401c add more data to the java server pinger 2024-04-10 14:24:55 +01:00
cd3738a2b9 add legacy forge mod and new forge mod support 2024-04-10 14:03:35 +01:00
63a3587586 add mods and plugins to server response 2024-04-10 13:24:56 +01:00
28cd7f192d add skin part caching 2024-04-10 12:41:35 +01:00
3790d4a312 change how the port is fetched from the hostname and lowered the timeout for server pings 2024-04-10 12:26:47 +01:00
7855b5bcca yes! 2024-04-10 12:17:27 +01:00
b1bcaad8dc use originalId on PlayerService#usernameToUuid 2024-04-10 12:16:24 +01:00
2ff9122a99 fix 2024-04-10 12:14:58 +01:00
cee9cfe01c fix 2024-04-10 12:13:04 +01:00
0b8c3cbc83 fix depends 2024-04-10 12:11:23 +01:00
48c9b66fd1 fix depends 2024-04-10 12:09:58 +01:00
11079ea572 update hostnameAndPort to be hostname 2024-04-10 12:08:12 +01:00
2ba9651161 add bedrock server support 2024-04-10 11:55:58 +01:00
5ad2f438d1 add server banned status and blocked endpoint 2024-04-10 11:39:17 +01:00
23bcb1d76e fix produces for server icon endpoint 2024-04-10 11:09:39 +01:00
c8c4c8ad3e cleanup swagger docs 2024-04-10 11:09:09 +01:00
78c3333038 add ServerController tests 2024-04-10 11:06:28 +01:00
146cc30413 update download image docs 2024-04-10 10:54:06 +01:00
e26a34fb74 fix Dockerfile 2024-04-10 10:51:16 +01:00
c0c4e32809 use version from the pom 2024-04-10 10:48:15 +01:00
cb7c2e162f add an option to download the image instead of viewing it 2024-04-10 10:45:17 +01:00
ac8b2e4b74 fix index.html 2024-04-10 10:40:00 +01:00
50d4b2df86 add more logging to the PlayerService and ServerService 2024-04-10 10:36:31 +01:00
50ae9b47be cleanup 2024-04-10 10:26:41 +01:00
3bd0ea3838 add better swagger docs with examples 2024-04-10 10:26:24 +01:00
708ccc294d fix wrong url being used for swagger 2024-04-10 10:09:34 +01:00
67ada952ca update ci 2024-04-10 09:56:14 +01:00
3e2bcbe922 add readme and update ci 2024-04-10 09:53:58 +01:00
8990a6308a add swagger 2024-04-10 09:51:31 +01:00
d959169f0b remove server debug 2024-04-10 09:32:56 +01:00
2e58d9c925 add mojang api rate limit error 2024-04-10 09:30:35 +01:00
330c3efc78 add more to the server response 2024-04-10 09:19:02 +01:00
c7fe26ef8f add server example to main page 2024-04-10 08:34:43 +01:00
d525d343e4 fix indicate that the server was not in the cache 2024-04-10 08:28:25 +01:00
b011fdbede indicate that the server and player was not in the cache 2024-04-10 08:24:25 +01:00
3a3a2e223f don't return the cache id for servers 2024-04-10 08:21:52 +01:00
2f09b55f33 update ci 2024-04-10 08:20:23 +01:00
1255cbbcd4 fix ci 2024-04-10 08:16:51 +01:00
f3522287fc fix test 2024-04-10 08:13:50 +01:00
fcb8ef0357 add server pinger 2024-04-10 07:43:38 +01:00
25c69e11e1 add server pinger 2024-04-10 07:43:13 +01:00
Lee
ed3b7e3064 Merge pull request 'Add very basic Java MC server impl' (#1) from Rainnny/minecraft-helper:master into master
Reviewed-on: Fascinated/minecraft-helper#1
2024-04-09 05:52:00 +00:00
147 changed files with 5807 additions and 1070 deletions

@ -1,32 +1,49 @@
name: "ci" name: Deploy App
on: on:
push: push:
branches: branches: ["master"]
- master paths-ignore:
- .gitignore
- README.md
- LICENSE
- docker-compose.yml
jobs: jobs:
deploy: docker:
runs-on: ubuntu-latest strategy:
matrix:
arch: ["ubuntu-latest"]
git-version: ["2.44.0"]
java-version: ["17"]
maven-version: ["3.8.5"]
runs-on: ${{ matrix.arch }}
# Steps to run
steps: steps:
- name: Cloning repo # Checkout the repo
- name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up JDK 17 and Maven # Setup Java and Maven
uses: s4u/setup-maven-action@v1.7.0 - name: Set up JDK and Maven
uses: s4u/setup-maven-action@v1.14.0
with: with:
java-version: '17' java-version: ${{ matrix.java-version }}
distribution: 'temurin' distribution: "zulu"
maven-version: 3.8.5 maven-version: ${{ matrix.maven-version }}
# Run JUnit Tests
- name: Run Tests - name: Run Tests
run: mvn --batch-mode test -q run: mvn --batch-mode test -q
- name: Cloning repo # Re-checkout to reset the FS before deploying to Dokku
- name: Checkout - Reset FS
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# Deploy to Dokku
- name: Push to dokku - name: Push to dokku
uses: dokku/github-action@master uses: dokku/github-action@master
with: with:

4
.gitignore vendored

@ -28,3 +28,7 @@ fabric.properties
git.properties git.properties
pom.xml.versionsBackup pom.xml.versionsBackup
application.yml application.yml
target/
### MaxMind GeoIP2
data/

@ -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 # Set the working directory
WORKDIR /home/container WORKDIR /home/container
@ -7,11 +11,14 @@ WORKDIR /home/container
COPY . . COPY . .
# Build the jar # Build the jar
RUN mvn package -q RUN mvn package -q -Dmaven.test.skip -DskipTests -T2C
# Make port 80 available to the world outside this container # Make port 80 available to the world outside this container
EXPOSE 80 EXPOSE 80
ENV PORT=80 ENV PORT=80
# Indicate that we're running in production
ENV ENVIRONMENT=production
# Run the jar file # Run the jar file
CMD ["java", "-jar", "target/Minecraft-Helper-1.0-SNAPSHOT.jar"] CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true

21
LICENSE Normal file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024, Fascinated
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file

@ -0,0 +1,3 @@
# Minecraft Utilities - Backend
See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.

7
influx-commands.md Normal file

@ -0,0 +1,7 @@
# Useful InfluxDB commands
## Delete data from bucket
```bash
influx delete --bucket mcutils --start 2024-01-01T00:00:00Z --stop 2025-01-05T00:00:00Z --org mcutils --token setme --predicate '_measurement="requests_per_route"
```

185
pom.xml

@ -5,8 +5,8 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>cc.fascinated</groupId> <groupId>cc.fascinated</groupId>
<artifactId>Minecraft-Helper</artifactId> <artifactId>Minecraft-Utilities</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0.0</version>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
@ -17,58 +17,89 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version> <version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<!-- Build Steps -->
<build> <build>
<finalName>${project.artifactId}</finalName>
<plugins> <plugins>
<!-- Spring -->
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
<configuration>
<additionalProperties>
<description>${project.description}</description>
</additionalProperties>
</configuration>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<dependencies> <repositories>
<dependency> <!-- Jitpack - Used for dnsjava -->
<groupId>org.projectlombok</groupId> <repository>
<artifactId>lombok</artifactId> <id>jitpack.io</id>
<version>1.18.32</version> <url>https://jitpack.io</url>
<scope>provided</scope> </repository>
</dependency> </repositories>
<dependency> <dependencies>
<groupId>org.apache.logging.log4j</groupId> <!-- Spring -->
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- Websockets -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Redis for caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<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.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -77,37 +108,95 @@
<version>0.5.11</version> <version>0.5.11</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>1.20-R0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents.client5</groupId> <groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId> <artifactId>httpclient5</artifactId>
<version>5.3.1</version> <version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<!-- Sentry -->
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.14.0</version>
</dependency>
<!-- InfluxDB Metrics -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-spring</artifactId>
<version>7.2.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>7.2.0</version>
</dependency>
<!-- DNS Lookup -->
<dependency>
<groupId>com.github.dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>v3.5.2</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- Unit Tests --> <!-- SwaggerUI -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.springdoc</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>5.10.2</version> <version>2.6.0</version>
<scope>test</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- GeoIP - IP Lookups -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>com.maxmind.geoip2</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>geoip2</artifactId>
<version>5.10.2</version> <version>4.2.0</version>
<scope>test</scope>
</dependency> </dependency>
<!-- Archive Utilities -->
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.codehaus.plexus</groupId>
<artifactId>spring-test</artifactId> <artifactId>plexus-archiver</artifactId>
<version>6.1.5</version> <version>4.10.0</version>
<scope>test</scope>
</dependency> </dependency>
<!-- Tests -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.github.codemonstur</groupId>
<artifactId>embedded-redis</artifactId>
<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> </dependencies>
</project> </project>

6
renovate.json Normal file

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

@ -1,72 +0,0 @@
package cc.fascinated.common;
import cc.fascinated.Main;
import cc.fascinated.model.player.Skin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@UtilityClass @Log4j2
public class PlayerUtils {
/**
* Gets the skin data from the URL.
*
* @return the skin data
*/
@SneakyThrows
@JsonIgnore
public static BufferedImage getSkinImage(String url) {
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofByteArray());
byte[] body = response.body();
if (body == null) {
return null;
}
return ImageIO.read(new ByteArrayInputStream(body));
}
/**
* Gets the part data from the skin.
*
* @return the part data
*/
public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) {
if (size == -1) {
size = part.getDefaultSize();
}
try {
BufferedImage image = skin.getSkinImage();
if (image == null) {
return null;
}
// Get the part of the image (e.g. the head)
BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight());
// Scale the image
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.drawImage(partImage, 0, 0, size, size, null);
graphics2D.dispose();
partImage = scaledImage;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(partImage, "png", byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
} catch (Exception ex) {
log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex);
return null;
}
}
}

@ -1,22 +0,0 @@
package cc.fascinated.common;
import lombok.experimental.UtilityClass;
@UtilityClass
public class UUIDUtils {
/**
* Add dashes to a UUID.
*
* @param idNoDashes the UUID without dashes
* @return the UUID with dashes
*/
public static String addUuidDashes(String idNoDashes) {
StringBuilder idBuff = new StringBuilder(idNoDashes);
idBuff.insert(20, '-');
idBuff.insert(16, '-');
idBuff.insert(12, '-');
idBuff.insert(8, '-');
return idBuff.toString();
}
}

@ -1,41 +0,0 @@
package cc.fascinated.common;
import lombok.experimental.UtilityClass;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;
@UtilityClass
public class WebRequest {
/**
* The web client.
*/
private static final RestClient CLIENT = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.build();
/**
* Gets a response from the given URL.
*
* @param url the url
* @return the response
* @param <T> the type of the response
*/
public static <T> T getAsEntity(String url, Class<T> clazz) {
try {
ResponseEntity<T> profile = CLIENT.get()
.uri(url)
.retrieve()
.toEntity(clazz);
if (profile.getStatusCode().isError()) {
return null;
}
return profile.getBody();
} catch (HttpClientErrorException ex) {
return null;
}
}
}

@ -1,20 +0,0 @@
package cc.fascinated.config;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
@Getter
public class Config {
public static Config INSTANCE;
@Value("${public-url}")
private String webPublicUrl;
@PostConstruct
public void onInitialize() {
INSTANCE = this;
}
}

@ -1,23 +0,0 @@
package cc.fascinated.controller;
import cc.fascinated.config.Config;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/")
public class HomeController {
/**
* The example UUID.
*/
@SuppressWarnings("FieldCanBeLocal")
private final String exampleUuid = "eeab5f8a-18dd-4d58-af78-2b3c4543da48";
@RequestMapping(value = "/")
public String home(Model model) {
model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid);
return "index";
}
}

@ -1,65 +0,0 @@
package cc.fascinated.controller;
import cc.fascinated.common.PlayerUtils;
import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin;
import cc.fascinated.model.response.impl.InvalidPartResponse;
import cc.fascinated.model.response.impl.PlayerNotFoundResponse;
import cc.fascinated.service.PlayerService;
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.*;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = "/player/")
public class PlayerController {
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
private final PlayerService playerManagerService;
@Autowired
public PlayerController(PlayerService playerManagerService) {
this.playerManagerService = playerManagerService;
}
@ResponseBody
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getPlayer(@PathVariable String id) {
Player player = playerManagerService.getPlayer(id);
if (player == null) { // No player with that id was found
return new PlayerNotFoundResponse().toResponseEntity();
}
// Return the player
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body(player);
}
@GetMapping(value = "/{part}/{id}")
public ResponseEntity<?> getPlayerHead(@PathVariable String part,
@PathVariable String id,
@RequestParam(required = false, defaultValue = "256") int size) {
Player player = playerManagerService.getPlayer(id);
byte[] partBytes = new byte[0];
if (player != null) { // The player exists
Skin skin = player.getSkin();
Skin.Parts skinPart = Skin.Parts.fromName(part);
if (skinPart == null) { // Unknown part name
return new InvalidPartResponse().toResponseEntity();
}
partBytes = PlayerUtils.getSkinPartBytes(skin, skinPart, size);
}
if (partBytes == null) { // Fallback to the default head
partBytes = PlayerUtils.getSkinPartBytes(Skin.DEFAULT_SKIN, Skin.Parts.HEAD, size);
}
// Return the part image
return ResponseEntity.ok()
.cacheControl(cacheControl)
.contentType(MediaType.IMAGE_PNG)
.body(partBytes);
}
}

@ -1,18 +0,0 @@
package cc.fascinated.controller;
import cc.fascinated.model.server.MinecraftServer;
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/server/")
public class ServerController {
@ResponseBody
@GetMapping(value = "/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MinecraftServer> getServer(@PathVariable String hostname) {
return ResponseEntity.ok(JavaMinecraftServerPinger.INSTANCE.ping(hostname, 25565));
}
}

@ -1,13 +0,0 @@
package cc.fascinated.model.mojang;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public final class JavaServerStatusToken {
private final String description;
}

@ -1,48 +0,0 @@
package cc.fascinated.model.player;
import cc.fascinated.common.Tuple;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.model.mojang.MojangProfile;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import java.util.UUID;
@Getter
public class Player {
/**
* The UUID of the player
*/
private final UUID uuid;
/**
* The username of the player
*/
@JsonProperty("username")
private final String name;
/**
* The skin of the player, null if the
* player does not have a skin
*/
private Skin skin;
/**
* The cape of the player, null if the
* player does not have a cape
*/
private Cape cape;
public Player(MojangProfile profile) {
this.uuid = UUID.fromString(UUIDUtils.addUuidDashes(profile.getId()));
this.name = profile.getName();
// Get the skin and cape
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();
if (skinAndCape != null) {
this.skin = skinAndCape.getLeft();
this.cape = skinAndCape.getRight();
}
}
}

@ -1,154 +0,0 @@
package cc.fascinated.model.player;
import cc.fascinated.common.PlayerUtils;
import cc.fascinated.config.Config;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
@Getter @Log4j2
public class Skin {
/**
* The default skin, usually used when the skin is not found.
*/
public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1",
Model.DEFAULT);
/**
* The URL for the skin
*/
private final String url;
/**
* The model for the skin
*/
private final Model model;
/**
* The skin image for the skin
*/
@JsonIgnore
private final BufferedImage skinImage;
/**
* The part URLs of the skin
*/
@JsonProperty("parts")
private final Map<String, String> partUrls = new HashMap<>();
public Skin(String url, Model model) {
this.url = url;
this.model = model;
this.skinImage = PlayerUtils.getSkinImage(url);
}
/**
* Gets the skin from a {@link JsonObject}.
*
* @param json the JSON object
* @return the skin
*/
public static Skin fromJson(JsonObject json) {
if (json == null) {
return null;
}
String url = json.get("url").getAsString();
JsonObject metadata = json.getAsJsonObject("metadata");
Model model = Model.fromName(metadata == null ? "slim" : // Fall back to slim if the model is not found
metadata.get("model").getAsString());
return new Skin(url, model);
}
/**
* Populates the part URLs for the skin.
*
* @param playerUuid the player's UUID
*/
public Skin populatePartUrls(String playerUuid) {
for (Parts part : Parts.values()) {
String partName = part.name().toLowerCase();
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=" + part.getDefaultSize());
}
return this;
}
/**
* The skin part enum that contains the
* information about the part.
*/
@Getter @AllArgsConstructor
public enum Parts {
HEAD(8, 8, 8, 8, 256);
/**
* The x and y position of the part.
*/
private final int x, y;
/**
* The width and height of the part.
*/
private final int width, height;
/**
* The scale of the part.
*/
private final int defaultSize;
/**
* Gets the name of the part.
*
* @return the name of the part
*/
public String getName() {
return this.name().toLowerCase();
}
/**
* Gets the skin part from its name.
*
* @param name the name of the part
* @return the skin part
*/
public static Parts fromName(String name) {
for (Parts part : values()) {
if (part.name().equalsIgnoreCase(name)) {
return part;
}
}
return null;
}
}
/**
* The model of the skin.
*/
public enum Model {
DEFAULT,
SLIM;
/**
* Gets the model from its name.
*
* @param name the name of the model
* @return the model
*/
public static Model fromName(String name) {
for (Model model : values()) {
if (model.name().equalsIgnoreCase(name)) {
return model;
}
}
return null;
}
}
}

@ -1,29 +0,0 @@
package cc.fascinated.model.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@Getter @AllArgsConstructor
public class Response {
/**
* The status code of this error.
*/
private HttpStatus status;
/**
* The message of this error.
*/
private String message;
/**
* Gets this response as a {@link ResponseEntity}.
*
* @return the response entity
*/
public ResponseEntity<?> toResponseEntity() {
return new ResponseEntity<>(this, status);
}
}

@ -1,11 +0,0 @@
package cc.fascinated.model.response.impl;
import cc.fascinated.model.response.Response;
import org.springframework.http.HttpStatus;
public class InvalidPartResponse extends Response {
public InvalidPartResponse() {
super(HttpStatus.NOT_FOUND, "Invalid part name.");
}
}

@ -1,11 +0,0 @@
package cc.fascinated.model.response.impl;
import cc.fascinated.model.response.Response;
import org.springframework.http.HttpStatus;
public class PlayerNotFoundResponse extends Response {
public PlayerNotFoundResponse() {
super(HttpStatus.NOT_FOUND, "Player not found.");
}
}

@ -1,10 +0,0 @@
package cc.fascinated.model.server;
/**
* @author Braydon
*/
public final class JavaMinecraftServer extends MinecraftServer {
public JavaMinecraftServer(String hostname, int port, String motd) {
super(hostname, port, motd);
}
}

@ -1,15 +0,0 @@
package cc.fascinated.model.server;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public class MinecraftServer {
private final String hostname;
private final int port;
private final String motd;
}

@ -1,40 +0,0 @@
package cc.fascinated.service;
import cc.fascinated.common.WebRequest;
import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service @Log4j2
public class MojangAPIService {
@Value("${mojang.session-server}")
private String mojangSessionServerUrl;
@Value("${mojang.api}")
private String mojangApiUrl;
/**
* Gets the Session Server profile of the
* player with the given UUID.
*
* @param id the uuid or name of the player
* @return the profile
*/
public MojangProfile getProfile(String id) {
return WebRequest.getAsEntity(mojangSessionServerUrl + "/session/minecraft/profile/" + id, MojangProfile.class);
}
/**
* Gets the UUID of the player using
* the name of the player.
*
* @param id the name of the player
* @return the profile
*/
public MojangUsernameToUuid getUuidFromUsername(String id) {
return WebRequest.getAsEntity(mojangApiUrl + "/users/profiles/minecraft/" + id, MojangUsernameToUuid.class);
}
}

@ -1,81 +0,0 @@
package cc.fascinated.service;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.model.player.Player;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service @Log4j2
public class PlayerService {
/**
* The cache of players.
*/
private final Map<UUID, Player> players = ExpiringMap.builder()
.expiration(1, TimeUnit.HOURS)
.expirationPolicy(ExpirationPolicy.CREATED)
.build();
/**
* The cache of player names to UUIDs.
*/
private final Map<String, UUID> playerNameToUUIDCache = ExpiringMap.builder()
.expiration(1, TimeUnit.DAYS)
.expirationPolicy(ExpirationPolicy.CREATED)
.build();
private final MojangAPIService mojangAPIService;
public PlayerService(MojangAPIService mojangAPIService) {
this.mojangAPIService = mojangAPIService;
}
/**
* Gets a player by their UUID.
*
* @param id the uuid or name of the player
* @return the player or null if the player does not exist
*/
public Player getPlayer(String id) {
UUID uuid = null;
if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID
try {
uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id);
} catch (Exception ignored) {}
} else { // Check if the id is a name
uuid = playerNameToUUIDCache.get(id.toUpperCase());
}
// Check if the player is cached
if (uuid != null && players.containsKey(uuid)) {
return players.get(uuid);
}
MojangProfile profile = uuid == null ? null : mojangAPIService.getProfile(uuid.toString());
if (profile == null) { // The player cannot be found using their UUID
MojangUsernameToUuid apiProfile = mojangAPIService.getUuidFromUsername(id); // Get the UUID of the player using their name
if (apiProfile == null || !apiProfile.isValid()) {
return null;
}
// Get the profile of the player using their UUID
profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ?
UUIDUtils.addUuidDashes(apiProfile.getId()) : apiProfile.getId());
}
if (profile == null) { // The player cannot be found using their name or UUID
log.info("Player with id {} could not be found", id);
return null;
}
Player player = new Player(profile);
players.put(player.getUuid(), player);
playerNameToUUIDCache.put(player.getName().toUpperCase(), player.getUuid());
return player;
}
}

@ -1,9 +0,0 @@
package cc.fascinated.service.pinger;
/**
* @author Braydon
* @param <T> the type of server to ping
*/
public interface MinecraftServerPinger<T> {
T ping(String hostname, int port);
}

@ -1,53 +0,0 @@
package cc.fascinated.service.pinger.impl;
import cc.fascinated.Main;
import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart;
import cc.fascinated.model.mojang.JavaServerStatusToken;
import cc.fascinated.model.server.JavaMinecraftServer;
import cc.fascinated.service.pinger.MinecraftServerPinger;
import lombok.extern.log4j.Log4j2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* @author Braydon
*/
@Log4j2(topic = "Java Pinger")
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
public static final JavaMinecraftServerPinger INSTANCE = new JavaMinecraftServerPinger();
private static final int TIMEOUT = 3000; // The timeout for the socket
@Override
public JavaMinecraftServer ping(String hostname, int port) {
log.info("Pinging {}:{}...", hostname, port);
// Open a socket connection to the server
try (Socket socket = new Socket()) {
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
// Open data streams to begin packet transaction
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
// Begin handshaking with the server
new JavaPacketHandshakingInSetProtocol(hostname, port, 47).process(inputStream, outputStream);
// Send the status request to the server, and await back the response
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream);
System.out.println("packetStatusInStart.getResponse() = " + packetStatusInStart.getResponse());
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
return new JavaMinecraftServer(hostname, port, token.getDescription());
}
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
}

@ -1,6 +1,7 @@
package cc.fascinated; package xyz.mcutils.backend;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@ -12,10 +13,12 @@ import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.Objects; import java.util.Objects;
@SpringBootApplication @Log4j2 @Log4j2(topic = "Main")
@SpringBootApplication
public class Main { public class Main {
public static final Gson GSON = new GsonBuilder()
public static final Gson GSON = new Gson(); .setDateFormat("MM-dd-yyyy HH:mm:ss")
.create();
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
@SneakyThrows @SneakyThrows
@ -30,6 +33,6 @@ public class Main {
} }
log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
SpringApplication.run(Main.class, args); SpringApplication.run(Main.class, args); // Start the application
} }
} }

@ -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;
}
}
}

@ -0,0 +1,59 @@
package xyz.mcutils.backend.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor @NoArgsConstructor
@Getter
public class CachedResponse {
/**
* The cache information for this response.
*/
private Cache cache;
@AllArgsConstructor @Getter @Setter
public static class Cache {
/**
* Whether this request is cached.
*/
private boolean cached;
/**
* The unix timestamp of when this was cached.
*/
private long cachedTime;
/**
* Create a new cache information object with the default values.
* <p>
* The default values are:
* <br>
* <ul>
* <li>cached: true</li>
* <li>cachedAt: {@link System#currentTimeMillis()}</li>
* </ul>
* <br>
* </p>
*
* @return the default cache information object
*/
public static Cache defaultCache() {
return new Cache(true, System.currentTimeMillis());
}
/**
* Sets if this request is cached.
*
* @param cached the new value of if this request is cached
*/
public void setCached(boolean cached) {
this.cached = cached;
if (!cached) {
cachedTime = -1;
}
}
}
}

@ -0,0 +1,112 @@
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;
/**
* @author Braydon
*/
@UtilityClass
public final class ColorUtils {
private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)§[0-9A-FK-OR]");
private static final Map<Character, String> COLOR_MAP = new HashMap<>();
static {
// Map each color to its corresponding hex code
COLOR_MAP.put('0', "#000000"); // Black
COLOR_MAP.put('1', "#0000AA"); // Dark Blue
COLOR_MAP.put('2', "#00AA00"); // Dark Green
COLOR_MAP.put('3', "#00AAAA"); // Dark Aqua
COLOR_MAP.put('4', "#AA0000"); // Dark Red
COLOR_MAP.put('5', "#AA00AA"); // Dark Purple
COLOR_MAP.put('6', "#FFAA00"); // Gold
COLOR_MAP.put('7', "#AAAAAA"); // Gray
COLOR_MAP.put('8', "#555555"); // Dark Gray
COLOR_MAP.put('9', "#5555FF"); // Blue
COLOR_MAP.put('a', "#55FF55"); // Green
COLOR_MAP.put('b', "#55FFFF"); // Aqua
COLOR_MAP.put('c', "#FF5555"); // Red
COLOR_MAP.put('d', "#FF55FF"); // Light Purple
COLOR_MAP.put('e', "#FFFF55"); // Yellow
COLOR_MAP.put('f', "#FFFFFF"); // White
}
/**
* Strip the color codes
* from the given input.
*
* @param input the input to strip
* @return the stripped input
*/
@NonNull
public static String stripColor(@NonNull String input) {
return STRIP_COLOR_PATTERN.matcher(input).replaceAll("");
}
/**
* Convert the given input
* into HTML.
*
* @param input the input to convert
* @return the HTML converted input
*/
@NonNull
public static String toHTML(@NonNull String input) {
StringBuilder builder = new StringBuilder();
boolean nextIsColor = false; // Is the next char a color code?
// Get the leading spaces from the first line
int leadingSpaces = 0;
boolean foundNonSpace = false;
for (char character : input.toCharArray()) {
if (character == ' ' && !foundNonSpace) {
leadingSpaces++;
} else {
foundNonSpace = true;
}
}
for (char character : input.toCharArray()) {
// Found color symbol, next color is the color
if (character == '§') {
nextIsColor = true;
continue;
}
if (nextIsColor) { // Map the current color to its hex code
String color = COLOR_MAP.getOrDefault(Character.toLowerCase(character), "");
builder.append("<span style=\"color:").append(color).append("\">");
nextIsColor = false;
continue;
}
if (character == ' ') { // Preserve space character
builder.append("&nbsp;");
continue;
}
builder.append(character); // Append the char...
}
// Add leading spaces to the end of the HTML string
builder.append("&nbsp;".repeat(Math.max(0, leadingSpaces)));
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);
}
}

@ -0,0 +1,58 @@
package xyz.mcutils.backend.common;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.Type;
import xyz.mcutils.backend.model.dns.impl.ARecord;
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
/**
* @author Braydon
*/
@UtilityClass
public final class DNSUtils {
private static final String SRV_QUERY_PREFIX = "_minecraft._tcp.%s";
/**
* Get the resolved address and port of the
* given hostname by resolving the SRV records.
*
* @param hostname the hostname to resolve
* @return the resolved address and port, null if none
*/
@SneakyThrows
public static SRVRecord resolveSRV(@NonNull String hostname) {
Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records
if (records == null) { // No records exist
return null;
}
SRVRecord result = null;
for (Record record : records) {
result = new SRVRecord((org.xbill.DNS.SRVRecord) record);
}
return result;
}
/**
* Get the resolved address of the given
* hostname by resolving the A records.
*
* @param hostname the hostname to resolve
* @return the resolved address, null if none
*/
@SneakyThrows
public static ARecord resolveA(@NonNull String hostname) {
Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records
if (records == null) { // No records exist
return null;
}
ARecord result = null;
for (Record record : records) {
result = new ARecord((org.xbill.DNS.ARecord) record);
}
return result;
}
}

@ -0,0 +1,21 @@
package xyz.mcutils.backend.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatusCode;
import java.util.List;
@AllArgsConstructor @Getter
public class Endpoint {
/**
* The endpoint.
*/
private final String endpoint;
/**
* The statuses that indicate that the endpoint is online.
*/
private final List<HttpStatusCode> allowedStatuses;
}

@ -0,0 +1,26 @@
package xyz.mcutils.backend.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
/**
* @author Braydon
*/
@UtilityClass
public final class EnumUtils {
/**
* Get the enum constant of the specified enum type with the specified name.
*
* @param enumType the enum type
* @param name the name of the constant to return
* @param <T> the type of the enum
* @return the enum constant of the specified enum type with the specified name
*/
public <T extends Enum<T>> T getEnumConstant(@NonNull Class<T> enumType, @NonNull String name) {
try {
return Enum.valueOf(enumType, name);
} catch (IllegalArgumentException ex) {
return null;
}
}
}

@ -0,0 +1,132 @@
package xyz.mcutils.backend.common;
import lombok.NonNull;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* A simple set that expires elements after a certain
* amount of time, utilizing the {@link ExpiringMap} library.
*
* @param <T> The type of element to store within this set
* @author Braydon
*/
public final class ExpiringSet<T> implements Iterable<T> {
/**
* The internal cache for this set.
*/
@NonNull private final ExpiringMap<T, Long> cache;
/**
* The lifetime (in millis) of the elements in this set.
*/
private final long lifetime;
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit) {
this(expirationPolicy, duration, timeUnit, ignored -> {});
}
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit, @NonNull Consumer<T> onExpire) {
//noinspection unchecked
this.cache = ExpiringMap.builder()
.expirationPolicy(expirationPolicy)
.expiration(duration, timeUnit)
.expirationListener((key, ignored) -> onExpire.accept((T) key))
.build();
this.lifetime = timeUnit.toMillis(duration); // Get the lifetime in millis
}
/**
* Add an element to this set.
*
* @param element the element
* @return whether the element was added
*/
public boolean add(@NonNull T element) {
boolean contains = contains(element); // Does this set already contain the element?
this.cache.put(element, System.currentTimeMillis() + this.lifetime);
return !contains;
}
/**
* Get the entry time of an element in this set.
*
* @param element the element
* @return the entry time, -1 if not contained
*/
public long getEntryTime(@NonNull T element) {
return contains(element) ? this.cache.get(element) - this.lifetime : -1L;
}
/**
* Check if an element is
* contained within this set.
*
* @param element the element
* @return whether the element is contained
*/
public boolean contains(@NonNull T element) {
Long timeout = this.cache.get(element); // Get the timeout for the element
return timeout != null && (timeout > System.currentTimeMillis());
}
/**
* Check if this set is empty.
*
* @return whether this set is empty
*/
public boolean isEmpty() {
return this.cache.isEmpty();
}
/**
* Get the size of this set.
*
* @return the size
*/
public int size() {
return this.cache.size();
}
/**
* Remove an element from this set.
*
* @param element the element
* @return whether the element was removed
*/
public boolean remove(@NonNull T element) {
return this.cache.remove(element) != null;
}
/**
* Clear this set.
*/
public void clear() {
this.cache.clear();
}
/**
* Get the elements in this set.
*
* @return the elements
*/
@NonNull
public Set<T> getElements() {
return this.cache.keySet();
}
/**
* Returns an iterator over elements of type {@code T}.
*
* @return an Iterator.
*/
@Override @NonNull
public Iterator<T> iterator() {
return this.cache.keySet().iterator();
}
}

@ -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);
}
}
}

@ -1,4 +1,4 @@
package cc.fascinated.common; package xyz.mcutils.backend.common;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;

@ -0,0 +1,78 @@
package xyz.mcutils.backend.common;
import jakarta.validation.constraints.NotNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
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(topic = "Image Utils")
public class ImageUtils {
/**
* Scale the given image to the provided scale.
*
* @param image the image to scale
* @param scale the scale to scale the image to
* @return the scaled image
*/
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(scale, scale), null);
graphics.dispose();
return scaled;
}
/**
* Flip the given image.
*
* @param image the image to flip
* @return the flipped image
*/
public static BufferedImage flip(@NotNull final BufferedImage image) {
BufferedImage flipped = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = flipped.createGraphics();
graphics.drawImage(image, image.getWidth(), 0, 0, image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), null);
graphics.dispose();
return flipped;
}
/**
* Convert an image to bytes.
*
* @param image the image to convert
* @return the image as bytes
*/
@SneakyThrows
public static byte[] imageToBytes(BufferedImage image) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", outputStream);
return outputStream.toByteArray();
} catch (Exception e) {
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);
}
}
}

@ -0,0 +1,202 @@
package xyz.mcutils.backend.common;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
/**
* @author Braydon
* @see <a href="https://wiki.vg/Protocol_version_numbers">Protocol Version Numbers</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-16">Spigot NMS (1.16+)</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-10-1-15">Spigot NMS (1.10 - 1.15)</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-legacy">Spigot NMS (1.8 - 1.9)</a>
*/
@RequiredArgsConstructor @Getter @ToString @Log4j2(topic = "Minecraft Version")
public enum JavaMinecraftVersion {
V1_20_3(765, "v1_20_R3"), // 1.20.3 & 1.20.4
V1_20_2(764, "v1_20_R2"), // 1.20.2
V1_20(763, "v1_20_R1"), // 1.20 & 1.20.1
V1_19_4(762, "v1_19_R3"), // 1.19.4
V1_19_3(761, "v1_19_R2"), // 1.19.3
V1_19_1(760, "v1_19_R1"), // 1.19.1 & 1.19.2
V1_19(759, "v1_19_R1"), // 1.19
V1_18_2(758, "v1_18_R2"), // 1.18.2
V1_18(757, "v1_18_R1"), // 1.18 & 1.18.1
V1_17_1(756, "v1_17_R1"), // 1.17.1
V1_17(755, "v1_17_R1"), // 1.17
V1_16_4(754, "v1_16_R3"), // 1.16.4 & 1.16.5
V1_16_3(753, "v1_16_R2"), // 1.16.3
V1_16_2(751, "v1_16_R2"), // 1.16.2
V1_16_1(736, "v1_16_R1"), // 1.16.1
V1_16(735, "v1_16_R1"), // 1.16
V1_15_2(578, "v1_15_R1"), // 1.15.2
V1_15_1(575, "v1_15_R1"), // 1.15.1
V1_15(573, "v1_15_R1"), // 1.15
V1_14_4(498, "v1_14_R1"), // 1.14.4
V1_14_3(490, "v1_14_R1"), // 1.14.3
V1_14_2(485, "v1_14_R1"), // 1.14.2
V1_14_1(480, "v1_14_R1"), // 1.14.1
V1_14(477, "v1_14_R1"), // 1.14
V1_13_2(404, "v1_13_R2"), // 1.13.2
V1_13_1(401, "v1_13_R2"), // 1.13.1
V1_13(393, "v1_13_R1"), // 1.13
V1_12_2(340, "v1_12_R1"), // 1.12.2
V1_12_1(338, "v1_12_R1"), // 1.12.1
V1_12(335, "v1_12_R1"), // 1.12
V1_11_1(316, "v1_11_R1"), // 1.11.1 & 1.11.2
V1_11(315, "v1_11_R1"), // 1.11
V1_10(210, "v1_10_R1"), // 1.10.x
V1_9_3(110, "v1_9_R2"), // 1.9.3 & 1.9.4
V1_9_2(109, "v1_9_R1"), // 1.9.2
V1_9_1(108, "v1_9_R1"), // 1.9.1
V1_9(107, "v1_9_R1"), // 1.9
V1_8(47, "v1_8_R3"), // 1.8.x
V1_7_6(5, "v1_7_R4"), // 1.7.6 - 1.7.10
UNKNOWN(-1, "Unknown");
// Game Updates
public static final JavaMinecraftVersion TRAILS_AND_TALES = JavaMinecraftVersion.V1_20;
public static final JavaMinecraftVersion THE_WILD_UPDATE = JavaMinecraftVersion.V1_19;
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_2 = JavaMinecraftVersion.V1_18;
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_1 = JavaMinecraftVersion.V1_17;
public static final JavaMinecraftVersion NETHER_UPDATE = JavaMinecraftVersion.V1_16;
public static final JavaMinecraftVersion BUZZY_BEES = JavaMinecraftVersion.V1_15;
public static final JavaMinecraftVersion VILLAGE_AND_PILLAGE = JavaMinecraftVersion.V1_14;
public static final JavaMinecraftVersion UPDATE_AQUATIC = JavaMinecraftVersion.V1_13;
public static final JavaMinecraftVersion WORLD_OF_COLOR_UPDATE = JavaMinecraftVersion.V1_12;
public static final JavaMinecraftVersion EXPLORATION_UPDATE = JavaMinecraftVersion.V1_11;
public static final JavaMinecraftVersion FROSTBURN_UPDATE = JavaMinecraftVersion.V1_10;
public static final JavaMinecraftVersion THE_COMBAT_UPDATE = JavaMinecraftVersion.V1_9;
public static final JavaMinecraftVersion BOUNTIFUL_UPDATE = JavaMinecraftVersion.V1_8;
private static final JavaMinecraftVersion[] VALUES = JavaMinecraftVersion.values();
/**
* The protocol number of this version.
*/
private final int protocol;
/**
* The server version for this version.
*/
private final String nmsVersion;
/**
* The cached name of this version.
*/
private String name;
/**
* Get the name of this protocol version.
*
* @return the name
*/
public String getName() {
// We have a name
if (this.name != null) {
return this.name;
}
// Use the server version as the name if unknown
if (this == UNKNOWN) {
this.name = this.getNmsVersion();
} else { // Parse the name
this.name = name().substring(1);
this.name = this.name.replace("_", ".");
}
return this.name;
}
/**
* Is this version legacy?
*
* @return whether this version is legacy
*/
public boolean isLegacy() {
return this.isBelow(JavaMinecraftVersion.V1_16);
}
/**
* Check if this version is
* above the one given.
*
* @param other the other version
* @return true if above, otherwise false
*/
public boolean isAbove(JavaMinecraftVersion other) {
return this.protocol > other.getProtocol();
}
/**
* Check if this version is
* or above the one given.
*
* @param other the other version
* @return true if is or above, otherwise false
*/
public boolean isOrAbove(JavaMinecraftVersion other) {
return this.protocol >= other.getProtocol();
}
/**
* Check if this version is
* below the one given.
*
* @param other the other version
* @return true if below, otherwise false
*/
public boolean isBelow(JavaMinecraftVersion other) {
return this.protocol < other.getProtocol();
}
/**
* Check if this version is
* or below the one given.
*
* @param other the other version
* @return true if is or below, otherwise false
*/
public boolean isOrBelow(JavaMinecraftVersion other) {
return this.protocol <= other.getProtocol();
}
/**
* Get the minimum Minecraft version.
*
* @return the minimum version
*/
@NonNull
public static JavaMinecraftVersion getMinimumVersion() {
return VALUES[VALUES.length - 2];
}
/**
* Get the version from the given protocol.
*
* @param protocol the protocol to get the version for
* @return the version, null if none
*/
public static JavaMinecraftVersion byProtocol(int protocol) {
for (JavaMinecraftVersion version : values()) {
if (version.getProtocol() == protocol) {
return version;
}
}
return null;
}
}

@ -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
}
}

@ -0,0 +1,50 @@
package xyz.mcutils.backend.common;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.exception.impl.BadRequestException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
@UtilityClass @Log4j2(topic = "Player Utils")
public class PlayerUtils {
/**
* Gets the UUID from the string.
*
* @param id the id string
* @return the UUID
*/
public static UUID getUuidFromString(String id) {
UUID uuid;
boolean isFullUuid = id.length() == 36;
if (id.length() == 32 || isFullUuid) {
try {
uuid = isFullUuid ? UUID.fromString(id) : UUIDUtils.addDashes(id);
} catch (IllegalArgumentException exception) {
throw new BadRequestException("Invalid UUID provided: %s".formatted(id));
}
return uuid;
}
return null;
}
/**
* Gets the skin data from the URL.
*
* @return the skin data
*/
@SneakyThrows
@JsonIgnore
public static byte[] getSkinImage(String url) {
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofByteArray());
return response.body();
}
}

@ -0,0 +1,16 @@
package xyz.mcutils.backend.common;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ServerUtils {
/**
* Gets the address of the server.
*
* @return the address of the server
*/
public static String getAddress(String ip, int port) {
return ip + (port == 25565 ? "" : ":" + port);
}
}

@ -0,0 +1,19 @@
package xyz.mcutils.backend.common;
public class Timer {
/**
* Schedules a task to run after a delay.
*
* @param runnable the task to run
* @param delay the delay before the task runs
*/
public static void scheduleRepeating(Runnable runnable, long delay, long period) {
new java.util.Timer().scheduleAtFixedRate(new java.util.TimerTask() {
@Override
public void run() {
runnable.run();
}
}, delay, period);
}
}

@ -1,4 +1,4 @@
package cc.fascinated.common; package xyz.mcutils.backend.common;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;

@ -0,0 +1,36 @@
package xyz.mcutils.backend.common;
import io.micrometer.common.lang.NonNull;
import lombok.experimental.UtilityClass;
import java.util.UUID;
@UtilityClass
public class UUIDUtils {
/**
* Add dashes to a UUID.
*
* @param trimmed the UUID without dashes
* @return the UUID with dashes
*/
@NonNull
public static UUID addDashes(@NonNull String trimmed) {
StringBuilder builder = new StringBuilder(trimmed);
for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
builder.insert(pos, "-");
}
return UUID.fromString(builder.toString());
}
/**
* Remove dashes from a UUID.
*
* @param dashed the UUID with dashes
* @return the UUID without dashes
*/
@NonNull
public static String removeDashes(@NonNull UUID dashed) {
return dashed.toString().replace("-", "");
}
}

@ -0,0 +1,77 @@
package xyz.mcutils.backend.common;
import lombok.experimental.UtilityClass;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import xyz.mcutils.backend.exception.impl.RateLimitException;
@UtilityClass
public class WebRequest {
/**
* The web client.
*/
private static final RestClient CLIENT;
static {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(2500); // 2.5 seconds
CLIENT = RestClient.builder()
.requestFactory(requestFactory)
.build();
}
/**
* Gets a response from the given URL.
*
* @param url the url
* @return the response
* @param <T> the type of the response
*/
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
ResponseEntity<T> responseEntity = CLIENT.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
.toEntity(clazz);
if (responseEntity.getStatusCode().isError()) {
return null;
}
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return responseEntity.getBody();
}
/**
* Gets a response from the given URL.
*
* @param url the url
* @return the response
*/
public static ResponseEntity<?> get(String url, Class<?> clazz) {
return CLIENT.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
.toEntity(clazz);
}
/**
* Gets a response from the given URL.
*
* @param url the url
* @return the response
*/
public static ResponseEntity<?> head(String url, Class<?> clazz) {
return CLIENT.head()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
.toEntity(clazz);
}
}

@ -0,0 +1,23 @@
package xyz.mcutils.backend.common.packet;
import lombok.NonNull;
import java.io.IOException;
import java.net.DatagramSocket;
/**
* Represents a packet in the
* Minecraft Bedrock protocol.
*
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
*/
public interface MinecraftBedrockPacket {
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
void process(@NonNull DatagramSocket socket) throws IOException;
}

@ -1,4 +1,4 @@
package cc.fascinated.common.packet; package xyz.mcutils.backend.common.packet;
import lombok.NonNull; import lombok.NonNull;

@ -0,0 +1,42 @@
package xyz.mcutils.backend.common.packet.impl.bedrock;
import lombok.NonNull;
import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* This packet is sent by the client to the server to
* request a pong response from the server. The server
* will respond with a string containing the server's status.
*
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
*/
public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
private static final byte ID = 0x01; // The ID of the packet
private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
// Construct the packet buffer
ByteBuffer buffer = ByteBuffer.allocate(33).order(ByteOrder.LITTLE_ENDIAN);;
buffer.put(ID); // Packet ID
buffer.putLong(System.currentTimeMillis()); // Timestamp
buffer.put(MAGIC); // Magic
buffer.putLong(0L); // Client GUID
// Send the packet
socket.send(new DatagramPacket(buffer.array(), 0, buffer.limit()));
}
}

@ -0,0 +1,62 @@
package xyz.mcutils.backend.common.packet.impl.bedrock;
import lombok.Getter;
import lombok.NonNull;
import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
import xyz.mcutils.backend.model.server.BedrockMinecraftServer;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
/**
* This packet is sent by the server to the client in
* response to the {@link BedrockPacketUnconnectedPing}.
*
* @author Braydon
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
*/
@Getter
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
private static final byte ID = 0x1C; // The ID of the packet
/**
* The response from the server, null if none.
*/
private String response;
/**
* Process this packet.
*
* @param socket the socket to process the packet for
* @throws IOException if an I/O error occurs
*/
@Override
public void process(@NonNull DatagramSocket socket) throws IOException {
// Handle receiving of the packet
byte[] receiveData = new byte[2048];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
socket.receive(receivePacket);
// Construct a buffer from the received packet
ByteBuffer buffer = ByteBuffer.wrap(receivePacket.getData()).order(ByteOrder.LITTLE_ENDIAN);
byte id = buffer.get(); // The received packet id
if (id == ID) {
String response = new String(buffer.array(), StandardCharsets.UTF_8).trim(); // Extract the response
// Trim the length of the response (short) from the
// start of the string, which begins with the edition name
for (BedrockMinecraftServer.Edition edition : BedrockMinecraftServer.Edition.values()) {
int startIndex = response.indexOf(edition.name());
if (startIndex != -1) {
response = response.substring(startIndex);
break;
}
}
this.response = response;
}
}
}

@ -1,9 +1,9 @@
package cc.fascinated.common.packet.impl.java; package xyz.mcutils.backend.common.packet.impl.java;
import cc.fascinated.common.packet.MinecraftJavaPacket;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.NonNull; import lombok.NonNull;
import lombok.ToString; import lombok.ToString;
import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;

@ -1,8 +1,8 @@
package cc.fascinated.common.packet.impl.java; package xyz.mcutils.backend.common.packet.impl.java;
import cc.fascinated.common.packet.MinecraftJavaPacket;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;

@ -0,0 +1,27 @@
package xyz.mcutils.backend.common.renderer;
import xyz.mcutils.backend.model.skin.ISkinPart;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public abstract class IsometricSkinRenderer<T extends ISkinPart> extends SkinRenderer<T> {
/**
* Draw a part onto the texture.
*
* @param graphics the graphics to draw to
* @param partImage the part image to draw
* @param transform the transform to apply
* @param x the x position to draw at
* @param y the y position to draw at
* @param width the part image width
* @param height the part image height
*/
protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform,
double x, double y, int width, int height) {
graphics.setTransform(transform);
graphics.drawImage(partImage, (int) x, (int) y, width, height, null);
}
}

@ -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);
}

@ -0,0 +1,108 @@
package xyz.mcutils.backend.common.renderer;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.model.skin.Skin;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@Log4j2(topic = "Skin Renderer")
public abstract class SkinRenderer<T extends ISkinPart> {
/**
* Get the texture of a part of the skin.
*
* @param skin the skin to get the part texture from
* @param part the part of the skin to get
* @param size the size to scale the texture to
* @param renderOverlays should the overlays be rendered
* @return the texture of the skin part
*/
@SneakyThrows
public BufferedImage getVanillaSkinPart(Skin skin, ISkinPart.Vanilla part, double size, boolean renderOverlays) {
ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part
// The skin texture is legacy, use legacy coordinates
if (skin.isLegacy() && part.hasLegacyCoordinates()) {
coordinates = part.getLegacyCoordinates();
}
int width = part.getWidth(); // The width of the part
if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) {
width--;
}
BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture
BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size);
if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) {
partTexture = ImageUtils.flip(partTexture);
}
// Draw part overlays
ISkinPart.Vanilla[] overlayParts = part.getOverlays();
if (overlayParts != null && renderOverlays) {
log.info("Applying overlays to part: {}", part.name());
for (ISkinPart.Vanilla overlay : overlayParts) {
applyOverlay(partTexture.createGraphics(), getVanillaSkinPart(skin, overlay, size, false));
}
}
return partTexture;
}
/**
* Get the texture of a specific part of the skin.
*
* @param skinImage the skin image to get the part from
* @param x the x position of the part
* @param y the y position of the part
* @param width the width of the part
* @param height the height of the part
* @param size the size to scale the part to
* @return the texture of the skin part
*/
@SneakyThrows
private BufferedImage getSkinPartTexture(BufferedImage skinImage, int x, int y, int width, int height, double size) {
// Create a new BufferedImage for the part of the skin texture
BufferedImage headTexture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// Crop just the part we want based on our x, y, width, and height
headTexture.getGraphics().drawImage(skinImage, 0, 0, width, height, x, y, x + width, y + height, null);
// Scale the skin part texture
if (size > 0D) {
headTexture = ImageUtils.resize(headTexture, size);
}
return headTexture;
}
/**
* Apply an overlay to a texture.
*
* @param graphics the graphics to overlay on
* @param overlayImage the part to overlay
*/
protected void applyOverlay(Graphics2D graphics, BufferedImage overlayImage) {
try {
graphics.drawImage(overlayImage, 0, 0, null);
graphics.dispose();
} catch (Exception ignored) {
// We can safely ignore this, legacy
// skins don't have overlays
}
}
/**
* Renders the skin part for the player's skin.
*
* @param skin the player's skin
* @param part the skin part to render
* @param renderOverlays should the overlays be rendered
* @param size the size of the part
* @return the rendered skin part
*/
public abstract BufferedImage render(Skin skin, T part, boolean renderOverlays, int size);
}

@ -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);
}
}

@ -0,0 +1,42 @@
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.SkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Body")
public class BodyRenderer extends SkinRenderer<ISkinPart.Custom> {
public static final BodyRenderer INSTANCE = new BodyRenderer();
@Override
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
BufferedImage texture = new BufferedImage(16, 32, BufferedImage.TYPE_INT_ARGB); // The texture to return
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
// Get the Vanilla skin parts to draw
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, -1, renderOverlays);
BufferedImage body = getVanillaSkinPart(skin, ISkinPart.Vanilla.BODY_FRONT, -1, renderOverlays);
BufferedImage leftArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_ARM_FRONT, -1, renderOverlays);
BufferedImage rightArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_ARM_FRONT, -1, renderOverlays);
BufferedImage leftLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_LEG_FRONT, -1, renderOverlays);
BufferedImage rightLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_LEG_FRONT, -1, renderOverlays);
// Draw the body parts
graphics.drawImage(face, 4, 0, null);
graphics.drawImage(body, 4, 8, null);
graphics.drawImage(leftArm, skin.getModel() == Skin.Model.SLIM ? 1 : 0, 8, null);
graphics.drawImage(rightArm, 12, 8, null);
graphics.drawImage(leftLeg, 8, 20, null);
graphics.drawImage(rightLeg, 4, 20, null);
graphics.dispose();
return ImageUtils.resize(texture, (double) size / 32);
}
}

@ -0,0 +1,48 @@
package xyz.mcutils.backend.common.renderer.impl.skin;
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.model.skin.Skin;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public class IsometricHeadRenderer extends IsometricSkinRenderer<ISkinPart.Custom> {
public static final IsometricHeadRenderer INSTANCE = new IsometricHeadRenderer();
private static final double SKEW_A = 26D / 45D; // 0.57777777
private static final double SKEW_B = SKEW_A * 2D; // 1.15555555
private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0);
private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A);
private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D);
@Override
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
double scale = (size / 8D) / 2.5;
double zOffset = scale * 3.5D;
double xOffset = scale * 2D;
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
// Get the Vanilla skin parts to draw
BufferedImage headTop = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_TOP, scale, renderOverlays);
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, scale, renderOverlays);
BufferedImage headLeft = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_LEFT, scale, renderOverlays);
// Draw the top head part
drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2);
// Draw the face part
double x = xOffset + 8 * scale;
drawPart(graphics, face, FACE_TRANSFORM, x, x + zOffset - 0.5, face.getWidth(), face.getHeight());
// Draw the left head part
drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight());
graphics.dispose();
return texture;
}
}

@ -0,0 +1,32 @@
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.common.renderer.SkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart;
import xyz.mcutils.backend.model.skin.Skin;
import java.awt.*;
import java.awt.image.BufferedImage;
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Square")
public class SquareRenderer extends SkinRenderer<ISkinPart.Vanilla> {
public static final SquareRenderer INSTANCE = new SquareRenderer();
@Override
public BufferedImage render(Skin skin, ISkinPart.Vanilla part, boolean renderOverlays, int size) {
double scale = size / 8D;
BufferedImage partImage = getVanillaSkinPart(skin, part, scale, renderOverlays); // Get the part image
if (!renderOverlays) { // Not rendering overlays
return partImage;
}
// Create a new image, draw our skin part texture, and then apply overlays
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
graphics.drawImage(partImage, 0, 0, null);
graphics.dispose();
return texture;
}
}

@ -0,0 +1,54 @@
package xyz.mcutils.backend.config;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
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(topic = "Config")
@Configuration
public class Config {
public static Config INSTANCE;
@Autowired
private Environment environment;
@Value("${public-url}")
private String webPublicUrl;
@PostConstruct
public void onInitialize() {
INSTANCE = this;
}
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
@Bean
public WebMvcConfigurer configureCors() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
// Allow all origins to access the API
registry.addMapping("/**")
.allowedOrigins("*") // Allow all origins
.allowedMethods("*") // Allow all methods
.allowedHeaders("*"); // Allow all headers
}
};
}
}

@ -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");
}
}

@ -0,0 +1,47 @@
package xyz.mcutils.backend.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class OpenAPIConfiguration {
/**
* The build properties of the
* app, null if the app is not built.
*/
private final BuildProperties buildProperties;
@Autowired
public OpenAPIConfiguration(BuildProperties buildProperties) {
this.buildProperties = buildProperties;
}
@Bean
public OpenAPI defineOpenAPI() {
Server server = new Server();
server.setUrl(Config.INSTANCE.getWebPublicUrl());
Contact contact = new Contact();
contact.setName("Liam");
contact.setEmail("liam@fascinated.cc");
contact.setUrl("https://fascinated.cc");
Info info = new Info();
info.setTitle("Minecraft Utilities API");
info.setVersion(buildProperties == null ? "N/A" : buildProperties.getVersion());
info.setDescription("Wrapper for the Minecraft APIs to make them easier to use.");
info.setContact(contact);
info.setLicense(new License().name("MIT License").url("https://opensource.org/licenses/MIT"));
return new OpenAPI().servers(List.of(server)).info(info);
}
}

@ -0,0 +1,75 @@
package xyz.mcutils.backend.config;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
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.
*/
@Value("${spring.data.redis.host}")
private String host;
/**
* The Redis server port.
*/
@Value("${spring.data.redis.port}")
private int port;
/**
* The Redis database index.
*/
@Value("${spring.data.redis.database}")
private int database;
/**
* The optional Redis password.
*/
@Value("${spring.data.redis.auth}")
private String auth;
/**
* Build the config to use for Redis.
*
* @return the config
* @see RedisTemplate for config
*/
@Bean @NonNull
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
/**
* Build the connection factory to use
* when making connections to Redis.
*
* @return the built factory
* @see JedisConnectionFactory for factory
*/
@Bean @NonNull
public JedisConnectionFactory jedisConnectionFactory() {
log.info("Connecting to Redis at {}:{}/{}", host, port, database);
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
config.setDatabase(database);
if (!auth.trim().isEmpty()) { // Auth with our provided password
log.info("Using auth...");
config.setPassword(auth);
}
return new JedisConnectionFactory(config);
}
}

@ -0,0 +1,20 @@
package xyz.mcutils.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Map;
@Controller
@RequestMapping(value = "/")
public class HealthController {
@GetMapping(value = "/health")
public ResponseEntity<?> home() {
return ResponseEntity.ok(Map.of(
"status", "OK"
));
}
}

@ -0,0 +1,28 @@
package xyz.mcutils.backend.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import xyz.mcutils.backend.config.Config;
@Controller
@RequestMapping(value = "/")
public class HomeController {
private final String examplePlayer = "Notch";
private final String exampleJavaServer = "aetheria.cc";
private final String exampleBedrockServer = "geo.hivebedrock.network";
@GetMapping(value = "/")
public String home(Model model) {
String publicUrl = Config.INSTANCE.getWebPublicUrl();
model.addAttribute("public_url", publicUrl);
model.addAttribute("player_example_url", publicUrl + "/player/" + examplePlayer);
model.addAttribute("java_server_example_url", publicUrl + "/server/java/" + exampleJavaServer);
model.addAttribute("bedrock_server_example_url", publicUrl + "/server/bedrock/" + exampleBedrockServer);
model.addAttribute("mojang_endpoint_status_url", publicUrl + "/mojang/status");
model.addAttribute("swagger_url", publicUrl + "/swagger-ui.html");
return "index";
}
}

@ -0,0 +1,32 @@
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;
import org.springframework.web.bind.annotation.RestController;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
import xyz.mcutils.backend.service.MojangService;
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 ResponseEntity<?> getStatus() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
.body(Map.of("endpoints", mojangService.getMojangServerStatus()));
}
}

@ -0,0 +1,70 @@
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;
import org.springframework.web.bind.annotation.*;
import xyz.mcutils.backend.model.cache.CachedPlayer;
import xyz.mcutils.backend.model.cache.CachedPlayerName;
import xyz.mcutils.backend.model.player.Player;
import xyz.mcutils.backend.service.PlayerService;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = "/player/")
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
public class PlayerController {
private final PlayerService playerService;
@Autowired
public PlayerController(PlayerService playerManagerService) {
this.playerService = playerManagerService;
}
@ResponseBody
@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.maxAge(1, TimeUnit.HOURS).cachePublic())
.body(player);
}
@ResponseBody
@GetMapping(value = "/uuid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CachedPlayerName> getPlayerUuid(
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
CachedPlayerName player = playerService.usernameToUuid(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePublic())
.body(player);
}
@GetMapping(value = "/{part}/{id}")
public ResponseEntity<?> getPlayerHead(
@Parameter(description = "The part of the skin", example = "head") @PathVariable String part,
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id,
@Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size,
@Parameter(description = "Whether to render the skin overlay (skin layers)", example = "false") @RequestParam(required = false, defaultValue = "false") boolean overlays,
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
CachedPlayer cachedPlayer = playerService.getPlayer(id);
Player player = cachedPlayer.getPlayer();
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
// Return the part image
return ResponseEntity.ok()
.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());
}
}

@ -0,0 +1,86 @@
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;
import org.springframework.web.bind.annotation.*;
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
import xyz.mcutils.backend.service.MojangService;
import xyz.mcutils.backend.service.ServerService;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@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;
private final MojangService mojangService;
@Autowired
public ServerController(ServerService serverService, MojangService mojangService) {
this.serverService = serverService;
this.mojangService = mojangService;
}
@ResponseBody
@GetMapping(value = "/{platform}/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
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) {
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<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(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()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
.body(Map.of(
"blocked", mojangService.isServerBlocked(hostname)
));
}
}

@ -1,12 +1,14 @@
package cc.fascinated.exception; package xyz.mcutils.backend.exception;
import cc.fascinated.model.response.Response;
import io.micrometer.common.lang.NonNull; import io.micrometer.common.lang.NonNull;
import io.sentry.Sentry;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import xyz.mcutils.backend.model.response.ErrorResponse;
@ControllerAdvice @ControllerAdvice
public final class ExceptionControllerAdvice { public final class ExceptionControllerAdvice {
@ -19,7 +21,12 @@ public final class ExceptionControllerAdvice {
*/ */
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(@NonNull Exception ex) { public ResponseEntity<?> handleException(@NonNull Exception ex) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; // Get the HTTP status HttpStatus status = null; // Get the HTTP status
if (ex instanceof NoResourceFoundException) { // Not found
status = HttpStatus.NOT_FOUND;
} else if (ex instanceof UnsupportedOperationException) { // Not implemented
status = HttpStatus.NOT_IMPLEMENTED;
}
if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation
status = ex.getClass().getAnnotation(ResponseStatus.class).value(); status = ex.getClass().getAnnotation(ResponseStatus.class).value();
} }
@ -27,7 +34,14 @@ public final class ExceptionControllerAdvice {
if (message == null) { // Fallback if (message == null) { // Fallback
message = "An internal error has occurred."; message = "An internal error has occurred.";
} }
ex.printStackTrace(); // Print the stack trace // Print the stack trace if no response status is present
return new Response(status, message).toResponseEntity(); // Return the error response if (status == null) {
ex.printStackTrace();
}
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);
} }
} }

@ -0,0 +1,12 @@
package xyz.mcutils.backend.exception.impl;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}

@ -0,0 +1,12 @@
package xyz.mcutils.backend.exception.impl;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class InternalServerErrorException extends RuntimeException {
public InternalServerErrorException(String message) {
super(message);
}
}

@ -0,0 +1,9 @@
package xyz.mcutils.backend.exception.impl;
public class MojangAPIRateLimitException extends RateLimitException {
public MojangAPIRateLimitException() {
super("Mojang API rate limit exceeded. Please try again later.");
}
}

@ -0,0 +1,12 @@
package xyz.mcutils.backend.exception.impl;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
}

@ -0,0 +1,9 @@
package xyz.mcutils.backend.exception.impl;
import lombok.experimental.StandardException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@StandardException
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException { }

@ -1,35 +1,39 @@
package cc.fascinated.log; package xyz.mcutils.backend.log;
import cc.fascinated.common.IPUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import xyz.mcutils.backend.common.IPUtils;
import xyz.mcutils.backend.service.MetricService;
import xyz.mcutils.backend.service.metric.metrics.RequestsPerRouteMetric;
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
import java.util.Arrays; import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ControllerAdvice @ControllerAdvice
@Slf4j(topic = "Req/Res Transaction") @Slf4j(topic = "Req Transaction")
public class TransactionLogger implements ResponseBodyAdvice<Object> { public class TransactionLogger implements ResponseBodyAdvice<Object> {
@Autowired
private MetricService metricService;
@Override @Override
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest, @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest,
@NonNull ServerHttpResponse rawResponse) { @NonNull ServerHttpResponse rawResponse) {
HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest(); HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest();
HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse();
// Get the request ip ip // Get the request ip ip
String ip = IPUtils.getRealIp(request); String ip = IPUtils.getRealIp(request);
@ -40,34 +44,17 @@ public class TransactionLogger implements ResponseBodyAdvice<Object> {
params.put(entry.getKey(), Arrays.toString(entry.getValue())); params.put(entry.getKey(), Arrays.toString(entry.getValue()));
} }
// Getting headers // Logging the request
Map<String, String> headers = new HashMap<>(); log.info(String.format("[Req] %s | %s | '%s', params=%s",
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headers.put(headerName, request.getHeader(headerName));
}
// Log the request
log.info(String.format("[Req] %s | %s | '%s', params=%s, headers=%s",
request.getMethod(), request.getMethod(),
ip, ip,
request.getRequestURI(), request.getRequestURI(),
params, params
headers
)); ));
// Getting response headers // Increment the metric
headers = new HashMap<>(); ((TotalRequestsMetric) metricService.getMetric(TotalRequestsMetric.class)).increment();
for (String headerName : response.getHeaderNames()) { ((RequestsPerRouteMetric) metricService.getMetric(RequestsPerRouteMetric.class)).increment(request.getRequestURI());
headers.put(headerName, response.getHeader(headerName));
}
// Log the response
log.info(String.format("[Res] %s, headers=%s",
response.getStatus(),
headers
));
return body; return body;
} }

@ -0,0 +1,49 @@
package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
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;
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 @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
public class CachedEndpointStatus extends CachedResponse implements Serializable {
/**
* The id for this endpoint cache.
*/
@Id @NonNull @JsonIgnore
private final String id;
/**
* The endpoint cache.
*/
private final List<Map<String, Object>> endpoints;
public CachedEndpointStatus(@NonNull String id, Map<MojangServer, MojangServer.Status> mojangServers) {
super(Cache.defaultCache());
this.id = id;
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);
}
}
}

@ -0,0 +1,39 @@
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 org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
import xyz.mcutils.backend.model.server.MinecraftServer;
import java.io.Serializable;
/**
* @author Braydon
*/
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
public class CachedMinecraftServer extends CachedResponse implements Serializable {
/**
* The id of this cached server.
*/
@Id @NonNull @JsonIgnore
private String id;
/**
* The cached server.
*/
@NonNull @JsonUnwrapped
private MinecraftServer server;
public CachedMinecraftServer(@NonNull String id, @NonNull MinecraftServer server) {
super(CachedResponse.Cache.defaultCache());
this.id = id;
this.server = server;
}
}

@ -0,0 +1,41 @@
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.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
import xyz.mcutils.backend.model.player.Player;
import java.io.Serializable;
import java.util.UUID;
/**
* A cacheable {@link Player}.
*
* @author Braydon
*/
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
public class CachedPlayer extends CachedResponse implements Serializable {
/**
* The unique id of the player.
*/
@JsonIgnore
@Id private UUID uniqueId;
/**
* The player to cache.
*/
@JsonUnwrapped
private Player player;
public CachedPlayer(UUID uniqueId, Player player) {
super(Cache.defaultCache());
this.uniqueId = uniqueId;
this.player = player;
}
}

@ -0,0 +1,42 @@
package xyz.mcutils.backend.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
import java.util.UUID;
/**
* @author Braydon
*/
@Setter
@Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "playerName", timeToLive = 60L * 60L * 6) // 6 hours (in seconds)
public class CachedPlayerName extends CachedResponse {
/**
* The id of the player.
*/
@JsonIgnore
@Id private final String id;
/**
* The username of the player.
*/
private final String username;
/**
* The unique id of the player.
*/
private final UUID uniqueId;
public CachedPlayerName(String id, String username, UUID uniqueId) {
super(Cache.defaultCache());
this.id = id;
this.username = username;
this.uniqueId = uniqueId;
}
}

@ -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 = "playerSkinPart", timeToLive = 60L * 60L) // 1 hour (in seconds)
public class CachedPlayerSkinPart {
/**
* The ID of the skin part
*/
@Id @NonNull private String id;
/**
* The skin part bytes
*/
private byte[] bytes;
}

@ -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;
}

@ -0,0 +1,26 @@
package xyz.mcutils.backend.model.dns;
import io.micrometer.common.lang.NonNull;
import lombok.*;
@NoArgsConstructor @AllArgsConstructor
@Setter @Getter @EqualsAndHashCode
public abstract class DNSRecord {
/**
* The type of this record.
*/
@NonNull
private Type type;
/**
* The TTL (Time To Live) of this record.
*/
private long ttl;
/**
* Types of a record.
*/
public enum Type {
A, SRV
}
}

@ -0,0 +1,24 @@
package xyz.mcutils.backend.model.dns.impl;
import io.micrometer.common.lang.NonNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import xyz.mcutils.backend.model.dns.DNSRecord;
import java.net.InetAddress;
@Setter @Getter
@NoArgsConstructor
public final class ARecord extends DNSRecord {
/**
* The address of this record, null if unresolved.
*/
private String address;
public ARecord(@NonNull org.xbill.DNS.ARecord bootstrap) {
super(Type.A, bootstrap.getTTL());
InetAddress address = bootstrap.getAddress();
this.address = address == null ? null : address.getHostAddress();
}
}

@ -0,0 +1,53 @@
package xyz.mcutils.backend.model.dns.impl;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.micrometer.common.lang.NonNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import xyz.mcutils.backend.model.dns.DNSRecord;
import java.net.InetSocketAddress;
@Setter @Getter
@NoArgsConstructor
public final class SRVRecord extends DNSRecord {
/**
* The priority of this record.
*/
private int priority;
/**
* The weight of this record.
*/
private int weight;
/**
* The port of this record.
*/
private int port;
/**
* The target of this record.
*/
@NonNull private String target;
public SRVRecord(@NonNull org.xbill.DNS.SRVRecord bootstrap) {
super(Type.SRV, bootstrap.getTTL());
priority = bootstrap.getPriority();
weight = bootstrap.getWeight();
port = bootstrap.getPort();
target = bootstrap.getTarget().toString().replaceFirst("\\.$", "");
}
/**
* Get a socket address from
* the target and port.
*
* @return the socket address
*/
@NonNull @JsonIgnore
public InetSocketAddress getSocketAddress() {
return new InetSocketAddress(target, port);
}
}

@ -0,0 +1,15 @@
package xyz.mcutils.backend.model.metric;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Map;
@AllArgsConstructor
@Getter
public class WebsocketMetrics {
/**
* The metrics to send to the client.
*/
private final Map<String, Object> metrics;
}

@ -0,0 +1,47 @@
package xyz.mcutils.backend.model.mojang;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@RequiredArgsConstructor
@Getter @Setter @EqualsAndHashCode
public class EndpointStatus {
/**
* The name of the service.
*/
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.
*/
ONLINE,
/**
* The service is online, but may be experiencing issues.
* This could be due to high load or other issues.
*/
DEGRADED,
/**
* The service is offline and not operational.
*/
OFFLINE
}
}

@ -1,10 +1,12 @@
package cc.fascinated.model.player; package xyz.mcutils.backend.model.player;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@Getter @AllArgsConstructor @AllArgsConstructor
@Getter @EqualsAndHashCode
public class Cape { public class Cape {
/** /**

@ -0,0 +1,63 @@
package xyz.mcutils.backend.model.player;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import xyz.mcutils.backend.common.Tuple;
import xyz.mcutils.backend.common.UUIDUtils;
import xyz.mcutils.backend.model.skin.Skin;
import xyz.mcutils.backend.model.token.MojangProfileToken;
import java.util.UUID;
@AllArgsConstructor @NoArgsConstructor
@Getter @EqualsAndHashCode
public class Player {
/**
* The UUID of the player
*/
private UUID uniqueId;
/**
* The trimmed UUID of the player
*/
private String trimmedUniqueId;
/**
* The username of the player
*/
private String username;
/**
* The skin of the player, null if the
* player does not have a skin
*/
private Skin skin;
/**
* The cape of the player, null if the
* player does not have a cape
*/
private Cape cape;
/**
* The raw properties of the player
*/
private MojangProfileToken.ProfileProperty[] rawProperties;
public Player(MojangProfileToken profile) {
this.uniqueId = UUIDUtils.addDashes(profile.getId());
this.trimmedUniqueId = UUIDUtils.removeDashes(this.uniqueId);
this.username = profile.getName();
this.rawProperties = profile.getProperties();
// Get the skin and cape
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();
if (skinAndCape != null) {
this.skin = skinAndCape.getLeft();
this.cape = skinAndCape.getRight();
}
}
}

@ -0,0 +1,40 @@
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 @EqualsAndHashCode
public class ErrorResponse {
/**
* The status code of this error.
*/
@NonNull
private final HttpStatus status;
/**
* The HTTP code of this error.
*/
private final int code;
/**
* The message of this error.
*/
@NonNull private final String message;
/**
* The timestamp this error occurred.
*/
@NonNull private final Date timestamp;
public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) {
this.status = status;
code = status.value();
this.message = message;
timestamp = new Date();
}
}

@ -0,0 +1,125 @@
package xyz.mcutils.backend.model.server;
import lombok.*;
import xyz.mcutils.backend.model.dns.DNSRecord;
/**
* A Bedrock edition {@link MinecraftServer}.
*
* @author Braydon
*/
@Getter @ToString(callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
public final class BedrockMinecraftServer extends MinecraftServer {
/**
* The unique ID of this server.
*/
@EqualsAndHashCode.Include @NonNull private final String id;
/**
* The edition of this server.
*/
@NonNull private final Edition edition;
/**
* The version information of this server.
*/
@NonNull private final Version version;
/**
* The gamemode of this server.
*/
@NonNull private final GameMode gamemode;
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, GeoLocation location) {
super(hostname, ip, port, records, motd, players, location);
this.id = id;
this.edition = edition;
this.version = version;
this.gamemode = gamemode;
}
/**
* Create a new Bedrock Minecraft server.
* <p>
* <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Token Format</a>
* </p>
*
* @param hostname the hostname of the server
* @param ip the IP address of the server
* @param port the port of the server
* @param token the status token
* @return the Bedrock Minecraft server
*/
@NonNull
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(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],
hostname,
ip,
port,
records,
edition,
version,
players,
motd,
gameMode,
location
);
}
/**
* The edition of a Bedrock server.
*/
@AllArgsConstructor @Getter
public enum Edition {
/**
* Minecraft: Pocket Edition.
*/
MCPE,
/**
* Minecraft: Education Edition.
*/
MCEE
}
/**
* Version information for a server.
*/
@AllArgsConstructor @Getter @ToString
public static class Version {
/**
* The protocol version of the server.
*/
private final int protocol;
/**
* The version name of the server.
*/
@NonNull private final String name;
}
/**
* The gamemode of a server.
*/
@AllArgsConstructor @Getter @ToString
public static class GameMode {
/**
* The name of this gamemode.
*/
@NonNull private final String name;
/**
* The numeric of this gamemode.
*/
private final int numericId;
}
}

@ -0,0 +1,272 @@
package xyz.mcutils.backend.model.server;
import com.google.gson.annotations.SerializedName;
import lombok.*;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.JavaMinecraftVersion;
import xyz.mcutils.backend.common.ServerUtils;
import xyz.mcutils.backend.config.Config;
import xyz.mcutils.backend.model.dns.DNSRecord;
import xyz.mcutils.backend.model.token.JavaServerStatusToken;
/**
* @author Braydon
*/
@Setter @Getter @EqualsAndHashCode(callSuper = false)
public final class JavaMinecraftServer extends MinecraftServer {
/**
* The version of the server.
*/
@NonNull private final Version version;
/**
* The favicon of the server.
*/
private Favicon favicon;
/**
* The mods running on this server.
*/
private ForgeModInfo modInfo;
/**
* The mods running on this server.
* <p>
* This is only used for servers
* running 1.13 and above.
* </p>
*/
private ForgeData forgeData;
/**
* Whether the server prevents chat reports.
*/
private boolean preventsChatReports;
/**
* Whether the server enforces secure chat.
*/
private boolean enforcesSecureChat;
/**
* Whether the server has previews chat enabled.
* <p>
* Chat Preview sends chat messages to the server as they are typed, even before they're sent.
* <a href="https://www.minecraft.net/es-mx/article/minecraft-snapshot-22w19a">More information</a>
* </p>
*/
private boolean previewsChat;
/**
* The mojang blocked status for the server.
*/
private boolean mojangBlocked;
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;
this.forgeData = forgeData;
this.preventsChatReports = preventsChatReports;
this.enforcesSecureChat = enforcesSecureChat;
this.previewsChat = previewsChat;
}
/**
* Create a new Java Minecraft server.
*
* @param hostname the hostname of the server
* @param ip the IP address of the server
* @param port the port of the server
* @param token the status token
* @return the Java Minecraft server
*/
@NonNull
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();
}
return new JavaMinecraftServer(
hostname,
ip,
port,
MinecraftServer.MOTD.create(hostname, Platform.JAVA, motdString),
token.getPlayers(),
location,
records,
token.getVersion().detailedCopy(),
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)),
token.getModInfo(),
token.getForgeData(),
token.isPreventsChatReports(),
token.isEnforcesSecureChat(),
token.isPreviewsChat()
);
}
@AllArgsConstructor @Getter
public static class Version {
/**
* The version name of the server.
*/
@NonNull
private final String name;
/**
* The server platform.
*/
private String platform;
/**
* The protocol version.
*/
private final int protocol;
/**
* The name of the protocol, null if not found.
*/
private final String protocolName;
/**
* Create a more detailed
* copy of this object.
*
* @return the detailed copy
*/
@NonNull
public Version detailedCopy() {
String platform = null;
if (name.contains(" ")) { // Parse the server platform
String[] split = name.split(" ");
if (split.length == 2) {
platform = split[0];
}
}
JavaMinecraftVersion minecraftVersion = JavaMinecraftVersion.byProtocol(protocol);
return new Version(name, platform, protocol, minecraftVersion == null ? null : minecraftVersion.getName());
}
}
@Getter @AllArgsConstructor
public static class Favicon {
/**
* The raw base64 of the favicon.
*/
private final String base64;
/**
* The url to the favicon.
*/
private String url;
/**
* Create a new favicon for a server.
*
* @param base64 the base64 of the favicon
* @param address the address of the server
* @return the new favicon
*/
public static Favicon create(String base64, @NonNull String address) {
if (base64 == null) { // The server doesn't have a favicon
return null;
}
return new Favicon(base64, Config.INSTANCE.getWebPublicUrl() + "/server/icon/%s".formatted(address));
}
}
/**
* Forge mod information for a server.
*/
@AllArgsConstructor @Getter @ToString
public static class ForgeModInfo {
/**
* The type of modded server this is.
*/
@NonNull private final String type;
/**
* The list of mods on this server, null or empty if none.
*/
private final ForgeMod[] modList;
/**
* A forge mod for a server.
*/
@AllArgsConstructor @Getter @ToString
private static class ForgeMod {
/**
* The id of this mod.
*/
@NonNull @SerializedName("modid") private final String name;
/**
* The version of this mod.
*/
private final String version;
}
}
@AllArgsConstructor @Getter
public static class ForgeData {
/**
* The list of mod channels on this server, null or empty if none.
*/
private final Channel[] channels;
/**
* The list of mods on this server, null or empty if none.
*/
private final Mod[] mods;
/**
* Whether the mod list is truncated.
*/
private final boolean truncated;
/**
* The version of the FML network.
*/
private final int fmlNetworkVersion;
@AllArgsConstructor @Getter
public static class Channel {
/**
* The id of this mod channel.
*/
@NonNull @SerializedName("res") private final String name;
/**
* The version of this mod channel.
*/
private final String version;
/**
* Whether this mod channel is required to join.
*/
private boolean required;
}
@AllArgsConstructor @Getter
public static class Mod {
/**
* The id of this mod.
*/
@NonNull @SerializedName("modId") private final String name;
/**
* The version of this mod.
*/
@SerializedName("modmarker") private final String version;
}
}
}

@ -0,0 +1,214 @@
package xyz.mcutils.backend.model.server;
import com.maxmind.geoip2.model.CityResponse;
import io.micrometer.common.lang.NonNull;
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;
import xyz.mcutils.backend.service.pinger.impl.JavaMinecraftServerPinger;
import java.util.Arrays;
import java.util.UUID;
/**
* @author Braydon
*/
@AllArgsConstructor
@Getter @Setter @EqualsAndHashCode
public class MinecraftServer {
/**
* The hostname of the server.
*/
private final String hostname;
/**
* The IP address of the server.
*/
private final String ip;
/**
* The port of the server.
*/
private final int port;
/**
* The DNS records for the server.
*/
private final DNSRecord[] records;
/**
* The motd for the server.
*/
private final MOTD motd;
/**
* The players on the server.
*/
private final Players players;
/**
* The location of the server.
*/
private final GeoLocation location;
/**
* A platform a Minecraft
* server can operate on.
*/
@AllArgsConstructor @Getter
public enum Platform {
/**
* The Java edition of Minecraft.
*/
JAVA(new JavaMinecraftServerPinger(), 25565),
/**
* The Bedrock edition of Minecraft.
*/
BEDROCK(new BedrockMinecraftServerPinger(), 19132);
/**
* The server pinger for this platform.
*/
@NonNull
private final MinecraftServerPinger<?> pinger;
/**
* The default server port for this platform.
*/
private final int defaultPort;
}
@AllArgsConstructor @Getter
public static class MOTD {
/**
* The raw motd lines
*/
private final String[] raw;
/**
* The clean motd lines
*/
private final String[] clean;
/**
* The html motd lines
*/
private final String[] html;
/**
* The URL to the server preview image.
*/
private final String preview;
/**
* Create a new MOTD from a raw string.
*
* @param raw the raw motd string
* @return the new motd
*/
@NonNull
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),
Config.INSTANCE.getWebPublicUrl() + "/server/%s/preview/%s".formatted(
platform.name().toLowerCase(),hostname)
);
}
}
/**
* Player count data for a server.
*/
@AllArgsConstructor @Getter
public static class Players {
/**
* The online players on this server.
*/
private final int online;
/**
* The maximum allowed players on this server.
*/
private final int max;
/**
* A sample of players on this server, null or empty if no sample.
*/
private final Sample[] sample;
/**
* A sample player.
*/
@AllArgsConstructor @Getter @ToString
public static class Sample {
/**
* The unique id of this player.
*/
@NonNull private final UUID id;
/**
* The name of this player.
*/
@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()
);
}
}
}

@ -0,0 +1,207 @@
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.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;
public interface ISkinPart {
Enum<?>[][] TYPES = { Vanilla.values(), Custom.values() };
/**
* The name of the part.
*
* @return the part name
*/
String name();
/**
* Should this part be hidden from the
* player skin part urls list?
*
* @return whether this part should be hidden
*/
boolean hidden();
/**
* Renders the skin part for the skin.
*
* @param skin the skin
* @param renderOverlays should the overlays be rendered
* @param size the size of the part
* @return the rendered skin part
*/
BufferedImage render(Skin skin, boolean renderOverlays, int size);
/**
* Get a skin part by the given name.
*
* @param name the name of the part
* @return the part, null if none
*/
static ISkinPart getByName(String name) {
name = name.toUpperCase();
for (Enum<?>[] type : TYPES) {
for (Enum<?> part : type) {
if (!part.name().equals(name)) {
continue;
}
return (ISkinPart) part;
}
}
return null;
}
/**
* The vanilla skin parts.
* <p>
* <a href="https://cdn.fascinated.cc/sXwEKAxm.png">Skin Format</a>
* </p>
*/
@Getter
enum Vanilla implements ISkinPart {
// Overlays
HEAD_OVERLAY_TOP(true, new Coordinates(40, 0), 8, 8),
HEAD_OVERLAY_FACE(true, new Coordinates(40, 8), 8, 8),
HEAD_OVERLAY_LEFT(true, new Coordinates(48, 8), 8, 8),
// Head
HEAD_TOP(true, new Coordinates(8, 0), 8, 8, HEAD_OVERLAY_TOP),
FACE(false, new Coordinates(8, 8), 8, 8, HEAD_OVERLAY_FACE),
HEAD_LEFT(true, new Coordinates(0, 8), 8, 8, HEAD_OVERLAY_LEFT),
HEAD_RIGHT(true, new Coordinates(16, 8), 8, 8),
HEAD_BOTTOM(true, new Coordinates(16, 0), 8, 8),
HEAD_BACK(true, new Coordinates(24, 8), 8, 8),
// Body
BODY_FRONT(true, new Coordinates(20, 20), 8, 12),
// Arms
LEFT_ARM_TOP(true, new Coordinates(36, 48), 4, 4),
RIGHT_ARM_TOP(true, new Coordinates(44, 16), 4, 4),
LEFT_ARM_FRONT(true, new Coordinates(44, 20), 4, 12),
RIGHT_ARM_FRONT(true, new Coordinates(36, 52), new LegacyCoordinates(44, 20, true), 4, 12),
// Legs
LEFT_LEG_FRONT(true, new Coordinates(4, 20), 4, 12), // Front
RIGHT_LEG_FRONT(true, new Coordinates(20, 52), new LegacyCoordinates(4, 20, true), 4, 12); // Front
/**
* Should this part be hidden from the
* player skin part urls list?
*/
private final boolean hidden;
/**
* The coordinates of the part.
*/
private final Coordinates coordinates;
/**
* The legacy coordinates of the part.
*/
private final LegacyCoordinates legacyCoordinates;
/**
* The width and height of the part.
*/
private final int width, height;
/**
* The overlays of the part.
*/
private final Vanilla[] overlays;
Vanilla(boolean hidden, Coordinates coordinates, int width, int height, Vanilla... overlays) {
this(hidden, coordinates, null, width, height, overlays);
}
Vanilla(boolean hidden, Coordinates coordinates, LegacyCoordinates legacyCoordinates, int width, int height, Vanilla... overlays) {
this.hidden = hidden;
this.coordinates = coordinates;
this.legacyCoordinates = legacyCoordinates;
this.width = width;
this.height = height;
this.overlays = overlays;
}
@Override
public boolean hidden() {
return this.isHidden();
}
@Override
public BufferedImage render(Skin skin, boolean renderOverlays, int size) {
return SquareRenderer.INSTANCE.render(skin, this, renderOverlays, size);
}
/**
* Is this part a front arm?
*
* @return whether this part is a front arm
*/
public boolean isFrontArm() {
return this == LEFT_ARM_FRONT || this == RIGHT_ARM_FRONT;
}
/**
* Does this part have legacy coordinates?
*
* @return whether this part has legacy coordinates
*/
public boolean hasLegacyCoordinates() {
return legacyCoordinates != null;
}
@AllArgsConstructor @Getter
public static class Coordinates {
/**
* The X and Y position of the part.
*/
private final int x, y;
}
@Getter
public static class LegacyCoordinates extends Coordinates {
/**
* Should the part be flipped horizontally?
*/
private final boolean flipped;
public LegacyCoordinates(int x, int y) {
this(x, y, false);
}
public LegacyCoordinates(int x, int y, boolean flipped) {
super(x, y);
this.flipped = flipped;
}
}
}
@AllArgsConstructor @Getter
enum Custom implements ISkinPart {
HEAD(IsometricHeadRenderer.INSTANCE),
BODY(BodyRenderer.INSTANCE);
/**
* The renderer to use for this part
*/
private final SkinRenderer<Custom> renderer;
@Override
public boolean hidden() {
return false;
}
@Override
public BufferedImage render(Skin skin, boolean renderOverlays, int size) {
return renderer.render(skin, this, renderOverlays, size);
}
}
}

@ -0,0 +1,108 @@
package xyz.mcutils.backend.model.skin;
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;
import xyz.mcutils.backend.common.EnumUtils;
import xyz.mcutils.backend.common.PlayerUtils;
import xyz.mcutils.backend.config.Config;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor @NoArgsConstructor
@Getter @Log4j2(topic = "Skin") @EqualsAndHashCode
public class Skin {
/**
* The URL for the skin
*/
private String url;
/**
* The model for the skin
*/
private Model model;
/**
* The legacy status of the skin
*/
private boolean legacy;
/**
* The skin image for the skin
*/
@JsonIgnore
private byte[] skinImage;
/**
* The part URLs of the skin
*/
@JsonProperty("parts")
private Map<String, String> partUrls = new HashMap<>();
public Skin(String url, Model model) {
this.url = url;
this.model = model;
this.skinImage = PlayerUtils.getSkinImage(url);
if (this.skinImage != null) {
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.skinImage));
this.legacy = image.getWidth() == 64 && image.getHeight() == 32;
} catch (Exception ignored) {}
}
}
/**
* Gets the skin from a {@link JsonObject}.
*
* @param json the JSON object
* @return the skin
*/
public static Skin fromJson(JsonObject json) {
if (json == null) {
return null;
}
String url = json.get("url").getAsString();
JsonObject metadata = json.getAsJsonObject("metadata");
return new Skin(
url,
EnumUtils.getEnumConstant(Model.class, metadata != null ? metadata.get("model").getAsString().toUpperCase() : "DEFAULT")
);
}
/**
* Populates the part URLs for the skin.
*
* @param playerUuid the player's UUID
*/
public Skin populatePartUrls(String playerUuid) {
for (Enum<?>[] type : ISkinPart.TYPES) {
for (Enum<?> part : type) {
ISkinPart skinPart = (ISkinPart) part;
if (skinPart.hidden()) {
continue;
}
String partName = part.name().toLowerCase();
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid);
}
}
return this;
}
/**
* The model of the skin.
*/
public enum Model {
DEFAULT,
SLIM
}
}

@ -0,0 +1,69 @@
package xyz.mcutils.backend.model.token;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
/**
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public final class JavaServerStatusToken {
/**
* The version of the server.
*/
private final JavaMinecraftServer.Version version;
/**
* The players on the server.
*/
private final MinecraftServer.Players players;
/**
* The mods running on this server.
*/
@SerializedName("modinfo")
private JavaMinecraftServer.ForgeModInfo modInfo;
/**
* The mods running on this server.
* <p>
* This is only used for servers
* running 1.13 and above.
* </p>
*/
private JavaMinecraftServer.ForgeData forgeData;
/**
* The motd of the server.
*/
private final Object description;
/**
* The favicon of the server.
*/
private final String favicon;
/**
* Whether the server prevents chat reports.
*/
private boolean preventsChatReports;
/**
* Whether the server enforces secure chat.
*/
private boolean enforcesSecureChat;
/**
* Whether the server has previews chat enabled.
* <p>
* Chat Preview sends chat messages to the server as they are typed, even before they're sent.
* <a href="https://www.minecraft.net/es-mx/article/minecraft-snapshot-22w19a">More information</a>
* </p>
*/
private boolean previewsChat;
}

@ -1,21 +1,20 @@
package cc.fascinated.model.mojang; package xyz.mcutils.backend.model.token;
import cc.fascinated.Main; import com.fasterxml.jackson.annotation.JsonIgnore;
import cc.fascinated.common.Tuple;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Skin;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.Tuple;
import xyz.mcutils.backend.common.UUIDUtils;
import xyz.mcutils.backend.model.player.Cape;
import xyz.mcutils.backend.model.skin.Skin;
import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List;
@Getter @NoArgsConstructor @Getter @NoArgsConstructor @AllArgsConstructor
public class MojangProfile { public class MojangProfileToken {
/** /**
* The UUID of the player. * The UUID of the player.
@ -30,7 +29,7 @@ public class MojangProfile {
/** /**
* The properties of the player. * The properties of the player.
*/ */
private final List<ProfileProperty> properties = new ArrayList<>(); private ProfileProperty[] properties = new ProfileProperty[0];
/** /**
* Get the skin and cape of the player. * Get the skin and cape of the player.
@ -42,10 +41,7 @@ public class MojangProfile {
if (textureProperty == null) { if (textureProperty == null) {
return null; return null;
} }
JsonObject texturesJson = textureProperty.getDecodedValue().getAsJsonObject("textures"); // Parse the decoded JSON and get the texture object
JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property
JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()), return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
Cape.fromJson(texturesJson.getAsJsonObject("CAPE"))); Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
} }
@ -56,7 +52,7 @@ public class MojangProfile {
* @return the formatted UUID * @return the formatted UUID
*/ */
public String getFormattedUuid() { public String getFormattedUuid() {
return id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id; return id.length() == 32 ? UUIDUtils.addDashes(id).toString() : id;
} }
/** /**
@ -73,7 +69,7 @@ public class MojangProfile {
return null; return null;
} }
@Getter @AllArgsConstructor @Getter @NoArgsConstructor
public static class ProfileProperty { public static class ProfileProperty {
/** /**
* The name of the property. * The name of the property.
@ -95,8 +91,9 @@ public class MojangProfile {
* *
* @return the decoded value * @return the decoded value
*/ */
public String getDecodedValue() { @JsonIgnore
return new String(Base64.getDecoder().decode(this.value)); public JsonObject getDecodedValue() {
return Main.GSON.fromJson(new String(Base64.getDecoder().decode(this.value)), JsonObject.class);
} }
/** /**

@ -1,20 +1,23 @@
package cc.fascinated.model.mojang; package xyz.mcutils.backend.model.token;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Getter @NoArgsConstructor @Getter @NoArgsConstructor
public class MojangUsernameToUuid { public class MojangUsernameToUuidToken {
/** /**
* The UUID of the player. * The UUID of the player.
*/ */
private String id; @JsonProperty("id")
private String uuid;
/** /**
* The name of the player. * The name of the player.
*/ */
private String name; @JsonProperty("name")
private String username;
/** /**
* Check if the profile is valid. * Check if the profile is valid.
@ -22,6 +25,6 @@ public class MojangUsernameToUuid {
* @return if the profile is valid * @return if the profile is valid
*/ */
public boolean isValid() { public boolean isValid() {
return id != null && name != null; return uuid != null && username != null;
} }
} }

@ -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> { }

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

@ -0,0 +1,13 @@
package xyz.mcutils.backend.repository.redis;
import org.springframework.data.repository.CrudRepository;
import xyz.mcutils.backend.model.cache.CachedPlayer;
import java.util.UUID;
/**
* A cache repository for {@link CachedPlayer}'s.
*
* @author Braydon
*/
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }

Some files were not shown because too many files have changed in this diff Show More