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 @@
| |__| / _/ -_) ' \(_- -_) \__ \/ -_) '_\ V / -_) '_|
|____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
- | API Version - v${application.version}
+ | Application Version - v${application.version}
| Spring Version - ${spring-boot.formatted-version}
___________________________________________________________
\ No newline at end of file