From 4cd9caeacb0967911398e5ce135ab771e737adca Mon Sep 17 00:00:00 2001 From: Braydon Date: Wed, 31 May 2023 22:30:27 -0400 Subject: [PATCH] Add Discord support --- pom.xml | 20 +++ .../me/braydon/license/common/MiscUtils.java | 23 +++ .../me/braydon/license/common/TimeUtils.java | 25 ++++ .../license/service/DiscordService.java | 141 ++++++++++++++++++ .../license/service/LicenseService.java | 108 ++++++++++++-- src/main/resources/application.yml | 18 ++- src/main/resources/banner.txt | 2 +- 7 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 src/main/java/me/braydon/license/common/MiscUtils.java create mode 100644 src/main/java/me/braydon/license/common/TimeUtils.java create mode 100644 src/main/java/me/braydon/license/service/DiscordService.java diff --git a/pom.xml b/pom.xml index a3dbd6b..3b10050 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,13 @@ + + + + build-info + + + @@ -49,6 +56,19 @@ provided + + + net.dv8tion + JDA + 5.0.0-beta.9 + + + club.minnced + opus-java + + + + org.mindrot diff --git a/src/main/java/me/braydon/license/common/MiscUtils.java b/src/main/java/me/braydon/license/common/MiscUtils.java new file mode 100644 index 0000000..26e6569 --- /dev/null +++ b/src/main/java/me/braydon/license/common/MiscUtils.java @@ -0,0 +1,23 @@ +package me.braydon.license.common; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +/** + * @author Braydon + */ +@UtilityClass +public final class MiscUtils { + /** + * Obfuscate the given key. + * + * @param rawKey the key to obfuscate + * @return the obfuscated key + */ + @NonNull + public static String obfuscateKey(@NonNull String rawKey) { + int length = 9; // The amount of chars to show + String key = rawKey.substring(0, length); + return key + "*".repeat(rawKey.length() - length); + } +} diff --git a/src/main/java/me/braydon/license/common/TimeUtils.java b/src/main/java/me/braydon/license/common/TimeUtils.java new file mode 100644 index 0000000..4908469 --- /dev/null +++ b/src/main/java/me/braydon/license/common/TimeUtils.java @@ -0,0 +1,25 @@ +package me.braydon.license.common; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author Braydon + */ +@UtilityClass +public final class TimeUtils { + private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); + + /** + * Get the current date time. + * + * @return the current date time + */ + @NonNull + public static String dateTime() { + return DATE_TIME_FORMAT.format(new Date()); + } +} diff --git a/src/main/java/me/braydon/license/service/DiscordService.java b/src/main/java/me/braydon/license/service/DiscordService.java new file mode 100644 index 0000000..6e356b1 --- /dev/null +++ b/src/main/java/me/braydon/license/service/DiscordService.java @@ -0,0 +1,141 @@ +package me.braydon.license.service; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import me.braydon.license.common.TimeUtils; +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.channel.concrete.TextChannel; +import net.dv8tion.jda.api.requests.GatewayIntent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.info.BuildProperties; +import org.springframework.stereotype.Service; + +/** + * @author Braydon + */ +@Service +@Slf4j +public final class DiscordService { + /** + * The version of this Springboot application. + */ + @NonNull private final String applicationVersion; + /** + * The name of this Springboot application. + */ + @Value("${spring.application.name}") + @NonNull private String applicationName; + /** + * The token to the Discord bot. + */ + @Value("${discord.token}") + @NonNull private String token; + + /** + * The channel ID to log to. + */ + @Value("${discord.logs.channel}") + private long logsChannel; + + /** + * Should used licenses be logged? + */ + @Value("${discord.logs.uses}") @Getter + private boolean logUses; + + /** + * Should we log if an expired license was used? + */ + @Value("${discord.logs.expired}") @Getter + private boolean logExpired; + + /** + * Should IP limited licenses be logged when used? + */ + @Value("${discord.logs.expired}") @Getter + private boolean logIpLimitExceeded; + + /** + * Should HWID limited licenses be logged when used? + */ + @Value("${discord.logs.expired}") @Getter + private boolean logHwidLimitExceeded; + + /** + * The {@link JDA} instance of the bot. + */ + private JDA jda; + + @Autowired + public DiscordService(@NonNull BuildProperties buildProperties) { + this.applicationVersion = buildProperties.getVersion(); + } + + @PostConstruct @SneakyThrows + public void onInitialize() { + // No token was provided + if (token.trim().isEmpty()) { + log.info("Not using Discord, no token provided"); + return; + } + // Initialize the bot + long before = System.currentTimeMillis(); + log.info("Logging in..."); // Log that we're logging in + jda = JDABuilder.createDefault(token) + .enableIntents( + GatewayIntent.GUILD_MEMBERS + ).setStatus(OnlineStatus.DO_NOT_DISTURB) + .setActivity(Activity.watching("your licenses")) + .build(); + jda.awaitReady(); // Await JDA to be ready + + // Log that we're logged in + log.info("Logged into {} in {}ms", + jda.getSelfUser().getAsTag(), System.currentTimeMillis() - before + ); + } + + /** + * Send a log to the logs channel + * with the given embed. + * + * @param embed the embed to send + * @see TextChannel for channel + * @see EmbedBuilder for embed + */ + public void sendLog(@NonNull EmbedBuilder embed) { + // JDA must be ready to send logs + if (!isReady()) { + return; + } + // Not enabled + if (logsChannel <= 0L) { + return; + } + TextChannel textChannel = jda.getTextChannelById(logsChannel); // Get the logs channel + if (textChannel == null) { // We must have a logs channel + 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(); + } + + /** + * Check if the bot is ready. + * + * @return true if ready, otherwise false + */ + public boolean isReady() { + return jda != null && (jda.getStatus() == JDA.Status.CONNECTED); + } +} \ 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 54596f2..c2aec2f 100644 --- a/src/main/java/me/braydon/license/service/LicenseService.java +++ b/src/main/java/me/braydon/license/service/LicenseService.java @@ -3,16 +3,17 @@ package me.braydon.license.service; import jakarta.annotation.PostConstruct; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import me.braydon.license.exception.APIException; -import me.braydon.license.exception.LicenseExpiredException; -import me.braydon.license.exception.LicenseNotFoundException; +import me.braydon.license.common.MiscUtils; +import me.braydon.license.exception.*; import me.braydon.license.model.License; import me.braydon.license.repository.LicenseRepository; +import net.dv8tion.jda.api.EmbedBuilder; import org.mindrot.jbcrypt.BCrypt; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.awt.*; import java.util.Date; import java.util.HashSet; import java.util.Optional; @@ -30,6 +31,11 @@ public final class LicenseService { */ @NonNull private final LicenseRepository repository; + /** + * The {@link DiscordService} to use for logging. + */ + @NonNull private final DiscordService discordService; + /** * The salt to use for hashing license keys. */ @@ -43,8 +49,9 @@ public final class LicenseService { @NonNull private String ipsSalt; @Autowired - public LicenseService(@NonNull LicenseRepository repository) { + public LicenseService(@NonNull LicenseRepository repository, @NonNull DiscordService discordService) { this.repository = repository; + this.discordService = discordService; } @PostConstruct @@ -100,20 +107,99 @@ public final class LicenseService { * @see License for license */ @NonNull - public License check(@NonNull String key, @NonNull String product, - @NonNull String ip, @NonNull String hwid) throws APIException { + public License check(@NonNull String key, @NonNull String product, @NonNull String ip, + @NonNull String hwid) throws APIException { Optional optionalLicense = repository.getLicense(BCrypt.hashpw(key, licensesSalt), product); // Get the license if (optionalLicense.isEmpty()) { // License key not found log.error("License key {} for product {} not found", key, product); // Log the error throw new LicenseNotFoundException(); } License license = optionalLicense.get(); // The license found - if (license.hasExpired()) { // The license has expired + + // Log the license being used, if enabled + if (discordService.isLogUses()) { + // god i hate sending discord embeds, it's so big and ugly :( + long expirationDate = (license.getCreated().getTime() + license.getDuration()) / 1000L; + discordService.sendLog(new EmbedBuilder() + .setColor(Color.BLUE) + .setTitle("License Used") + .addField("License", + "`" + MiscUtils.obfuscateKey(key) + "`", + true + ) + .addField("Product", + license.getProduct(), + true + ) + .addField("Description", + license.getDescription(), + true + ) + .addField("Owner ID", + "504147739131641857", + true + ) + .addField("Expires", + license.isPermanent() ? "Never" : "", + true + ) + .addField("IP", + ip, + true + ) + .addField("HWID", + "```" + hwid + "```", + false + ) + .addField("IPs", + license.getIps().size() + "/" + license.getIpLimit(), + true + ) + .addField("HWIDs", + license.getHwids().size() + "/" + license.getHwidLimit(), + true + ) + ); + } + // The license has expired + if (license.hasExpired()) { + // Log the expired license + if (discordService.isLogExpired()) { + discordService.sendLog(new EmbedBuilder() + .setColor(Color.RED) + .setTitle("License Expired") + .setDescription("License `%s` is expired".formatted(MiscUtils.obfuscateKey(key))) + ); + } throw new LicenseExpiredException(); } - license.use(ip, ipsSalt, hwid); // Use the license - repository.save(license); // Save the used license - log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid); - return license; + try { + license.use(ip, ipsSalt, hwid); // Use the license + repository.save(license); // Save the used license + log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid); + return license; + } catch (APIException ex) { + // Log that the license has reached it's IP limit + if (ex instanceof LicenseIpLimitExceededException && discordService.isLogIpLimitExceeded()) { + discordService.sendLog(new EmbedBuilder() + .setColor(Color.RED) + .setTitle("License IP Limit Reached") + .setDescription("License `%s` has reached it's IP limit: **%s**".formatted( + MiscUtils.obfuscateKey(key), + license.getIpLimit() + )) + ); + } else if (ex instanceof LicenseHwidLimitExceededException && discordService.isLogHwidLimitExceeded()) { + discordService.sendLog(new EmbedBuilder() + .setColor(Color.RED) + .setTitle("License HWID Limit Reached") + .setDescription("License `%s` has reached it's HWID limit: **%s**".formatted( + MiscUtils.obfuscateKey(key), + license.getHwidLimit() + )) + ); + } + throw ex; // Rethrow to handle where this method was invoked + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 05a399e..220c063 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,16 @@ salts: licenses: "$2a$10$/nQyzQDMkCf97ZlJLLWa3O" ips: "$2a$10$Xus.AHTCas97Ofx0tFs85O" +# Discord Bot Configuration +discord: + token: "" + logs: + channel: 0 # The channel ID to log to, leave as 0 to disable + uses: true # Should used licenses be logged? + expired: true # Should we log if an expired license was used? + ipLimitExceeded: true # Should IP limited licenses be logged when used? + hwidLimitExceeded: true # Should HWID limited licenses be logged when used? + # Log Configuration logging: file: @@ -16,14 +26,16 @@ logging: # Spring Configuration spring: + application: + name: "License Server" + + # Database Configuration data: # MongoDB - This is used to store persistent data mongodb: uri: "mongodb://licenseServer:p4$$w0rd@127.0.0.1:27017/licenseServer?authSource=admin" auto-index-creation: true # Automatically create collection indexes - # Ignore - application: - name: "License Server" + # Banner banner: location: "classpath:banner.txt" \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index 7e29760..652a42a 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -3,6 +3,6 @@ | |__| / _/ -_) ' \(_-