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..92556db 100644 --- a/src/main/java/StartUp.java +++ b/src/main/java/StartUp.java @@ -2,27 +2,27 @@ 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; 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); @@ -40,14 +40,13 @@ 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(); - } 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/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/ExternalApiConnection.java b/src/main/java/ch/yoinc/http/ExternalApiConnection.java new file mode 100644 index 0000000..a2c6c4c --- /dev/null +++ b/src/main/java/ch/yoinc/http/ExternalApiConnection.java @@ -0,0 +1,127 @@ +package ch.yoinc.http; + +import ch.yoinc.model.leetify.LeetifyMatchResponse; +import ch.yoinc.model.leetify.LeetifyProfileResponse; +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; +import com.google.gson.stream.JsonWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.time.Instant; +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; + 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 ExternalApiConnection(Properties properties) { + this.properties = properties; + this.apiKey = properties.getProperty("leetify.apiToken"); + gson = new GsonBuilder() + .registerTypeAdapter(Instant.class, new TypeAdapter() { + @Override + public void write(JsonWriter out, Instant value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.toString()); + } + } + + @Override + public Instant read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return Instant.parse(in.nextString()); + } + }) + .create(); + 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()); + 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) throws InterruptedException, IOException { + String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; + HttpRequest request; + request = HttpRequest.newBuilder() + .uri(URI.create(LEETIFY_API + "/v3/profile?" + parameter)) + .header("Authorization", "Bearer " + apiKey) + .GET() + .build(); + + 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) throws InterruptedException, IOException { + String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; + HttpRequest request; + request = HttpRequest.newBuilder() + .uri(URI.create(LEETIFY_API + "/v3/profile/matches?" + parameter)) + .header("Authorization", "Bearer " + apiKey) + .GET() + .build(); + + 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/http/LeetifyConnection.java b/src/main/java/ch/yoinc/http/LeetifyConnection.java deleted file mode 100644 index ba84869..0000000 --- a/src/main/java/ch/yoinc/http/LeetifyConnection.java +++ /dev/null @@ -1,99 +0,0 @@ -package ch.yoinc.http; - -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.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -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.time.Instant; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Properties; - -public class LeetifyConnection { - - private final String apiKey; - private final HttpClient client; - private final Gson gson; - private final String LEETIFY_API = "https://api-public.cs-prod.leetify.com"; - - - public LeetifyConnection(Properties properties) { - this.apiKey = properties.getProperty("leetify.apiToken"); - gson = new GsonBuilder() - .registerTypeAdapter(Instant.class, new TypeAdapter() { - @Override - public void write(JsonWriter out, Instant value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.toString()); - } - } - - @Override - public Instant read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - return Instant.parse(in.nextString()); - } - }) - .create(); - this.client = HttpClient.newHttpClient(); - } - - public LeetifyProfileResponse getPlayerProfile(String steam64ID, String leetifyID) { - String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; - HttpRequest request; - request = HttpRequest.newBuilder() - .uri(URI.create(LEETIFY_API + "/v3/profile?" + parameter)) - .header("Authorization", "Bearer " + apiKey) - .GET() - .build(); - - try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - return gson.fromJson(response.body(), LeetifyProfileResponse.class); - } 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()); - } - return null; - } - - public List getPlayerMatchHistory(String steam64ID, String leetifyID) { - String parameter = steam64ID == null ? "id=" + leetifyID : "steam64_id=" + steam64ID; - HttpRequest request; - request = HttpRequest.newBuilder() - .uri(URI.create(LEETIFY_API + "/v3/profile/matches?" + parameter)) - .header("Authorization", "Bearer " + apiKey) - .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 - LeetifyConnection - " + 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()); - } - return null; - } -} 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 4d2f898..d5169c3 100644 --- a/src/main/java/ch/yoinc/services/CsStatsService.java +++ b/src/main/java/ch/yoinc/services/CsStatsService.java @@ -1,32 +1,43 @@ package ch.yoinc.services; -import com.google.gson.JsonSyntaxException; import ch.yoinc.http.CarthageException; -import ch.yoinc.http.ConnectionBuilder; +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; 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; +import org.slf4j.LoggerFactory; import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; public class CsStatsService { - ResourceBundle resourceBundle; - //I know. I don't like it either. - private int winsOne; - private int winsTwo; - ConnectionBuilder connectionBuilder; + 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; + 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) { @@ -35,14 +46,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")); } } @@ -50,34 +61,121 @@ 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")); } } - private EmbedBuilder comparePlayers(ResponseData playerOneData, ResponseData playerTwoData) { - EmbedBuilder embedBuilder = new EmbedBuilder(); - winsOne = 0; - winsTwo = 0; + public EmbedBuilder handleLeetifyUserContext(UserContextInteractionEvent event) { + resourceBundle = ResourceBundle.getBundle("localization", Locale.of("en")); + try { + 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", 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", 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", 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); + } + + return returnEmbed; + } + } 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")); + } + 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 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(); + } - embedBuilder.setTitle(resourceBundle.getString("compare.title").replace("%s", playerOneData.getSteamUserInfo().getPlayers().getFirst().getPersonaname()).replace("%t", playerTwoData.getSteamUserInfo().getPlayers().getFirst().getPersonaname())) + 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) { + 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), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.deaths"), getWinner(playerOneData, playerTwoData, "total_deaths", false), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.wins"), getWinner(playerOneData, playerTwoData, "total_wins", true), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.planted"), getWinner(playerOneData, playerTwoData, "total_planted_bombs", true), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.defused"), getWinner(playerOneData, playerTwoData, "total_defused_bombs", true), true)) - .addField(new MessageEmbed.Field(resourceBundle.getString("stats.damage"), getWinner(playerOneData, playerTwoData, "total_damage_done", true), true)); + .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()); @@ -87,36 +185,33 @@ private EmbedBuilder comparePlayers(ResponseData playerOneData, ResponseData pla return embedBuilder; } - private String getWinner(ResponseData playerOneData, ResponseData playerTwoData, String statName, boolean higherRequired) { + private ComparisonResult getWinner(ResponseData playerOneData, ResponseData playerTwoData, String statName, boolean higherRequired) { long playerOneLong = playerOneData.getLongStatsForName(statName); long playerTwoLong = playerTwoData.getLongStatsForName(statName); - if (higherRequired) { - return getString(playerOneLong, playerTwoLong, playerOneLong > playerTwoLong, playerTwoLong > playerOneLong); - } else { - return getString(playerOneLong, playerTwoLong, playerOneLong < playerTwoLong, playerTwoLong < playerOneLong); - } + boolean playerOneWins = higherRequired ? playerOneLong > playerTwoLong : playerOneLong < playerTwoLong; + boolean playerTwoWins = higherRequired ? playerTwoLong > playerOneLong : playerTwoLong < playerOneLong; + + return getString(playerOneLong, playerTwoLong, playerOneWins, playerTwoWins); } @NotNull - private String getString(long playerOneLong, long playerTwoLong, boolean b, boolean b2) { - if (b) { - winsOne++; - return "** :star: " + playerOneLong + " ** vs " + playerTwoLong; - } else if (b2) { - winsTwo++; - return playerOneLong + " vs ** " + playerTwoLong + " ** :star: "; + private ComparisonResult getString(long playerOneLong, long playerTwoLong, boolean playerOneWins, boolean playerTwoWins) { + if (playerOneWins) { + return new ComparisonResult("** :star: " + playerOneLong + " ** vs " + playerTwoLong, ComparisonResult.PLAYER_ONE); + } else if (playerTwoWins) { + 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); } } - 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); if (StringUtils.isNotEmpty(steamID)) { - responseData = connectionBuilder.fetchSteamUserStats(steamID); + responseData = connection.fetchSteamUserStats(steamID); } return responseData; } 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/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..ded0290 100644 --- a/src/main/java/ch/yoinc/tasks/LeetifyTask.java +++ b/src/main/java/ch/yoinc/tasks/LeetifyTask.java @@ -1,6 +1,7 @@ package ch.yoinc.tasks; -import ch.yoinc.http.LeetifyConnection; +import ch.yoinc.http.CarthageException; +import ch.yoinc.http.ExternalApiConnection; import ch.yoinc.model.internal.InternalUser; import ch.yoinc.model.leetify.LeetifyMatchResponse; import ch.yoinc.model.leetify.LeetifyPlayerStatsResponse; @@ -8,102 +9,112 @@ 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 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()); - 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: " + formatRating(stats.leetify_rating) + - "\nCT Rating: " + formatRating(stats.ct_leetify_rating) + - "\nT Rating: " + 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", "matchmaking_competitive" -> 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", "matchmaking_competitive" -> 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", 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); + 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); } } 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"; @@ -120,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 = leetifyConnection.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; } @@ -149,11 +164,4 @@ private HashMap> setNewlyPlayedMatches(List + + + %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/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; + } +} diff --git a/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java b/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java index c2fdf80..3671a99 100644 --- a/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java +++ b/src/test/java/ch/yoinc/tasks/LeetifyTaskTest.java @@ -1,10 +1,11 @@ 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; 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()); } // --------------------------------------------------------------------- @@ -108,9 +110,9 @@ void formatRating_returnsPlaceholder_whenRatingIsMissing() throws Exception { @Timeout(value = 5, unit = TimeUnit.SECONDS) void setNewlyPlayedMatches_returnsEmptyMap_whenUserHasNoMatchHistory() throws Exception { DataService dataService = mock(DataService.class); - LeetifyConnection leetifyConnection = mock(LeetifyConnection.class); - when(leetifyConnection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(null); - injectDependencies(dataService, leetifyConnection); + ExternalApiConnection connection = mock(ExternalApiConnection.class); + when(connection.getPlayerMatchHistory(eq("STEAM1"), isNull())).thenReturn(null); + injectDependencies(dataService, connection); // the method sleeps 10s (real) after every user; pre-interrupting this thread makes // that Thread.sleep() throw immediately instead of actually waiting @@ -127,14 +129,14 @@ void setNewlyPlayedMatches_returnsEmptyMap_whenUserHasNoMatchHistory() throws Ex @Timeout(value = 5, unit = TimeUnit.SECONDS) void setNewlyPlayedMatches_onlyIncludesMatchesReturnedAsNew() throws Exception { DataService dataService = mock(DataService.class); - LeetifyConnection leetifyConnection = mock(LeetifyConnection.class); + ExternalApiConnection connection = mock(ExternalApiConnection.class); LeetifyMatchResponse oldMatch = match("old-match", "Alice"); LeetifyMatchResponse newMatch = match("new-match", "Alice"); 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 +152,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 @@ -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") @@ -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("connection", connection); } private void setPrivateField(String name, Object value) throws Exception { 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 @@ + + + +