From 1425e4e9b6a6107ab226394ce4a6c9b2547fb1c7 Mon Sep 17 00:00:00 2001 From: Janes Thomas Date: Wed, 2 Sep 2026 03:58:27 +0200 Subject: [PATCH 1/5] refactored ConnectionBuilder to set up new feature --- .../java/ch/yoinc/http/ConnectionBuilder.java | 45 ----------------- ...ection.java => ExternalApiConnection.java} | 49 +++++++++++++++---- .../ch/yoinc/services/CsStatsService.java | 11 +++-- .../ch/yoinc/services/DiscordService.java | 9 ++++ src/main/java/ch/yoinc/tasks/LeetifyTask.java | 36 ++++++-------- .../java/ch/yoinc/tasks/LeetifyTaskTest.java | 26 +++++----- 6 files changed, 83 insertions(+), 93 deletions(-) delete mode 100644 src/main/java/ch/yoinc/http/ConnectionBuilder.java rename src/main/java/ch/yoinc/http/{LeetifyConnection.java => ExternalApiConnection.java} (65%) diff --git a/src/main/java/ch/yoinc/http/ConnectionBuilder.java b/src/main/java/ch/yoinc/http/ConnectionBuilder.java deleted file mode 100644 index 4c1a849..0000000 --- a/src/main/java/ch/yoinc/http/ConnectionBuilder.java +++ /dev/null @@ -1,45 +0,0 @@ -package ch.yoinc.http; - -import com.google.gson.*; -import ch.yoinc.model.steam.ResponseData; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Properties; - -public class ConnectionBuilder { - - Properties properties; - - private final String STEAM_API = "https://api.steampowered.com"; - - public ConnectionBuilder(Properties properties) { - this.properties = properties; - } - - public ResponseData fetchSteamUserStats(String steamID) throws InterruptedException, IOException { - - HttpClient client = HttpClient.newHttpClient(); - - HttpRequest request; - ResponseData responseData; - - request = HttpRequest.newBuilder() - .uri(URI.create(STEAM_API + "/ISteamUser/GetPlayerSummaries/v0002/?key=" + properties.get("steam.api") + "&steamids=" + steamID)) - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - - responseData = new Gson().fromJson(response.body(), ResponseData.class); - - request = HttpRequest.newBuilder() - .uri(URI.create(STEAM_API + "/ISteamUserStats/GetUserStatsForGame/v0002/?key=" + properties.get("steam.api") + "&appid=730&steamid=" + steamID)) - .build(); - response = client.send(request, HttpResponse.BodyHandlers.ofString()); - responseData.setPlayerstats(new Gson().fromJson(response.body(), ResponseData.class).getPlayerstats()); - - return responseData; - } -} diff --git a/src/main/java/ch/yoinc/http/LeetifyConnection.java b/src/main/java/ch/yoinc/http/ExternalApiConnection.java similarity index 65% rename from src/main/java/ch/yoinc/http/LeetifyConnection.java rename to src/main/java/ch/yoinc/http/ExternalApiConnection.java index ba84869..c16778c 100644 --- a/src/main/java/ch/yoinc/http/LeetifyConnection.java +++ b/src/main/java/ch/yoinc/http/ExternalApiConnection.java @@ -2,9 +2,8 @@ import ch.yoinc.model.leetify.LeetifyMatchResponse; import ch.yoinc.model.leetify.LeetifyProfileResponse; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; +import com.google.gson.*; +import ch.yoinc.model.steam.ResponseData; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; @@ -21,15 +20,17 @@ import java.util.List; import java.util.Properties; -public class LeetifyConnection { +public class ExternalApiConnection { private final String apiKey; private final HttpClient client; private final Gson gson; + private final Properties properties; private final String LEETIFY_API = "https://api-public.cs-prod.leetify.com"; + private final String STEAM_API = "https://api.steampowered.com"; - - public LeetifyConnection(Properties properties) { + public ExternalApiConnection(Properties properties) { + this.properties = properties; this.apiKey = properties.getProperty("leetify.apiToken"); gson = new GsonBuilder() .registerTypeAdapter(Instant.class, new TypeAdapter() { @@ -55,6 +56,29 @@ public Instant read(JsonReader in) throws IOException { this.client = HttpClient.newHttpClient(); } + public ResponseData fetchSteamUserStats(String steamID) throws InterruptedException, IOException { + + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest request; + ResponseData responseData; + + request = HttpRequest.newBuilder() + .uri(URI.create(STEAM_API + "/ISteamUser/GetPlayerSummaries/v0002/?key=" + properties.get("steam.api") + "&steamids=" + steamID)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + responseData = new Gson().fromJson(response.body(), ResponseData.class); + + request = HttpRequest.newBuilder() + .uri(URI.create(STEAM_API + "/ISteamUserStats/GetUserStatsForGame/v0002/?key=" + properties.get("steam.api") + "&appid=730&steamid=" + steamID)) + .build(); + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + responseData.setPlayerstats(new Gson().fromJson(response.body(), ResponseData.class).getPlayerstats()); + + return responseData; + } + public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyID) { String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; HttpRequest request; @@ -66,9 +90,14 @@ public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyI try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - return gson.fromJson(response.body(), LeetifyProfileResponse.class); + if (response.statusCode() == 200) { + return gson.fromJson(response.body(), LeetifyProfileResponse.class); + } + if (response.statusCode() != 404) { + System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] getPlayerProfile for " + parameter + " returned status " + response.statusCode() + ", body: " + response.body()); + } } catch (IOException | InterruptedException ex) { - System.out.println("[CSBot - LeetifyConnection - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); + System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); } return null; } @@ -89,10 +118,10 @@ public List getPlayerMatchHistory(String steam64ID, String }.getType()); } if(response.statusCode() != 404) { - System.out.println("[CSBot - LeetifyConnection - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] getPlayerMatchHistory for " + parameter + " returned status " + response.statusCode() + ", body: " + response.body()); + System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] getPlayerMatchHistory for " + parameter + " returned status " + response.statusCode() + ", body: " + response.body()); } } catch (IOException | InterruptedException ex) { - System.out.println("[CSBot - LeetifyConnection - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); + System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); } return null; } diff --git a/src/main/java/ch/yoinc/services/CsStatsService.java b/src/main/java/ch/yoinc/services/CsStatsService.java index a55e93d..7b56f90 100644 --- a/src/main/java/ch/yoinc/services/CsStatsService.java +++ b/src/main/java/ch/yoinc/services/CsStatsService.java @@ -2,7 +2,7 @@ import com.google.gson.JsonSyntaxException; import ch.yoinc.http.CarthageException; -import ch.yoinc.http.ConnectionBuilder; +import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.steam.ResponseData; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; @@ -17,13 +17,14 @@ public class CsStatsService { ResourceBundle resourceBundle; - - ConnectionBuilder connectionBuilder; + ExternalApiConnection connection; DataService dataService; + DiscordService discordService; public CsStatsService(Properties properties, DataService dataService) { this.dataService = dataService; - connectionBuilder = new ConnectionBuilder(properties); + connection = new ExternalApiConnection(properties); + discordService = new DiscordService(); } public EmbedBuilder handleStatsEvent(SlashCommandInteractionEvent event) { @@ -111,7 +112,7 @@ private ResponseData getUserResponseData(String discordID) throws NullPointerExc String steamID = dataService.getSteamIDForDiscordID(discordID); if (StringUtils.isNotEmpty(steamID)) { - responseData = connectionBuilder.fetchSteamUserStats(steamID); + responseData = connection.fetchSteamUserStats(steamID); } return responseData; } diff --git a/src/main/java/ch/yoinc/services/DiscordService.java b/src/main/java/ch/yoinc/services/DiscordService.java index 5fabdfe..1afef30 100644 --- a/src/main/java/ch/yoinc/services/DiscordService.java +++ b/src/main/java/ch/yoinc/services/DiscordService.java @@ -2,6 +2,8 @@ import net.dv8tion.jda.api.EmbedBuilder; +import java.util.Locale; + public class DiscordService { public EmbedBuilder createEmbedBuilder(String title, String description, String imageUrl, String footer) { @@ -12,6 +14,13 @@ public EmbedBuilder createEmbedBuilder(String title, String description, String .setFooter(footer); } + public String formatRating(Double rating) { + if (rating == null) { + return "n/a"; + } + return String.format(Locale.US, "%.2f", rating * 100.0); + } + public static class YoincEmbedBuilder extends EmbedBuilder { public YoincEmbedBuilder() { super(); diff --git a/src/main/java/ch/yoinc/tasks/LeetifyTask.java b/src/main/java/ch/yoinc/tasks/LeetifyTask.java index 51cb963..43851c6 100644 --- a/src/main/java/ch/yoinc/tasks/LeetifyTask.java +++ b/src/main/java/ch/yoinc/tasks/LeetifyTask.java @@ -1,6 +1,6 @@ package ch.yoinc.tasks; -import ch.yoinc.http.LeetifyConnection; +import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.internal.InternalUser; import ch.yoinc.model.leetify.LeetifyMatchResponse; import ch.yoinc.model.leetify.LeetifyPlayerStatsResponse; @@ -17,15 +17,20 @@ public class LeetifyTask implements ScheduledTask { private DataService dataService; - private LeetifyConnection leetifyConnection; + private DiscordService discordService; + + private ExternalApiConnection connection; @Override public void execute(JDA jda, Properties properties) { if (dataService == null) { dataService = new DataService(properties); } - if (leetifyConnection == null) { - leetifyConnection = new LeetifyConnection(properties); + if (discordService == null) { + discordService = new DiscordService(); + } + if (connection == null) { + connection = new ExternalApiConnection(properties); } dataService.setBotID(jda.getSelfUser().getId()); @@ -70,9 +75,9 @@ public void execute(JDA jda, Properties properties) { "Kills: " + stats.total_kills + "\nDeaths: " + stats.total_deaths + "\nADR: " + stats.dpr + - "\nRating: " + formatRating(stats.leetify_rating) + - "\nCT Rating: " + formatRating(stats.ct_leetify_rating) + - "\nT Rating: " + formatRating(stats.t_leetify_rating), + "\nRating: " + discordService.formatRating(stats.leetify_rating) + + "\nCT Rating: " + discordService.formatRating(stats.ct_leetify_rating) + + "\nT Rating: " + discordService.formatRating(stats.t_leetify_rating), true ); } @@ -93,17 +98,15 @@ public void execute(JDA jda, Properties properties) { .addField("Kills", Integer.toString(match.stats.getFirst().total_kills), true) .addField("Deaths", Integer.toString(match.stats.getFirst().total_deaths), true) .addField("ADR", Double.toString(match.stats.getFirst().dpr), true) - .addField("Rating", formatRating(match.stats.getFirst().leetify_rating), true) - .addField("CT Rating", formatRating(match.stats.getFirst().ct_leetify_rating), true) - .addField("T Rating", formatRating(match.stats.getFirst().t_leetify_rating), true); + .addField("Rating", discordService.formatRating(match.stats.getFirst().leetify_rating), true) + .addField("CT Rating", discordService.formatRating(match.stats.getFirst().ct_leetify_rating), true) + .addField("T Rating", discordService.formatRating(match.stats.getFirst().t_leetify_rating), true); } Objects.requireNonNull(jda.getTextChannelById(properties.getProperty("discord.channelID"))).sendMessageEmbeds(matchEmbed.build()).queue(); } } private EmbedBuilder returnFilledEmbed(String title, Color color, String description, String map_name, String matchID, String footer) { - DiscordService discordService = new DiscordService(); - String DEFAULT_LEETIFY_URL = "https://leetify.com/app/match-details/%s/overview"; String DEFAULT_MAP_LOGO_URL = "https://raw.githubusercontent.com/MurkyYT/cs2-map-icons/main/images/%s.png"; String DEFAULT_MAP_URL = "https://raw.githubusercontent.com/MurkyYT/cs2-map-icons/main/images/thumbs/%s_1_png.png"; @@ -121,7 +124,7 @@ private HashMap> setNewlyPlayedMatches(List> results = new HashMap<>(); for (InternalUser user : steamInternalUsers) { - List matches = leetifyConnection.getPlayerMatchHistory(user.steamID, null); + List matches = connection.getPlayerMatchHistory(user.steamID, null); if (matches != null && !matches.isEmpty()) { List newMatches = dataService.insertAndGetNewMatches(matches, user.userID); @@ -149,11 +152,4 @@ private HashMap> setNewlyPlayedMatches(List history = List.of(oldMatch, newMatch); - when(leetifyConnection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(history); + when(connection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(history); when(dataService.insertAndGetNewMatches(history, 1)).thenReturn(List.of("new-match")); - injectDependencies(dataService, leetifyConnection); + injectDependencies(dataService, connection); Thread.currentThread().interrupt(); @@ -150,18 +150,18 @@ void setNewlyPlayedMatches_onlyIncludesMatchesReturnedAsNew() throws Exception { @Timeout(value = 12, unit = TimeUnit.SECONDS) void setNewlyPlayedMatches_groupsMatchesFromDifferentUsersUnderSameMatchId() throws Exception { DataService dataService = mock(DataService.class); - LeetifyConnection leetifyConnection = mock(LeetifyConnection.class); + ExternalApiConnection connection = mock(ExternalApiConnection.class); LeetifyMatchResponse aliceMatch = match("shared-match", "Alice"); LeetifyMatchResponse bobMatch = match("shared-match", "Bob"); List aliceHistory = List.of(aliceMatch); List bobHistory = List.of(bobMatch); - when(leetifyConnection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(aliceHistory); - when(leetifyConnection.getPlayerMatchHistory(eq("STEAM2"), isNull())).thenReturn(bobHistory); + when(connection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(aliceHistory); + when(connection.getPlayerMatchHistory(eq("STEAM2"), isNull())).thenReturn(bobHistory); when(dataService.insertAndGetNewMatches(aliceHistory, 1)).thenReturn(List.of("shared-match")); when(dataService.insertAndGetNewMatches(bobHistory, 2)).thenReturn(List.of("shared-match")); - injectDependencies(dataService, leetifyConnection); + injectDependencies(dataService, connection); // the production loop sleeps 10s (real) between users, so the first sleep has to be // waited out for the second user to be processed at all; once it's back we interrupt @@ -217,9 +217,9 @@ private HashMap> invokeSetNewlyPlayedMatches( } } - private void injectDependencies(DataService dataService, LeetifyConnection leetifyConnection) throws Exception { + private void injectDependencies(DataService dataService, ExternalApiConnection connection) throws Exception { setPrivateField("dataService", dataService); - setPrivateField("leetifyConnection", leetifyConnection); + setPrivateField("connectionBuilder", connection); } private void setPrivateField(String name, Object value) throws Exception { From aadb84563f75a4aed3e3651e0f8d8e5418ba977a Mon Sep 17 00:00:00 2001 From: Janes Thomas Date: Wed, 2 Sep 2026 04:06:43 +0200 Subject: [PATCH 2/5] fixed unit tests --- src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java b/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java index fcc8915..3671a99 100644 --- a/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java +++ b/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java @@ -5,6 +5,7 @@ import ch.yoinc.model.leetify.LeetifyMatchResponse; import ch.yoinc.model.leetify.LeetifyPlayerStatsResponse; import ch.yoinc.services.DataService; +import ch.yoinc.services.DiscordService; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; import org.junit.jupiter.api.BeforeEach; @@ -37,8 +38,9 @@ class LeetifyTaskTest { private LeetifyTask task; @BeforeEach - void setUp() { + void setUp() throws Exception { task = new LeetifyTask(); + setPrivateField("discordService", new DiscordService()); } // --------------------------------------------------------------------- @@ -200,10 +202,8 @@ private MessageEmbed invokeReturnFilledEmbed(String title, Color color, String d return builder.build(); } - private String invokeFormatRating(Double rating) throws Exception { - Method method = LeetifyTask.class.getDeclaredMethod("formatRating", Double.class); - method.setAccessible(true); - return (String) method.invoke(null, rating); + private String invokeFormatRating(Double rating) { + return new DiscordService().formatRating(rating); } @SuppressWarnings("unchecked") @@ -219,7 +219,7 @@ private HashMap> invokeSetNewlyPlayedMatches( private void injectDependencies(DataService dataService, ExternalApiConnection connection) throws Exception { setPrivateField("dataService", dataService); - setPrivateField("connectionBuilder", connection); + setPrivateField("connection", connection); } private void setPrivateField(String name, Object value) throws Exception { From bafae65f0aa0666371433fb8e795632aa61def6f Mon Sep 17 00:00:00 2001 From: Janes Thomas Date: Wed, 2 Sep 2026 16:42:29 +0200 Subject: [PATCH 3/5] added logging --- .gitignore | 3 +- pom.xml | 7 + src/main/java/StartUp.java | 15 +- .../ch/yoinc/http/CarthageConnection.java | 2 +- .../ch/yoinc/http/ExternalApiConnection.java | 51 +++-- .../ch/yoinc/services/CsStatsService.java | 41 ++-- .../java/ch/yoinc/services/DataService.java | 31 +-- src/main/java/ch/yoinc/tasks/LeetifyTask.java | 178 ++++++++++-------- .../java/ch/yoinc/tasks/TaskScheduler.java | 10 +- src/main/resources/localization.properties | 3 +- src/main/resources/localization_en.properties | 3 +- src/main/resources/log4j.properties | 1 - src/main/resources/logback.xml | 27 +++ src/test/resources/logback-test.xml | 4 + 14 files changed, 201 insertions(+), 175 deletions(-) delete mode 100644 src/main/resources/log4j.properties create mode 100644 src/main/resources/logback.xml create mode 100644 src/test/resources/logback-test.xml diff --git a/.gitignore b/.gitignore index 3dff92f..8d7990d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ out/ ## Project-specific ignores ############################## /src/main/resources/config-local.properties -/src/main/resources/config-prod.properties \ No newline at end of file +/src/main/resources/config-prod.properties +/logs/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1f1c642..4b0d9a1 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,12 @@ rkon-core 1.1.2 + + ch.qos.logback + logback-classic + 1.5.18 + runtime + @@ -149,6 +155,7 @@ StartUp + diff --git a/src/main/java/StartUp.java b/src/main/java/StartUp.java index 17c59ee..738a152 100644 --- a/src/main/java/StartUp.java +++ b/src/main/java/StartUp.java @@ -7,22 +7,21 @@ import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.MemberCachePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Properties; import java.util.ResourceBundle; public class StartUp { + private static final Logger log = LoggerFactory.getLogger(StartUp.class); + public static void main(String[] args) { try { - - System.out.println("[CSBot - StartUp - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] - Application Started"); - InputStream inputStream = StartUp.class.getClassLoader().getResourceAsStream("config.properties"); Properties properties = new Properties(); properties.load(inputStream); @@ -44,10 +43,8 @@ public static void main(String[] args) { .queue(); jda.awaitReady(); - } catch (InterruptedException ex) { - System.out.println("[CSBot - StartUp - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] InterruptedException thrown: " + ex.getMessage()); - } catch (IOException ex) { - System.out.println("[CSBot - StartUp - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException thrown: " + ex.getMessage()); + } catch (InterruptedException | IOException ex) { + log.error(ex.getMessage(), ex); } } } diff --git a/src/main/java/ch/yoinc/http/CarthageConnection.java b/src/main/java/ch/yoinc/http/CarthageConnection.java index f22bcef..3d42f3d 100644 --- a/src/main/java/ch/yoinc/http/CarthageConnection.java +++ b/src/main/java/ch/yoinc/http/CarthageConnection.java @@ -101,7 +101,7 @@ private String extractString(JsonObject responseBody, String field) { if (responseBody.has(field) && !responseBody.get(field).isJsonNull()) { return responseBody.get(field).getAsString(); } - return null; + return ""; } private List extractList(JsonObject responseBody, String field, Type type) { diff --git a/src/main/java/ch/yoinc/http/ExternalApiConnection.java b/src/main/java/ch/yoinc/http/ExternalApiConnection.java index c16778c..a2c6c4c 100644 --- a/src/main/java/ch/yoinc/http/ExternalApiConnection.java +++ b/src/main/java/ch/yoinc/http/ExternalApiConnection.java @@ -8,6 +8,8 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; @@ -15,13 +17,13 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Instant; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Properties; public class ExternalApiConnection { + private static final Logger log = LoggerFactory.getLogger(ExternalApiConnection.class); + private final String apiKey; private final HttpClient client; private final Gson gson; @@ -74,12 +76,17 @@ public ResponseData fetchSteamUserStats(String steamID) throws InterruptedExcept .uri(URI.create(STEAM_API + "/ISteamUserStats/GetUserStatsForGame/v0002/?key=" + properties.get("steam.api") + "&appid=730&steamid=" + steamID)) .build(); response = client.send(request, HttpResponse.BodyHandlers.ofString()); - responseData.setPlayerstats(new Gson().fromJson(response.body(), ResponseData.class).getPlayerstats()); + if (response.statusCode() == 200) { + responseData.setPlayerstats(new Gson().fromJson(response.body(), ResponseData.class).getPlayerstats()); + } + if (response.statusCode() != 404) { + log.warn("fetchSteamUserStats for {} returned status {}, body: {}", steamID, response.statusCode(), response.body()); + } return responseData; } - public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyID) { + public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyID) throws InterruptedException, IOException { String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; HttpRequest request; request = HttpRequest.newBuilder() @@ -88,21 +95,17 @@ public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyI .GET() .build(); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 200) { - return gson.fromJson(response.body(), LeetifyProfileResponse.class); - } - if (response.statusCode() != 404) { - System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] getPlayerProfile for " + parameter + " returned status " + response.statusCode() + ", body: " + response.body()); - } - } catch (IOException | InterruptedException ex) { - System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return gson.fromJson(response.body(), LeetifyProfileResponse.class); + } + if (response.statusCode() != 404) { + log.warn("getPlayerProfile for {} returned status {}, body: {}", parameter, response.statusCode(), response.body()); } return null; } - public List getPlayerMatchHistory(String steam64ID, String leetifyID) { + public List getPlayerMatchHistory(String steam64ID, String leetifyID) throws InterruptedException, IOException { String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; HttpRequest request; request = HttpRequest.newBuilder() @@ -111,17 +114,13 @@ public List getPlayerMatchHistory(String steam64ID, String .GET() .build(); - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 200) { - return gson.fromJson(response.body(), new TypeToken>() { - }.getType()); - } - if(response.statusCode() != 404) { - System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] getPlayerMatchHistory for " + parameter + " returned status " + response.statusCode() + ", body: " + response.body()); - } - } catch (IOException | InterruptedException ex) { - System.out.println("[CSBot - ConnectionBuilder - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] IOException / InterruptedException thrown: " + ex.getMessage()); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return gson.fromJson(response.body(), new TypeToken>() { + }.getType()); + } + if (response.statusCode() != 404) { + log.warn("getPlayerMatchHistory for {} returned status {}, body: {}", parameter, response.statusCode(), response.body()); } return null; } diff --git a/src/main/java/ch/yoinc/services/CsStatsService.java b/src/main/java/ch/yoinc/services/CsStatsService.java index 7b56f90..4381565 100644 --- a/src/main/java/ch/yoinc/services/CsStatsService.java +++ b/src/main/java/ch/yoinc/services/CsStatsService.java @@ -1,6 +1,5 @@ package ch.yoinc.services; -import com.google.gson.JsonSyntaxException; import ch.yoinc.http.CarthageException; import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.steam.ResponseData; @@ -9,13 +8,16 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import org.codehaus.plexus.util.StringUtils; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; public class CsStatsService { + + private static final Logger log = LoggerFactory.getLogger(CsStatsService.class); + ResourceBundle resourceBundle; ExternalApiConnection connection; DataService dataService; @@ -33,14 +35,14 @@ public EmbedBuilder handleStatsEvent(SlashCommandInteractionEvent event) { try { ResponseData responseData = getUserResponseData(Objects.requireNonNull(event.getOption("player")).getAsMentionable().getId()); return responseData.getBasicInfo(resourceBundle); - } catch (InterruptedException | IOException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] InterruptedException / IOException thrown: " + ex.getMessage()); - return new EmbedBuilder().setTitle(resourceBundle.getString("error.interruptedexception")); - } catch (NullPointerException | JsonSyntaxException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] NullPointerException / JSonSyntaxException thrown: " + ex.getMessage()); + } catch (IOException | InterruptedException ex) { + log.error(ex.getMessage(), ex); + return new EmbedBuilder().setTitle(resourceBundle.getString("error.connectionerror")); + } catch (NullPointerException ex) { + log.error(ex.getMessage(), ex); return new EmbedBuilder().setTitle(resourceBundle.getString("error.privacysettings")); } catch (CarthageException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] CarthageException thrown: " + ex.getMessage()); + log.error(ex.getMessage(), ex); return new EmbedBuilder().setTitle(resourceBundle.getString("error.majorerror")); } } @@ -48,17 +50,18 @@ public EmbedBuilder handleStatsEvent(SlashCommandInteractionEvent event) { public EmbedBuilder handleCompareEvent(SlashCommandInteractionEvent event) { resourceBundle = ResourceBundle.getBundle("localization", Locale.of("en")); try { - String requestedUserOneID = Objects.requireNonNull(event.getOption("playerone")).getAsMentionable().getId(); - String requestedUserTwoID = Objects.requireNonNull(event.getOption("playertwo")).getAsMentionable().getId(); - return comparePlayers(getUserResponseData(requestedUserOneID), getUserResponseData(requestedUserTwoID)); - } catch (NullPointerException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] NullPointerException thrown: " + ex.getMessage()); - return new EmbedBuilder().setTitle(resourceBundle.getString("error.wrongqueryparameters")); + ResponseData userDataOne = getUserResponseData(Objects.requireNonNull(event.getOption("playerone")).getAsMentionable().getId()); + ResponseData userDataTwo = getUserResponseData(Objects.requireNonNull(event.getOption("playertwo")).getAsMentionable().getId()); + + if(userDataOne == null || userDataTwo == null) { + return new EmbedBuilder().setTitle(resourceBundle.getString("error.privacysettings")); + } + return comparePlayers(userDataOne, userDataTwo); } catch (InterruptedException | IOException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] InterruptedException / IOException thrown: " + ex.getMessage()); - return new EmbedBuilder().setTitle(resourceBundle.getString("error.interruptedexception")); + log.error(ex.getMessage(), ex); + return new EmbedBuilder().setTitle(resourceBundle.getString("error.connectionerror")); } catch (CarthageException ex) { - System.out.println("[CSBot - CsStatsService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] CarthageException thrown: " + ex.getMessage()); + log.error(ex.getMessage(), ex); return new EmbedBuilder().setTitle(resourceBundle.getString("error.majorerror")); } } @@ -107,7 +110,7 @@ private String getString(long playerOneLong, long playerTwoLong, boolean playerO } } - private ResponseData getUserResponseData(String discordID) throws NullPointerException, InterruptedException, IOException, CarthageException { + private ResponseData getUserResponseData(String discordID) throws IOException, InterruptedException, CarthageException { ResponseData responseData = null; String steamID = dataService.getSteamIDForDiscordID(discordID); diff --git a/src/main/java/ch/yoinc/services/DataService.java b/src/main/java/ch/yoinc/services/DataService.java index 87206d2..b8071fa 100644 --- a/src/main/java/ch/yoinc/services/DataService.java +++ b/src/main/java/ch/yoinc/services/DataService.java @@ -6,8 +6,6 @@ import ch.yoinc.model.leetify.LeetifyMatchResponse; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Properties; @@ -23,31 +21,18 @@ public void setBotID(String botID) { this.botID = botID; } - public String getSteamIDForDiscordID(String discordID) throws CarthageException { - try { - return carthageConnection.getSteamIDForDiscordID(botID, discordID); - } catch (IOException | InterruptedException ex) { - throw new CarthageException("Failed to fetch steamID for discordID " + discordID + " from carthage: " + ex.getMessage()); - } + public String getSteamIDForDiscordID(String discordID) throws IOException, InterruptedException, CarthageException { + return carthageConnection.getSteamIDForDiscordID(botID, discordID); } - public List getAllSteamUsers() { - try { - return carthageConnection.getAllSteamUsers(botID); - } catch (IOException | InterruptedException | CarthageException ex) { - System.out.println("[CSBot - DataService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] InterruptedException / IOException / CarthageException thrown: " + ex.getMessage()); - } - return List.of(); + public List getAllSteamUsers() throws IOException, InterruptedException, CarthageException { + return carthageConnection.getAllSteamUsers(botID); } - public List insertAndGetNewMatches(List matches, Integer userID) { - try { - if (matches != null && !matches.isEmpty()) { - List matchIDs = matches.stream().map(match -> match.id).toList(); - return carthageConnection.insertAndGetNewMatches(matchIDs, userID, botID); - } - } catch (IOException | InterruptedException | CarthageException ex) { - System.out.println("[CSBot - DataService - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] InterruptedException / IOException / CarthageException thrown: " + ex.getMessage()); + public List insertAndGetNewMatches(List matches, Integer userID) throws IOException, InterruptedException, CarthageException { + if (matches != null && !matches.isEmpty()) { + List matchIDs = matches.stream().map(match -> match.id).toList(); + return carthageConnection.insertAndGetNewMatches(matchIDs, userID, botID); } return List.of(); } diff --git a/src/main/java/ch/yoinc/tasks/LeetifyTask.java b/src/main/java/ch/yoinc/tasks/LeetifyTask.java index 43851c6..a6e6db4 100644 --- a/src/main/java/ch/yoinc/tasks/LeetifyTask.java +++ b/src/main/java/ch/yoinc/tasks/LeetifyTask.java @@ -1,5 +1,6 @@ package ch.yoinc.tasks; +import ch.yoinc.http.CarthageException; import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.internal.InternalUser; import ch.yoinc.model.leetify.LeetifyMatchResponse; @@ -8,17 +9,20 @@ import ch.yoinc.services.DiscordService; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.awt.*; +import java.io.IOException; import java.util.*; import java.util.List; -import java.util.Locale; public class LeetifyTask implements ScheduledTask { + private static final Logger log = LoggerFactory.getLogger(LeetifyTask.class); + private DataService dataService; private DiscordService discordService; - private ExternalApiConnection connection; @Override @@ -34,75 +38,79 @@ public void execute(JDA jda, Properties properties) { } dataService.setBotID(jda.getSelfUser().getId()); - List internalUsers = dataService.getAllSteamUsers(); - - //this map contains all newly played matches found during the next run of the - //leetify task. its identifier is the match ID and the list contains all - //match responses with this id. why? because that way it's easier to see if - //for example a group has played in the same match. this would allow a different message - //to be sent to the discord instead of the generic "person x has played a match." - HashMap> newlyPlayedMatches = setNewlyPlayedMatches(internalUsers); - - for (String matchID : newlyPlayedMatches.keySet()) { - List matches = newlyPlayedMatches.get(matchID); - EmbedBuilder matchEmbed = new EmbedBuilder(); - LeetifyMatchResponse match = matches.getFirst(); - boolean hasWon = match.stats.getFirst().rounds_won >= match.stats.getFirst().rounds_lost; //tie is a victory, change my mind - if (matches.size() > 1) { - List playerNames = new ArrayList<>(); - for (LeetifyMatchResponse playerMatch : matches) { - playerNames.add(playerMatch.stats.getFirst().name); - } - String players = String.join(", ", playerNames); - - matchEmbed = switch (match.data_source) { - case "faceit" -> returnFilledEmbed("New Faceit Match", - Color.ORANGE, players + " played a Faceit match together and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - case "matchmaking_wingman" -> returnFilledEmbed("New Wingman Match", - Color.GREEN, players + " played a Wingman match together and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - case "matchmaking" -> returnFilledEmbed("New Competitive Match", - Color.YELLOW, players + " played a Competitive match together and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - default -> matchEmbed; - }; - - for (LeetifyMatchResponse playerMatch : matches) { - LeetifyPlayerStatsResponse stats = playerMatch.stats.getFirst(); - matchEmbed.addField( - stats.name, - "Kills: " + stats.total_kills + - "\nDeaths: " + stats.total_deaths + - "\nADR: " + stats.dpr + - "\nRating: " + discordService.formatRating(stats.leetify_rating) + - "\nCT Rating: " + discordService.formatRating(stats.ct_leetify_rating) + - "\nT Rating: " + discordService.formatRating(stats.t_leetify_rating), - true - ); + try { + List internalUsers = dataService.getAllSteamUsers(); + + //this map contains all newly played matches found during the next run of the + //leetify task. its identifier is the match ID and the list contains all + //match responses with this id. why? because that way it's easier to see if + //for example a group has played in the same match. this would allow a different message + //to be sent to the discord instead of the generic "person x has played a match." + HashMap> newlyPlayedMatches = setNewlyPlayedMatches(internalUsers); + + for (String matchID : newlyPlayedMatches.keySet()) { + List matches = newlyPlayedMatches.get(matchID); + EmbedBuilder matchEmbed = new EmbedBuilder(); + LeetifyMatchResponse match = matches.getFirst(); + boolean hasWon = match.stats.getFirst().rounds_won >= match.stats.getFirst().rounds_lost; //tie is a victory, change my mind + if (matches.size() > 1) { + List playerNames = new ArrayList<>(); + for (LeetifyMatchResponse playerMatch : matches) { + playerNames.add(playerMatch.stats.getFirst().name); + } + String players = String.join(", ", playerNames); + + matchEmbed = switch (match.data_source) { + case "faceit" -> returnFilledEmbed("New Faceit Match", + Color.ORANGE, players + " played a Faceit match together and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + case "matchmaking_wingman" -> returnFilledEmbed("New Wingman Match", + Color.GREEN, players + " played a Wingman match together and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + case "matchmaking" -> returnFilledEmbed("New Competitive Match", + Color.YELLOW, players + " played a Competitive match together and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + default -> matchEmbed; + }; + + for (LeetifyMatchResponse playerMatch : matches) { + LeetifyPlayerStatsResponse stats = playerMatch.stats.getFirst(); + matchEmbed.addField( + stats.name, + "Kills: " + stats.total_kills + + "\nDeaths: " + stats.total_deaths + + "\nADR: " + stats.dpr + + "\nRating: " + discordService.formatRating(stats.leetify_rating) + + "\nCT Rating: " + discordService.formatRating(stats.ct_leetify_rating) + + "\nT Rating: " + discordService.formatRating(stats.t_leetify_rating), + true + ); + } + } else { + matchEmbed = switch (match.data_source) { + case "faceit" -> returnFilledEmbed("New Faceit Match", Color.ORANGE, + match.stats.getFirst().name + " played a Faceit match and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + case "matchmaking_wingman" -> returnFilledEmbed("New Wingman Match", Color.GREEN, + match.stats.getFirst().name + " played a Wingman match and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + case "matchmaking" -> returnFilledEmbed("New Competitive Match", Color.YELLOW, + match.stats.getFirst().name + " played a Competitive match and " + ((hasWon) ? "**won**." : "**lost**."), + match.map_name, matchID, "Finished at " + match.finished_at); + default -> matchEmbed; + }; + matchEmbed + .addField("Kills", Integer.toString(match.stats.getFirst().total_kills), true) + .addField("Deaths", Integer.toString(match.stats.getFirst().total_deaths), true) + .addField("ADR", Double.toString(match.stats.getFirst().dpr), true) + .addField("Rating", discordService.formatRating(match.stats.getFirst().leetify_rating), true) + .addField("CT Rating", discordService.formatRating(match.stats.getFirst().ct_leetify_rating), true) + .addField("T Rating", discordService.formatRating(match.stats.getFirst().t_leetify_rating), true); } - } else { - matchEmbed = switch (match.data_source) { - case "faceit" -> returnFilledEmbed("New Faceit Match", Color.ORANGE, - match.stats.getFirst().name + " played a Faceit match and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - case "matchmaking_wingman" -> returnFilledEmbed("New Wingman Match", Color.GREEN, - match.stats.getFirst().name + " played a Wingman match and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - case "matchmaking" -> returnFilledEmbed("New Competitive Match", Color.YELLOW, - match.stats.getFirst().name + " played a Competitive match and " + ((hasWon) ? "**won**." : "**lost**."), - match.map_name, matchID, "Finished at " + match.finished_at); - default -> matchEmbed; - }; - matchEmbed - .addField("Kills", Integer.toString(match.stats.getFirst().total_kills), true) - .addField("Deaths", Integer.toString(match.stats.getFirst().total_deaths), true) - .addField("ADR", Double.toString(match.stats.getFirst().dpr), true) - .addField("Rating", discordService.formatRating(match.stats.getFirst().leetify_rating), true) - .addField("CT Rating", discordService.formatRating(match.stats.getFirst().ct_leetify_rating), true) - .addField("T Rating", discordService.formatRating(match.stats.getFirst().t_leetify_rating), true); + Objects.requireNonNull(jda.getTextChannelById(properties.getProperty("discord.channelID"))).sendMessageEmbeds(matchEmbed.build()).queue(); } - Objects.requireNonNull(jda.getTextChannelById(properties.getProperty("discord.channelID"))).sendMessageEmbeds(matchEmbed.build()).queue(); + } catch (InterruptedException | IOException | CarthageException ex) { + log.error(ex.getMessage(), ex); } } @@ -123,27 +131,31 @@ private EmbedBuilder returnFilledEmbed(String title, Color color, String descrip private HashMap> setNewlyPlayedMatches(List steamInternalUsers) { HashMap> results = new HashMap<>(); - for (InternalUser user : steamInternalUsers) { - List matches = connection.getPlayerMatchHistory(user.steamID, null); + try { + for (InternalUser user : steamInternalUsers) { + List matches = connection.getPlayerMatchHistory(user.steamID, null); - if (matches != null && !matches.isEmpty()) { - List newMatches = dataService.insertAndGetNewMatches(matches, user.userID); + if (matches != null && !matches.isEmpty()) { + List newMatches = dataService.insertAndGetNewMatches(matches, user.userID); - for (String matchID : newMatches) { - for (LeetifyMatchResponse match : matches) { - if (match.id.equals(matchID)) { - results.computeIfAbsent(matchID, k -> new ArrayList<>()).add(match); + for (String matchID : newMatches) { + for (LeetifyMatchResponse match : matches) { + if (match.id.equals(matchID)) { + results.computeIfAbsent(matchID, k -> new ArrayList<>()).add(match); + } } } } - } - try { - Thread.sleep(10000); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return results; + try { + Thread.sleep(10000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return results; + } } + } catch (InterruptedException | IOException | CarthageException ex) { + log.error(ex.getMessage(), ex); } return results; } diff --git a/src/main/java/ch/yoinc/tasks/TaskScheduler.java b/src/main/java/ch/yoinc/tasks/TaskScheduler.java index 539e277..93e9120 100644 --- a/src/main/java/ch/yoinc/tasks/TaskScheduler.java +++ b/src/main/java/ch/yoinc/tasks/TaskScheduler.java @@ -2,8 +2,6 @@ import net.dv8tion.jda.api.JDA; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; /** @@ -35,7 +33,7 @@ public void registerTask(ScheduledTask task) { /** * Start all registered ch.yoinc.tasks. * - * @param jda The JDA instance + * @param jda The JDA instance */ public void startAllTasks(JDA jda) { this.jda = jda; @@ -59,11 +57,7 @@ private void startTask(ScheduledTask task) { TimerTask timerTask = new TimerTask() { @Override public void run() { - try { - task.execute(jda, properties); - } catch (Exception ex) { - System.out.println("[CSBot - TaskScheduler - " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - HH:mm:ss")) + "] Exception thrown: " + ex.getMessage()); - } + task.execute(jda, properties); } }; diff --git a/src/main/resources/localization.properties b/src/main/resources/localization.properties index ccaf0e0..26fe449 100644 --- a/src/main/resources/localization.properties +++ b/src/main/resources/localization.properties @@ -1,6 +1,5 @@ -error.interruptedexception=Connection issues. :( +error.connectionerror=Connection issues. :( error.privacysettings=No stats could be loaded. (Steam Privacy Settings?) -error.wrongqueryparameters=The players were not submitted properly. error.noteamcreation=No teams could be created. error.notincorrectvc=You are not in a voice channel. error.majorerror=Something broke. We are working on this issue. diff --git a/src/main/resources/localization_en.properties b/src/main/resources/localization_en.properties index ccaf0e0..26fe449 100644 --- a/src/main/resources/localization_en.properties +++ b/src/main/resources/localization_en.properties @@ -1,6 +1,5 @@ -error.interruptedexception=Connection issues. :( +error.connectionerror=Connection issues. :( error.privacysettings=No stats could be loaded. (Steam Privacy Settings?) -error.wrongqueryparameters=The players were not submitted properly. error.noteamcreation=No teams could be created. error.notincorrectvc=You are not in a voice channel. error.majorerror=Something broke. We are working on this issue. diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties deleted file mode 100644 index 54c9727..0000000 --- a/src/main/resources/log4j.properties +++ /dev/null @@ -1 +0,0 @@ -log4j.appender.file.File=retake.log \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..c38e7c3 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + %d{dd.MM.yyyy - HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + logs/csbot.log + + logs/csbot.%d{yyyy-MM-dd}.%i.log + 10MB + 14 + 200MB + + + %d{dd.MM.yyyy - HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..8781576 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + From e787176a51c08834b016fbc5f258ce2fc25e4db0 Mon Sep 17 00:00:00 2001 From: Janes Thomas Date: Sat, 12 Sep 2026 14:31:21 +0200 Subject: [PATCH 4/5] added feature "leetify" on user context interaction --- src/main/java/StartUp.java | 4 +- .../listeners/CounterStrikeBotListener.java | 7 ++- .../ch/yoinc/model/steam/ResponseData.java | 2 +- .../ch/yoinc/services/CsStatsService.java | 59 +++++++++++++++++++ src/main/resources/localization.properties | 1 + src/main/resources/localization_en.properties | 1 + 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/main/java/StartUp.java b/src/main/java/StartUp.java index 738a152..92556db 100644 --- a/src/main/java/StartUp.java +++ b/src/main/java/StartUp.java @@ -2,6 +2,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.Commands; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -39,7 +40,8 @@ public static void main(String[] args) { jda.updateCommands().addCommands( Commands.slash("stats", resourceBundle.getString("command.stats.description")).addOption(OptionType.MENTIONABLE, "player", resourceBundle.getString("command.stats.value.description"), true), Commands.slash("compare", resourceBundle.getString("command.compare.description")).addOption(OptionType.MENTIONABLE, "playerone", resourceBundle.getString("command.compare.valueone.description"), true).addOption(OptionType.MENTIONABLE, "playertwo", resourceBundle.getString("command.compare.valuetwo.description"), true), - Commands.slash("teams", resourceBundle.getString("command.teams.description")).addOption(OptionType.NUMBER, "amountofteams", resourceBundle.getString("command.teams.value.description"), false)) + Commands.slash("teams", resourceBundle.getString("command.teams.description")).addOption(OptionType.NUMBER, "amountofteams", resourceBundle.getString("command.teams.value.description"), false), + Commands.context(Command.Type.USER, "Leetify")) .queue(); jda.awaitReady(); diff --git a/src/main/java/ch/yoinc/listeners/CounterStrikeBotListener.java b/src/main/java/ch/yoinc/listeners/CounterStrikeBotListener.java index 1525d42..cfb211c 100644 --- a/src/main/java/ch/yoinc/listeners/CounterStrikeBotListener.java +++ b/src/main/java/ch/yoinc/listeners/CounterStrikeBotListener.java @@ -18,14 +18,12 @@ public class CounterStrikeBotListener extends ListenerAdapter { private final CsFunService csFunService; private final CsStatsService csStatsService; private final DataService dataService; - private final DiscordService discordService; private final TaskScheduler taskScheduler; public CounterStrikeBotListener(Properties properties) { dataService = new DataService(properties); csStatsService = new CsStatsService(properties, dataService); csFunService = new CsFunService(new MessageService(properties)); - discordService = new DiscordService(); taskScheduler = new TaskScheduler(properties); } @@ -54,7 +52,10 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even @Override public void onUserContextInteraction(@NotNull UserContextInteractionEvent event) { - + if ("leetify".equalsIgnoreCase(event.getName())) { + event.deferReply().queue(); + event.getHook().sendMessageEmbeds(csStatsService.handleLeetifyUserContext(event).build()).queue(); + } } @Override diff --git a/src/main/java/ch/yoinc/model/steam/ResponseData.java b/src/main/java/ch/yoinc/model/steam/ResponseData.java index 9dd8551..63af8a8 100644 --- a/src/main/java/ch/yoinc/model/steam/ResponseData.java +++ b/src/main/java/ch/yoinc/model/steam/ResponseData.java @@ -33,7 +33,7 @@ public EmbedBuilder getBasicInfo(ResourceBundle resourceBundle) { return new EmbedBuilder() .setTitle(resourceBundle.getString("stats.title").replace("%s", getSteamUserInfo().getPlayers().get(0).getPersonaname())) .setAuthor(resourceBundle.getString("stats.author"), "https://www.yoinc.ch") - .setImage(getSteamUserInfo().getPlayers().get(0).getAvatarmedium()) + .setImage(getSteamUserInfo().getPlayers().getFirst().getAvatarmedium()) .addField(new MessageEmbed.Field(resourceBundle.getString("stats.kills"), String.valueOf(getLongStatsForName("total_kills")), true)) .addField(new MessageEmbed.Field(resourceBundle.getString("stats.deaths"),String.valueOf(getLongStatsForName("total_deaths")),true)) .addField(new MessageEmbed.Field(resourceBundle.getString("stats.wins"),String.valueOf(getLongStatsForName("total_wins")),true)) diff --git a/src/main/java/ch/yoinc/services/CsStatsService.java b/src/main/java/ch/yoinc/services/CsStatsService.java index 4381565..4785a45 100644 --- a/src/main/java/ch/yoinc/services/CsStatsService.java +++ b/src/main/java/ch/yoinc/services/CsStatsService.java @@ -2,10 +2,13 @@ import ch.yoinc.http.CarthageException; import ch.yoinc.http.ExternalApiConnection; +import ch.yoinc.model.leetify.LeetifyProfileResponse; +import ch.yoinc.model.leetify.LeetifyRatingResponse; import ch.yoinc.model.steam.ResponseData; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; import org.codehaus.plexus.util.StringUtils; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -18,6 +21,13 @@ public class CsStatsService { private static final Logger log = LoggerFactory.getLogger(CsStatsService.class); + private static final String[] WINGMAN_RANK_NAMES = { + "Silver I", "Silver II", "Silver III", "Silver IV", "Silver Elite", "Silver Elite Master", + "Gold Nova I", "Gold Nova II", "Gold Nova III", "Gold Nova Master", + "Master Guardian I", "Master Guardian II", "Master Guardian Elite", "Distinguished Master Guardian", + "Legendary Eagle", "Legendary Eagle Master", "Supreme Master First Class", "Global Elite" + }; + ResourceBundle resourceBundle; ExternalApiConnection connection; DataService dataService; @@ -66,6 +76,55 @@ public EmbedBuilder handleCompareEvent(SlashCommandInteractionEvent event) { } } + public EmbedBuilder handleLeetifyUserContext(UserContextInteractionEvent event) { + resourceBundle = ResourceBundle.getBundle("localization", Locale.of("en")); + try { + String steamID = dataService.getSteamIDForDiscordID(Objects.requireNonNull(event.getMember()).getId()); + LeetifyProfileResponse profileResponse = connection.getPlayerProfile(steamID, null); + if(profileResponse != null) { + EmbedBuilder returnEmbed = discordService.createEmbedBuilder(profileResponse.name + "'s Leetify Stats", null, null, + "First match played at " + profileResponse.first_match_date); + + returnEmbed + .setThumbnail("https://cdn.brandfetch.io/idYPcZQLsh/w/820/h/820/theme/dark/logo.png?c=1dxbfHSJFAPEGdCLU4o5B") + .addField("Faceit", profileResponse.ranks.faceit == null ? "n/a" + "(" + profileResponse.ranks.faceit_elo + ")" : profileResponse.ranks.faceit + "(" + profileResponse.ranks.faceit_elo + ")", true) + .addField("Premier", Integer.toString(profileResponse.ranks.premier), true) + .addField("Wingman", getWingmanRankName(profileResponse.ranks.wingman), true) + .addField("Leetify Rating", String.format("%.2f", profileResponse.ranks.leetify), true) + .addField("Win Rate", discordService.formatRating(profileResponse.winrate), true) + .addField("Played matches", Integer.toString(profileResponse.total_matches), true); + + LeetifyRatingResponse rating = profileResponse.rating; + if (rating != null) { + returnEmbed + .addField("CT Rating", discordService.formatRating(rating.ct_leetify), true) + .addField("T Rating", discordService.formatRating(rating.t_leetify), true) + .addField("Aim", Double.toString(rating.aim), true) + .addField("Positioning", Double.toString(rating.positioning), true) + .addField("Utility", Double.toString(rating.utility), true) + .addField("Clutch", discordService.formatRating(rating.clutch), true) + .addField("Opening", discordService.formatRating(rating.opening), true); + } + + return returnEmbed; + } + } catch (InterruptedException | IOException ex) { + log.error(ex.getMessage(), ex); + return new EmbedBuilder().setTitle(resourceBundle.getString("error.connectionerror")); + } catch (CarthageException ex) { + log.error(ex.getMessage(), ex); + return new EmbedBuilder().setTitle(resourceBundle.getString("error.majorerror")); + } + return new EmbedBuilder().setTitle(resourceBundle.getString("error.noleetifyprofile")); + } + + private String getWingmanRankName(Integer wingmanRank) { + if (wingmanRank == null || wingmanRank < 1 || wingmanRank > WINGMAN_RANK_NAMES.length) { + return "n/a"; + } + return WINGMAN_RANK_NAMES[wingmanRank - 1]; + } + private EmbedBuilder comparePlayers(ResponseData playerOneData, ResponseData playerTwoData) { int[] wins = new int[2]; // wins[0] = playerOne, wins[1] = playerTwo diff --git a/src/main/resources/localization.properties b/src/main/resources/localization.properties index 26fe449..764d5d1 100644 --- a/src/main/resources/localization.properties +++ b/src/main/resources/localization.properties @@ -3,6 +3,7 @@ error.privacysettings=No stats could be loaded. (Steam Privacy Settings?) error.noteamcreation=No teams could be created. error.notincorrectvc=You are not in a voice channel. error.majorerror=Something broke. We are working on this issue. +error.noleetifyprofile=No Leetify profile could be found for this user. info.messagesent=A message was sent to csgo-stuff. stats.title=__Stats for %s__ diff --git a/src/main/resources/localization_en.properties b/src/main/resources/localization_en.properties index 26fe449..764d5d1 100644 --- a/src/main/resources/localization_en.properties +++ b/src/main/resources/localization_en.properties @@ -3,6 +3,7 @@ error.privacysettings=No stats could be loaded. (Steam Privacy Settings?) error.noteamcreation=No teams could be created. error.notincorrectvc=You are not in a voice channel. error.majorerror=Something broke. We are working on this issue. +error.noleetifyprofile=No Leetify profile could be found for this user. info.messagesent=A message was sent to csgo-stuff. stats.title=__Stats for %s__ From 3cd2a6d4e1eaa732fe5a680c4d3f3247f84d308e Mon Sep 17 00:00:00 2001 From: Janes Thomas Date: Sat, 12 Sep 2026 18:40:17 +0200 Subject: [PATCH 5/5] refactored CsStatsService and created tests --- .../ch/yoinc/services/CsStatsService.java | 91 +++-- .../ch/yoinc/services/CsStatsServiceTest.java | 347 ++++++++++++++++++ 2 files changed, 411 insertions(+), 27 deletions(-) create mode 100644 src/test/java/ch/yoinc/services/CsStatsServiceTest.java diff --git a/src/main/java/ch/yoinc/services/CsStatsService.java b/src/main/java/ch/yoinc/services/CsStatsService.java index 4785a45..d5169c3 100644 --- a/src/main/java/ch/yoinc/services/CsStatsService.java +++ b/src/main/java/ch/yoinc/services/CsStatsService.java @@ -3,6 +3,7 @@ import ch.yoinc.http.CarthageException; import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.leetify.LeetifyProfileResponse; +import ch.yoinc.model.leetify.LeetifyRankResponse; import ch.yoinc.model.leetify.LeetifyRatingResponse; import ch.yoinc.model.steam.ResponseData; import net.dv8tion.jda.api.EmbedBuilder; @@ -79,29 +80,34 @@ public EmbedBuilder handleCompareEvent(SlashCommandInteractionEvent event) { public EmbedBuilder handleLeetifyUserContext(UserContextInteractionEvent event) { resourceBundle = ResourceBundle.getBundle("localization", Locale.of("en")); try { - String steamID = dataService.getSteamIDForDiscordID(Objects.requireNonNull(event.getMember()).getId()); + String steamID = dataService.getSteamIDForDiscordID(Objects.requireNonNull(event.getTargetMember()).getId()); + if (!StringUtils.isNotEmpty(steamID)) { + return new EmbedBuilder().setTitle(resourceBundle.getString("error.noleetifyprofile")); + } + LeetifyProfileResponse profileResponse = connection.getPlayerProfile(steamID, null); if(profileResponse != null) { EmbedBuilder returnEmbed = discordService.createEmbedBuilder(profileResponse.name + "'s Leetify Stats", null, null, "First match played at " + profileResponse.first_match_date); + LeetifyRankResponse ranks = profileResponse.ranks; returnEmbed .setThumbnail("https://cdn.brandfetch.io/idYPcZQLsh/w/820/h/820/theme/dark/logo.png?c=1dxbfHSJFAPEGdCLU4o5B") - .addField("Faceit", profileResponse.ranks.faceit == null ? "n/a" + "(" + profileResponse.ranks.faceit_elo + ")" : profileResponse.ranks.faceit + "(" + profileResponse.ranks.faceit_elo + ")", true) - .addField("Premier", Integer.toString(profileResponse.ranks.premier), true) - .addField("Wingman", getWingmanRankName(profileResponse.ranks.wingman), true) - .addField("Leetify Rating", String.format("%.2f", profileResponse.ranks.leetify), true) + .addField("Faceit", formatFaceitRank(ranks), true) + .addField("Premier", ranks == null ? "n/a" : formatNullable(ranks.premier), true) + .addField("Wingman", ranks == null ? "n/a" : getWingmanRankName(ranks.wingman), true) + .addField("Leetify Rating", ranks == null ? "n/a" : formatNullable(ranks.leetify), true) .addField("Win Rate", discordService.formatRating(profileResponse.winrate), true) - .addField("Played matches", Integer.toString(profileResponse.total_matches), true); + .addField("Played matches", formatNullable(profileResponse.total_matches), true); LeetifyRatingResponse rating = profileResponse.rating; if (rating != null) { returnEmbed .addField("CT Rating", discordService.formatRating(rating.ct_leetify), true) .addField("T Rating", discordService.formatRating(rating.t_leetify), true) - .addField("Aim", Double.toString(rating.aim), true) - .addField("Positioning", Double.toString(rating.positioning), true) - .addField("Utility", Double.toString(rating.utility), true) + .addField("Aim", formatNullable(rating.aim), true) + .addField("Positioning", formatNullable(rating.positioning), true) + .addField("Utility", formatNullable(rating.utility), true) .addField("Clutch", discordService.formatRating(rating.clutch), true) .addField("Opening", discordService.formatRating(rating.opening), true); } @@ -111,6 +117,9 @@ public EmbedBuilder handleLeetifyUserContext(UserContextInteractionEvent event) } catch (InterruptedException | IOException ex) { log.error(ex.getMessage(), ex); return new EmbedBuilder().setTitle(resourceBundle.getString("error.connectionerror")); + } catch (NullPointerException ex) { + log.error(ex.getMessage(), ex); + return new EmbedBuilder().setTitle(resourceBundle.getString("error.privacysettings")); } catch (CarthageException ex) { log.error(ex.getMessage(), ex); return new EmbedBuilder().setTitle(resourceBundle.getString("error.majorerror")); @@ -125,47 +134,75 @@ private String getWingmanRankName(Integer wingmanRank) { return WINGMAN_RANK_NAMES[wingmanRank - 1]; } + private String formatFaceitRank(LeetifyRankResponse ranks) { + if (ranks == null || ranks.faceit == null) { + return "n/a"; + } + return ranks.faceit + " (" + formatNullable(ranks.faceit_elo) + ")"; + } + + private String formatNullable(Integer value) { + return value == null ? "n/a" : value.toString(); + } + + private String formatNullable(Double value) { + return value == null ? "n/a" : String.format("%.2f", value); + } + + private record ComparisonResult(String display, int winner) { + private static final int TIE = 0; + private static final int PLAYER_ONE = 1; + private static final int PLAYER_TWO = 2; + } + private EmbedBuilder comparePlayers(ResponseData playerOneData, ResponseData playerTwoData) { - int[] wins = new int[2]; // wins[0] = playerOne, wins[1] = playerTwo + ComparisonResult kills = getWinner(playerOneData, playerTwoData, "total_kills", true); + ComparisonResult deaths = getWinner(playerOneData, playerTwoData, "total_deaths", false); + ComparisonResult wins = getWinner(playerOneData, playerTwoData, "total_wins", true); + ComparisonResult planted = getWinner(playerOneData, playerTwoData, "total_planted_bombs", true); + ComparisonResult defused = getWinner(playerOneData, playerTwoData, "total_defused_bombs", true); + ComparisonResult damage = getWinner(playerOneData, playerTwoData, "total_damage_done", true); + + List results = List.of(kills, deaths, wins, planted, defused, damage); + long winsOne = results.stream().filter(result -> result.winner() == ComparisonResult.PLAYER_ONE).count(); + long winsTwo = results.stream().filter(result -> result.winner() == ComparisonResult.PLAYER_TWO).count(); EmbedBuilder embedBuilder = new EmbedBuilder() .setTitle(resourceBundle.getString("compare.title").replace("%s", playerOneData.getSteamUserInfo().getPlayers().getFirst().getPersonaname()).replace("%t", playerTwoData.getSteamUserInfo().getPlayers().getFirst().getPersonaname())) .setAuthor(resourceBundle.getString("stats.author"), "https://www.yoinc.ch") - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.kills"), getWinner(playerOneData, playerTwoData, "total_kills", true, wins), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.deaths"), getWinner(playerOneData, playerTwoData, "total_deaths", false, wins), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.wins"), getWinner(playerOneData, playerTwoData, "total_wins", true, wins), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.planted"), getWinner(playerOneData, playerTwoData, "total_planted_bombs", true, wins), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.defused"), getWinner(playerOneData, playerTwoData, "total_defused_bombs", true, wins), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.damage"), getWinner(playerOneData, playerTwoData, "total_damage_done", true, wins), true)); - - if (wins[0] > wins[1]) { + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.kills"), kills.display(), true)) + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.deaths"), deaths.display(), true)) + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.wins"), wins.display(), true)) + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.planted"), planted.display(), true)) + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.defused"), defused.display(), true)) + .addField(new MessageEmbed.Field(resourceBundle.getString("stats.damage"), damage.display(), true)); + + if (winsOne > winsTwo) { embedBuilder.setImage(playerOneData.getSteamUserInfo().getPlayers().getFirst().getAvatarmedium()); - } else if (wins[1] > wins[0]) { + } else if (winsTwo > winsOne) { embedBuilder.setImage(playerTwoData.getSteamUserInfo().getPlayers().getFirst().getAvatarmedium()); } return embedBuilder; } - private String getWinner(ResponseData playerOneData, ResponseData playerTwoData, String statName, boolean higherRequired, int[] wins) { + private ComparisonResult getWinner(ResponseData playerOneData, ResponseData playerTwoData, String statName, boolean higherRequired) { long playerOneLong = playerOneData.getLongStatsForName(statName); long playerTwoLong = playerTwoData.getLongStatsForName(statName); boolean playerOneWins = higherRequired ? playerOneLong > playerTwoLong : playerOneLong < playerTwoLong; boolean playerTwoWins = higherRequired ? playerTwoLong > playerOneLong : playerTwoLong < playerOneLong; - return getString(playerOneLong, playerTwoLong, playerOneWins, playerTwoWins, wins); + return getString(playerOneLong, playerTwoLong, playerOneWins, playerTwoWins); } @NotNull - private String getString(long playerOneLong, long playerTwoLong, boolean playerOneWins, boolean playerTwoWins, int[] wins) { + private ComparisonResult getString(long playerOneLong, long playerTwoLong, boolean playerOneWins, boolean playerTwoWins) { if (playerOneWins) { - wins[0]++; - return "** :star: " + playerOneLong + " ** vs " + playerTwoLong; + return new ComparisonResult("** :star: " + playerOneLong + " ** vs " + playerTwoLong, ComparisonResult.PLAYER_ONE); } else if (playerTwoWins) { - wins[1]++; - return playerOneLong + " vs ** " + playerTwoLong + " ** :star: "; + return new ComparisonResult(playerOneLong + " vs ** " + playerTwoLong + " ** :star: ", ComparisonResult.PLAYER_TWO); } else { - return resourceBundle.getString("compare.equal").replace("%s", String.valueOf(playerOneLong)); + return new ComparisonResult(resourceBundle.getString("compare.equal").replace("%s", String.valueOf(playerOneLong)), ComparisonResult.TIE); } } diff --git a/src/test/java/ch/yoinc/services/CsStatsServiceTest.java b/src/test/java/ch/yoinc/services/CsStatsServiceTest.java new file mode 100644 index 0000000..62394e7 --- /dev/null +++ b/src/test/java/ch/yoinc/services/CsStatsServiceTest.java @@ -0,0 +1,347 @@ +package ch.yoinc.services; + +import ch.yoinc.http.ExternalApiConnection; +import ch.yoinc.model.leetify.LeetifyProfileResponse; +import ch.yoinc.model.leetify.LeetifyRankResponse; +import ch.yoinc.model.leetify.LeetifyRatingResponse; +import ch.yoinc.model.steam.PlayerStats; +import ch.yoinc.model.steam.ResponseData; +import ch.yoinc.model.steam.SingleStat; +import ch.yoinc.model.steam.SteamUser; +import ch.yoinc.model.steam.SteamUserInfo; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.ResourceBundle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CsStatsServiceTest { + + private CsStatsService service; + + @BeforeEach + void setUp() { + service = new CsStatsService(new Properties(), mock(DataService.class)); + service.resourceBundle = ResourceBundle.getBundle("localization", Locale.of("en")); + } + + // --------------------------------------------------------------------- + // getWingmanRankName + // --------------------------------------------------------------------- + + @Test + void getWingmanRankName_mapsLowestAndHighestTier() throws Exception { + assertEquals("Silver I", invokeGetWingmanRankName(1)); + assertEquals("Global Elite", invokeGetWingmanRankName(18)); + } + + @Test + void getWingmanRankName_returnsPlaceholder_whenOutOfRangeOrMissing() throws Exception { + assertEquals("n/a", invokeGetWingmanRankName(null)); + assertEquals("n/a", invokeGetWingmanRankName(0)); + assertEquals("n/a", invokeGetWingmanRankName(19)); + } + + // --------------------------------------------------------------------- + // formatFaceitRank + // --------------------------------------------------------------------- + + @Test + void formatFaceitRank_returnsPlaceholder_whenRanksOrFaceitMissing() throws Exception { + assertEquals("n/a", invokeFormatFaceitRank(null)); + assertEquals("n/a", invokeFormatFaceitRank(new LeetifyRankResponse())); + } + + @Test + void formatFaceitRank_combinesRankAndElo() throws Exception { + LeetifyRankResponse ranks = new LeetifyRankResponse(); + ranks.faceit = 8; + ranks.faceit_elo = 1750; + + assertEquals("8 (1750)", invokeFormatFaceitRank(ranks)); + } + + @Test + void formatFaceitRank_placeholdersMissingElo() throws Exception { + LeetifyRankResponse ranks = new LeetifyRankResponse(); + ranks.faceit = 8; + + assertEquals("8 (n/a)", invokeFormatFaceitRank(ranks)); + } + + // --------------------------------------------------------------------- + // formatNullable + // --------------------------------------------------------------------- + + @Test + void formatNullableInteger_returnsPlaceholder_whenNull() throws Exception { + assertEquals("n/a", invokeFormatNullable((Integer) null)); + } + + @Test + void formatNullableInteger_returnsValue() throws Exception { + assertEquals("42", invokeFormatNullable(42)); + } + + @Test + void formatNullableDouble_returnsPlaceholder_whenNull() throws Exception { + assertEquals("n/a", invokeFormatNullable((Double) null)); + } + + @Test + void formatNullableDouble_roundsToTwoDecimals() throws Exception { + assertEquals("3.14", invokeFormatNullable(3.14159)); + } + + // --------------------------------------------------------------------- + // comparePlayers + // --------------------------------------------------------------------- + + @Test + void comparePlayers_marksHigherStatAsWinnerWhenHigherIsBetter() throws Exception { + ResponseData playerOne = responseData("Alice", "http://avatar/alice", 20, 5, 1, 0, 0, 100); + ResponseData playerTwo = responseData("Bob", "http://avatar/bob", 10, 5, 1, 0, 0, 100); + + MessageEmbed embed = invokeComparePlayers(playerOne, playerTwo).build(); + + assertEquals("** :star: 20 ** vs 10", fieldValue(embed, "Kills")); + } + + @Test + void comparePlayers_marksLowerStatAsWinnerWhenLowerIsBetter() throws Exception { + ResponseData playerOne = responseData("Alice", "http://avatar/alice", 10, 2, 1, 0, 0, 100); + ResponseData playerTwo = responseData("Bob", "http://avatar/bob", 10, 8, 1, 0, 0, 100); + + MessageEmbed embed = invokeComparePlayers(playerOne, playerTwo).build(); + + assertEquals("** :star: 2 ** vs 8", fieldValue(embed, "Deaths")); + } + + @Test + void comparePlayers_reportsTie() throws Exception { + ResponseData playerOne = responseData("Alice", "http://avatar/alice", 10, 5, 1, 0, 0, 100); + ResponseData playerTwo = responseData("Bob", "http://avatar/bob", 10, 5, 1, 0, 0, 100); + + MessageEmbed embed = invokeComparePlayers(playerOne, playerTwo).build(); + + assertEquals("10 both.", fieldValue(embed, "Kills")); + } + + @Test + void comparePlayers_setsWinningPlayersAvatarAsImage() throws Exception { + ResponseData playerOne = responseData("Alice", "http://avatar/alice", 20, 5, 5, 0, 0, 200); + ResponseData playerTwo = responseData("Bob", "http://avatar/bob", 10, 5, 1, 0, 0, 100); + + MessageEmbed embed = invokeComparePlayers(playerOne, playerTwo).build(); + + assertEquals("http://avatar/alice", embed.getImage().getUrl()); + } + + @Test + void comparePlayers_setsNoImage_whenTiedOverall() throws Exception { + ResponseData playerOne = responseData("Alice", "http://avatar/alice", 10, 5, 1, 0, 0, 100); + ResponseData playerTwo = responseData("Bob", "http://avatar/bob", 10, 5, 1, 0, 0, 100); + + MessageEmbed embed = invokeComparePlayers(playerOne, playerTwo).build(); + + assertNull(embed.getImage()); + } + + // --------------------------------------------------------------------- + // handleLeetifyUserContext + // --------------------------------------------------------------------- + + @Test + void handleLeetifyUserContext_returnsPrivacyError_whenEventHasNoTargetMember() { + UserContextInteractionEvent event = mock(UserContextInteractionEvent.class); + when(event.getTargetMember()).thenReturn(null); + + MessageEmbed embed = service.handleLeetifyUserContext(event).build(); + + assertEquals("No stats could be loaded. (Steam Privacy Settings?)", embed.getTitle()); + } + + @Test + void handleLeetifyUserContext_returnsNoProfileError_whenNoSteamIdLinked() throws Exception { + DataService dataService = mock(DataService.class); + when(dataService.getSteamIDForDiscordID("42")).thenReturn(""); + service.dataService = dataService; + + UserContextInteractionEvent event = mockEventForMember("42"); + + MessageEmbed embed = service.handleLeetifyUserContext(event).build(); + + assertEquals("No Leetify profile could be found for this user.", embed.getTitle()); + } + + @Test + void handleLeetifyUserContext_fillsAllFields_forCompleteProfile() throws Exception { + DataService dataService = mock(DataService.class); + when(dataService.getSteamIDForDiscordID("42")).thenReturn("STEAM64"); + service.dataService = dataService; + + ExternalApiConnection connection = mock(ExternalApiConnection.class); + when(connection.getPlayerProfile("STEAM64", null)).thenReturn(leetifyProfile()); + service.connection = connection; + + UserContextInteractionEvent event = mockEventForMember("42"); + + MessageEmbed embed = service.handleLeetifyUserContext(event).build(); + + assertEquals("Kaiser's Leetify Stats", embed.getTitle()); + assertEquals("7 (2100)", fieldValue(embed, "Faceit")); + assertEquals("15000", fieldValue(embed, "Premier")); + assertEquals("Global Elite", fieldValue(embed, "Wingman")); + assertEquals("2.34", fieldValue(embed, "Leetify Rating")); + assertEquals("55.00", fieldValue(embed, "Win Rate")); + assertEquals("120", fieldValue(embed, "Played matches")); + assertEquals("0.65", fieldValue(embed, "Aim")); + } + + @Test + void handleLeetifyUserContext_omitsRatingBreakdown_whenRatingAndRanksMissing() throws Exception { + DataService dataService = mock(DataService.class); + when(dataService.getSteamIDForDiscordID("42")).thenReturn("STEAM64"); + service.dataService = dataService; + + LeetifyProfileResponse profile = leetifyProfile(); + profile.ranks = null; + profile.rating = null; + + ExternalApiConnection connection = mock(ExternalApiConnection.class); + when(connection.getPlayerProfile("STEAM64", null)).thenReturn(profile); + service.connection = connection; + + UserContextInteractionEvent event = mockEventForMember("42"); + + MessageEmbed embed = service.handleLeetifyUserContext(event).build(); + + assertEquals("n/a", fieldValue(embed, "Faceit")); + assertEquals("n/a", fieldValue(embed, "Premier")); + assertNull(findField(embed, "Aim")); + } + + // --------------------------------------------------------------------- + // helpers + // --------------------------------------------------------------------- + + private String invokeGetWingmanRankName(Integer wingmanRank) throws Exception { + Method method = CsStatsService.class.getDeclaredMethod("getWingmanRankName", Integer.class); + method.setAccessible(true); + return (String) method.invoke(service, wingmanRank); + } + + private String invokeFormatFaceitRank(LeetifyRankResponse ranks) throws Exception { + Method method = CsStatsService.class.getDeclaredMethod("formatFaceitRank", LeetifyRankResponse.class); + method.setAccessible(true); + return (String) method.invoke(service, ranks); + } + + private String invokeFormatNullable(Integer value) throws Exception { + Method method = CsStatsService.class.getDeclaredMethod("formatNullable", Integer.class); + method.setAccessible(true); + return (String) method.invoke(service, value); + } + + private String invokeFormatNullable(Double value) throws Exception { + Method method = CsStatsService.class.getDeclaredMethod("formatNullable", Double.class); + method.setAccessible(true); + return (String) method.invoke(service, value); + } + + private EmbedBuilder invokeComparePlayers(ResponseData playerOne, ResponseData playerTwo) throws Exception { + Method method = CsStatsService.class.getDeclaredMethod("comparePlayers", ResponseData.class, ResponseData.class); + method.setAccessible(true); + return (EmbedBuilder) method.invoke(service, playerOne, playerTwo); + } + + private static UserContextInteractionEvent mockEventForMember(String memberId) { + UserContextInteractionEvent event = mock(UserContextInteractionEvent.class); + Member targetMember = mock(Member.class); + when(targetMember.getId()).thenReturn(memberId); + when(event.getTargetMember()).thenReturn(targetMember); + return event; + } + + private static String fieldValue(MessageEmbed embed, String name) { + return findField(embed, name).getValue(); + } + + private static MessageEmbed.Field findField(MessageEmbed embed, String name) { + return embed.getFields().stream() + .filter(field -> name.equals(field.getName())) + .findFirst() + .orElse(null); + } + + private static ResponseData responseData(String name, String avatar, long kills, long deaths, long wins, + long planted, long defused, long damage) { + SteamUser user = new SteamUser(); + user.setPersonaname(name); + user.setAvatarmedium(avatar); + + SteamUserInfo info = new SteamUserInfo(); + info.setPlayers(List.of(user)); + + PlayerStats playerStats = new PlayerStats(); + playerStats.setStats(List.of( + stat("total_kills", kills), + stat("total_deaths", deaths), + stat("total_wins", wins), + stat("total_planted_bombs", planted), + stat("total_defused_bombs", defused), + stat("total_damage_done", damage) + )); + + ResponseData data = new ResponseData(); + data.setSteamUserInfo(info); + data.setPlayerstats(playerStats); + return data; + } + + private static SingleStat stat(String name, long value) { + SingleStat stat = new SingleStat(); + stat.setName(name); + stat.setValue(value); + return stat; + } + + private static LeetifyProfileResponse leetifyProfile() { + LeetifyProfileResponse profile = new LeetifyProfileResponse(); + profile.name = "Kaiser"; + profile.winrate = 0.55; + profile.total_matches = 120; + + LeetifyRankResponse ranks = new LeetifyRankResponse(); + ranks.faceit = 7; + ranks.faceit_elo = 2100; + ranks.premier = 15000; + ranks.wingman = 18; + ranks.leetify = 2.34; + profile.ranks = ranks; + + LeetifyRatingResponse rating = new LeetifyRatingResponse(); + rating.ct_leetify = 0.6; + rating.t_leetify = 0.5; + rating.aim = 0.65; + rating.positioning = 0.7; + rating.utility = 0.4; + rating.clutch = 0.3; + rating.opening = 0.2; + profile.rating = rating; + + return profile; + } +}