Add Discord support
This commit is contained in:
parent
feaf965859
commit
4cd9caeacb
20
pom.xml
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>
|
||||||
|
23
src/main/java/me/braydon/license/common/MiscUtils.java
Normal file
23
src/main/java/me/braydon/license/common/MiscUtils.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
25
src/main/java/me/braydon/license/common/TimeUtils.java
Normal file
25
src/main/java/me/braydon/license/common/TimeUtils.java
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
}
|
141
src/main/java/me/braydon/license/service/DiscordService.java
Normal file
141
src/main/java/me/braydon/license/service/DiscordService.java
Normal file
@ -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();
|
||||||
}
|
}
|
||||||
license.use(ip, ipsSalt, hwid); // Use the license
|
try {
|
||||||
repository.save(license); // Save the used license
|
license.use(ip, ipsSalt, hwid); // Use the license
|
||||||
log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid);
|
repository.save(license); // Save the used license
|
||||||
return 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
___________________________________________________________
|
___________________________________________________________
|
Loading…
x
Reference in New Issue
Block a user