diff --git a/src/main/java/me/braydon/license/service/DiscordService.java b/src/main/java/me/braydon/license/service/DiscordService.java index 796e561..f53a43b 100644 --- a/src/main/java/me/braydon/license/service/DiscordService.java +++ b/src/main/java/me/braydon/license/service/DiscordService.java @@ -6,12 +6,16 @@ import lombok.NonNull; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import me.braydon.license.common.TimeUtils; +import me.braydon.license.model.License; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.GatewayIntent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -33,6 +37,7 @@ public final class DiscordService { */ @Value("${spring.application.name}") @NonNull private String applicationName; + /** * The token to the Discord bot. */ @@ -69,6 +74,24 @@ public final class DiscordService { @Value("${discord.logs.expired}") @Getter private boolean logHwidLimitExceeded; + /** + * Should new IPs be sent to the license owner? + */ + @Value("${discord.owner-logs.newIp}") @Getter + private boolean logNewIpsToOwner; + + /** + * Should new HWIDs be sent to the license owner? + */ + @Value("${discord.owner-logs.newHwid}") @Getter + private boolean logNewHwidsToOwner; + + /** + * Should the license owner be notified when their license is about to expire? + */ + @Value("${discord.owner-logs.expiringSoon}") @Getter + private boolean logExpiringSoonToOwner; + /** * The {@link JDA} instance of the bot. */ @@ -125,9 +148,33 @@ public final class DiscordService { throw new IllegalArgumentException("Log channel %s wasn't found".formatted(logsChannel)); } // Send the log - textChannel.sendMessageEmbeds(embed.setFooter("%s v%s - %s".formatted( - applicationName, applicationVersion, TimeUtils.dateTime() - )).build()).queue(); + textChannel.sendMessageEmbeds(buildEmbed(embed)).queue(); + } + + public void sendOwnerLog(@NonNull License license, @NonNull EmbedBuilder embed) { + // We need an owner for the license + if (license.getOwnerSnowflake() <= 0L) { + return; + } + // Lookup the owner of the license + jda.retrieveUserById(license.getOwnerSnowflake()).queue(owner -> { + if (owner == null) { // Couldn't locate the owner of the license + return; + } + owner.openPrivateChannel().queue(channel -> { + channel.sendMessageEmbeds(buildEmbed(embed)).queue(null, ex -> { + // Ignore the ex if the owner has priv msgs turned off, we don't care + if (((ErrorResponseException) ex).getErrorResponse() != ErrorResponse.CANNOT_SEND_TO_USER) { + ex.printStackTrace(); + } + }); + }); + }, ex -> { + // Ignore the ex if the owner isn't found, we don't care + if (((ErrorResponseException) ex).getErrorResponse() != ErrorResponse.UNKNOWN_USER) { + ex.printStackTrace(); + } + }); } /** @@ -138,4 +185,17 @@ public final class DiscordService { public boolean isReady() { return jda != null && (jda.getStatus() == JDA.Status.CONNECTED); } + + /** + * Build the given embed. + * + * @param embedBuilder the embed builder + * @return the built embed + */ + @NonNull + private MessageEmbed buildEmbed(@NonNull EmbedBuilder embedBuilder) { + return embedBuilder.setFooter("%s v%s - %s".formatted( + applicationName, applicationVersion, TimeUtils.dateTime() + )).build(); + } } \ No newline at end of file diff --git a/src/main/java/me/braydon/license/service/LicenseService.java b/src/main/java/me/braydon/license/service/LicenseService.java index bda4269..21e6a19 100644 --- a/src/main/java/me/braydon/license/service/LicenseService.java +++ b/src/main/java/me/braydon/license/service/LicenseService.java @@ -107,12 +107,13 @@ public final class LicenseService { } License license = optionalLicense.get(); // The license found String hashedIp = BCrypt.hashpw(ip, ipsSalt); // Hash the IP + String obfuscateKey = MiscUtils.obfuscateKey(key); + boolean newIp = !license.getIps().contains(hashedIp); // Is the IP new? + boolean newHwid = !license.getHwids().contains(hwid); // Is the HWID new? // Log the license being used, if enabled if (discordService.isLogUses()) { // god i hate sending discord embeds, it's so big and ugly :( - boolean newIp = !license.getIps().contains(hashedIp); // Is the IP new? - boolean newHwid = !license.getHwids().contains(hwid); // Is the HWID new? // Constructing tags StringBuilder tags = new StringBuilder(); @@ -126,21 +127,14 @@ public final class LicenseService { tags.append("HWID"); } long expirationDate = (license.getCreated().getTime() + license.getDuration()) / 1000L; + int ipCount = license.getIps().size(); + int hwidCount = license.getHwids().size(); discordService.sendLog(new EmbedBuilder() .setColor(Color.BLUE) .setTitle("License Used" + (!tags.isEmpty() ? " (" + tags + ")" : "")) - .addField("License", - "`" + MiscUtils.obfuscateKey(key) + "`", - true - ) - .addField("Product", - license.getProduct(), - true - ) - .addField("Description", - license.getDescription(), - true - ) + .addField("License", "`" + obfuscateKey + "`", true) + .addField("Product", license.getProduct(), true) + .addField("Description", license.getDescription(), true) .addField("Owner ID", license.getOwnerSnowflake() <= 0L ? "N/A" : String.valueOf(license.getOwnerSnowflake()), true @@ -153,20 +147,14 @@ public final class LicenseService { license.isPermanent() ? "Never" : "", true ) - .addField("IP", - ip, - true - ) - .addField("HWID", - "```" + hwid + "```", - false - ) + .addField("IP", ip, true) + .addField("HWID", "```" + hwid + "```", false) .addField("IPs", - license.getIps().size() + "/" + license.getIpLimit(), + (newIp ? ipCount + 1 : ipCount) + "/" + license.getIpLimit(), true ) .addField("HWIDs", - license.getHwids().size() + "/" + license.getHwidLimit(), + (newHwid ? hwidCount + 1 : hwidCount) + "/" + license.getHwidLimit(), true ) ); @@ -178,13 +166,37 @@ public final class LicenseService { discordService.sendLog(new EmbedBuilder() .setColor(Color.RED) .setTitle("License Expired") - .setDescription("License `%s` is expired".formatted(MiscUtils.obfuscateKey(key))) + .setDescription("License `%s` is expired".formatted(obfuscateKey)) ); } throw new LicenseExpiredException(); } + + // Sending new IP log to the license owner + if (newIp && discordService.isLogNewIpsToOwner()) { + discordService.sendOwnerLog(license, new EmbedBuilder() + .setColor(0xF2781B) + .setTitle("New IP") + .setDescription("One of your licenses has been used on a new IP:") + .addField("License", "`" + obfuscateKey + "`", true) + .addField("Product", license.getProduct(), true) + .addField("IP", "```" + ip + "```", false) + ); + } + // Sending new HWID log to the license owner + if (newHwid && discordService.isLogNewHwidsToOwner()) { + discordService.sendOwnerLog(license, new EmbedBuilder() + .setColor(0xF2781B) + .setTitle("New HWID") + .setDescription("One of your licenses has been used on a new HWID:") + .addField("License", "`" + obfuscateKey + "`", true) + .addField("Product", license.getProduct(), true) + .addField("HWID", "```" + hwid + "```", false) + ); + } + // Use the license try { - license.use(hashedIp, hwid); // Use the license + license.use(hashedIp, hwid); repository.save(license); // Save the used license log.info("License key '{}' for product '{}' was used by {} (HWID: {})", key, product, ip, hwid); return license; @@ -195,7 +207,7 @@ public final class LicenseService { .setColor(Color.RED) .setTitle("License IP Limit Reached") .setDescription("License `%s` has reached it's IP limit: **%s**".formatted( - MiscUtils.obfuscateKey(key), + obfuscateKey, license.getIpLimit() )) ); @@ -204,7 +216,7 @@ public final class LicenseService { .setColor(Color.RED) .setTitle("License HWID Limit Reached") .setDescription("License `%s` has reached it's HWID limit: **%s**".formatted( - MiscUtils.obfuscateKey(key), + obfuscateKey, license.getHwidLimit() )) ); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 220c063..d63bf7e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,8 @@ salts: # Discord Bot Configuration discord: token: "" + + # Global Logs logs: channel: 0 # The channel ID to log to, leave as 0 to disable uses: true # Should used licenses be logged? @@ -19,6 +21,12 @@ discord: ipLimitExceeded: true # Should IP limited licenses be logged when used? hwidLimitExceeded: true # Should HWID limited licenses be logged when used? + # License Owner Logs + owner-logs: + newIp: true # Should new IPs be sent to the license owner? + newHwid: true # Should new HWIDs be sent to the license owner? + expiringSoon: true # Should the license owner be notified when their license is about to expire? + # Log Configuration logging: file: