Add Discord support

This commit is contained in:
Braydon 2023-05-31 22:30:27 -04:00
parent feaf965859
commit 4cd9caeacb
7 changed files with 322 additions and 15 deletions

20
pom.xml

@ -36,6 +36,13 @@
</exclude> </exclude>
</excludes> </excludes>
</configuration> </configuration>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@ -49,6 +56,19 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- Discord JDA -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-beta.9</version>
<exclusions>
<exclusion>
<groupId>club.minnced</groupId>
<artifactId>opus-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- BCrypt --> <!-- BCrypt -->
<dependency> <dependency>
<groupId>org.mindrot</groupId> <groupId>org.mindrot</groupId>

@ -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);
}
}

@ -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());
}
}

@ -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);
}
}

@ -3,16 +3,17 @@ package me.braydon.license.service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.braydon.license.exception.APIException; import me.braydon.license.common.MiscUtils;
import me.braydon.license.exception.LicenseExpiredException; import me.braydon.license.exception.*;
import me.braydon.license.exception.LicenseNotFoundException;
import me.braydon.license.model.License; import me.braydon.license.model.License;
import me.braydon.license.repository.LicenseRepository; import me.braydon.license.repository.LicenseRepository;
import net.dv8tion.jda.api.EmbedBuilder;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.awt.*;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -30,6 +31,11 @@ public final class LicenseService {
*/ */
@NonNull private final LicenseRepository repository; @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. * The salt to use for hashing license keys.
*/ */
@ -43,8 +49,9 @@ public final class LicenseService {
@NonNull private String ipsSalt; @NonNull private String ipsSalt;
@Autowired @Autowired
public LicenseService(@NonNull LicenseRepository repository) { public LicenseService(@NonNull LicenseRepository repository, @NonNull DiscordService discordService) {
this.repository = repository; this.repository = repository;
this.discordService = discordService;
} }
@PostConstruct @PostConstruct
@ -100,20 +107,99 @@ public final class LicenseService {
* @see License for license * @see License for license
*/ */
@NonNull @NonNull
public License check(@NonNull String key, @NonNull String product, public License check(@NonNull String key, @NonNull String product, @NonNull String ip,
@NonNull String ip, @NonNull String hwid) throws APIException { @NonNull String hwid) throws APIException {
Optional<License> optionalLicense = repository.getLicense(BCrypt.hashpw(key, licensesSalt), product); // Get the license Optional<License> optionalLicense = repository.getLicense(BCrypt.hashpw(key, licensesSalt), product); // Get the license
if (optionalLicense.isEmpty()) { // License key not found if (optionalLicense.isEmpty()) { // License key not found
log.error("License key {} for product {} not found", key, product); // Log the error log.error("License key {} for product {} not found", key, product); // Log the error
throw new LicenseNotFoundException(); throw new LicenseNotFoundException();
} }
License license = optionalLicense.get(); // The license found 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" : "<t:" + expirationDate + ":R>",
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(); throw new LicenseExpiredException();
} }
try {
license.use(ip, ipsSalt, hwid); // Use the license license.use(ip, ipsSalt, hwid); // Use the license
repository.save(license); // Save the used license repository.save(license); // Save the used license
log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid); log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid);
return license; 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
}
} }
} }

@ -9,6 +9,16 @@ salts:
licenses: "$2a$10$/nQyzQDMkCf97ZlJLLWa3O" licenses: "$2a$10$/nQyzQDMkCf97ZlJLLWa3O"
ips: "$2a$10$Xus.AHTCas97Ofx0tFs85O" 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 # Log Configuration
logging: logging:
file: file:
@ -16,14 +26,16 @@ logging:
# Spring Configuration # Spring Configuration
spring: spring:
application:
name: "License Server"
# Database Configuration
data: data:
# MongoDB - This is used to store persistent data # MongoDB - This is used to store persistent data
mongodb: mongodb:
uri: "mongodb://licenseServer:p4$$w0rd@127.0.0.1:27017/licenseServer?authSource=admin" uri: "mongodb://licenseServer:p4$$w0rd@127.0.0.1:27017/licenseServer?authSource=admin"
auto-index-creation: true # Automatically create collection indexes auto-index-creation: true # Automatically create collection indexes
# Ignore # Banner
application:
name: "License Server"
banner: banner:
location: "classpath:banner.txt" location: "classpath:banner.txt"

@ -3,6 +3,6 @@
| |__| / _/ -_) ' \(_-</ -_) \__ \/ -_) '_\ V / -_) '_| | |__| / _/ -_) ' \(_-</ -_) \__ \/ -_) '_\ V / -_) '_|
|____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_| |____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
| API Version - v${application.version} | Application Version - v${application.version}
| Spring Version - ${spring-boot.formatted-version} | Spring Version - ${spring-boot.formatted-version}
___________________________________________________________ ___________________________________________________________