Compare commits

72 Commits

Author SHA1 Message Date
34102e9b22 Merge branch 'master' of https://git.fascinated.cc/Fascinated/Bat
# Conflicts:
#	pom.xml
2024-07-03 18:55:48 -05:00
7083bebef1 impl drag feature 2024-07-04 00:54:16 +01:00
c81835cb2d funny git merge 2024-07-03 18:46:42 -05:00
80e7afedea Silly extra space 2024-07-03 18:44:26 -05:00
285a0ca00a Merge branch 'master' of https://git.fascinated.cc/Fascinated/Bat
# Conflicts:
#	src/main/java/cc/fascinated/bat/command/Category.java
2024-07-03 18:44:09 -05:00
f07e30d843 Add support for TMDB movie + series lookup 2024-07-03 18:41:58 -05:00
bd9ac1e138 maybe fix npe?? 2024-07-03 23:23:23 +01:00
3878d3029b mention voice channel in the embed 2024-07-03 23:09:28 +01:00
831bc934b4 cleanup voice logs 2024-07-03 23:07:50 +01:00
938005f6d9 add null check 2024-07-03 23:02:42 +01:00
5959b814a7 use proper embed color for voice channel logs 2024-07-03 23:01:56 +01:00
5f75302f3a don't make accounts for bots 2024-07-03 22:59:05 +01:00
a7a7bc784b impl voice join and leave logging 2024-07-03 22:57:51 +01:00
2b4980fb10 change auto role log to include the guild 2024-07-03 22:18:51 +01:00
655662c6f8 fix embed color 2024-07-03 22:18:35 +01:00
642185f8c5 fix welcomer serialization 2024-07-03 22:12:56 +01:00
c2e447f416 fix welcomer placeholders and fix channel command 2024-07-03 22:10:35 +01:00
271a1cf88d fix npe 2024-07-03 22:04:40 +01:00
11e7ca4aa6 impl purge command 2024-07-03 21:43:29 +01:00
f6834db9cb add 8ball command 2024-07-03 20:19:21 +01:00
90aaf5422f fix some messages 2024-07-03 19:53:47 +01:00
e4183b4882 impl welcomer feature 2024-07-03 19:49:19 +01:00
f62a022ed5 logging messages 2024-07-03 16:51:36 +01:00
50b8b4b2c1 Merge remote-tracking branch 'origin/master' 2024-07-03 16:35:40 +01:00
920755eae0 maybe fix member leave?? 2024-07-03 16:34:52 +01:00
Lee
2255b02a60 update vote cmd 2024-07-03 01:09:50 +00:00
f30697d1a6 fix null on old avatar in logs 2024-07-03 02:03:26 +01:00
83250d2c08 add check to see if the member is in the guild before logging some events 2024-07-03 01:59:53 +01:00
d7916ad24a oopsie, log after command was ran 2024-07-03 00:41:28 +01:00
295d673d06 fix cmd execution log 2024-07-03 00:40:50 +01:00
da06a01097 fix WebRequest#getAsEntity 2024-07-03 00:39:20 +01:00
6202aa6691 update member join log 2024-07-03 00:35:58 +01:00
82a87c79b2 add execution time for commands 2024-07-03 00:34:10 +01:00
e795d542b9 update max reminder time 2024-07-03 00:33:07 +01:00
162d7af46b fix max reminder length 2024-07-03 00:32:14 +01:00
821190a144 fix reminder message 2024-07-03 00:30:27 +01:00
cb35182c6a update reminder message 2024-07-03 00:29:50 +01:00
ac499898e3 fix 2024-07-03 00:23:46 +01:00
35596b720b maybe fix a NPE?? 2024-07-03 00:14:00 +01:00
048d2856f9 impl reminders 2024-07-03 00:10:02 +01:00
5f654f9ca6 update vote command 2024-07-02 21:41:13 +01:00
4540bdef99 add tos and privacy policy to the help cmd 2024-07-02 21:30:10 +01:00
eda4eb5973 temp tos and privacy policy 2024-07-02 21:17:37 +01:00
68a9a6dc48 impl member avatar update logging 2024-07-02 21:05:20 +01:00
ff23ea1d6c impl global name and username update logging 2024-07-02 21:03:40 +01:00
982c038b07 don't show clickable channel on channel delete log 2024-07-02 20:07:34 +01:00
52223b5233 add avatar to member join log 2024-07-02 20:06:41 +01:00
45755503a7 show account age on member join log 2024-07-02 20:04:27 +01:00
194a5d8119 fix timeout millis 2024-07-02 19:57:25 +01:00
120afee73b impl member timeout logging 2024-07-02 19:55:50 +01:00
8b7340715c impl member ban and unban logging 2024-07-02 19:47:51 +01:00
38bde93d16 update channel log messages 2024-07-02 19:43:27 +01:00
a3ffaf1ab9 impl channel create and delete logging 2024-07-02 19:24:48 +01:00
86c147f359 merge role add/remove into 1 update type and rename nickname logtype 2024-07-02 19:09:01 +01:00
deb93e442c impl role add/remove logging 2024-07-02 19:05:21 +01:00
6eca92b4cf add member nickname update log 2024-07-02 18:58:50 +01:00
3f93df131d looks too messy with this 2024-07-02 18:47:28 +01:00
1028dca15a add more data to the logs 2024-07-02 18:36:17 +01:00
e03aef0ad5 impl member join and leave logs 2024-07-02 18:31:18 +01:00
8ce3a5d25c implement logging feature base 2024-07-02 18:21:24 +01:00
0b8caf3e25 add vote link 2024-07-02 01:56:59 +01:00
317eaaec8a impl a EmbedDescriptionBuilder 2024-07-02 01:51:13 +01:00
4f975ab07a use paste for messages longer than 512 and fix message sniping 2024-07-02 01:20:41 +01:00
1a69bce9dd sort sniped messages 2024-07-02 00:54:55 +01:00
37c69597be why angry?????? 2024-07-01 23:02:28 +01:00
3146ed7d6d fix dev commands 2024-07-01 22:56:52 +01:00
69281d113c fix marked as non null err and removed profiles debug 2024-07-01 21:51:51 +01:00
c6289d1c8e fix for npe?? 2024-07-01 21:49:02 +01:00
3082265ec6 add a new motd 2024-07-01 21:27:20 +01:00
a057853cbd add feature disabled check 2024-07-01 21:21:47 +01:00
8b451c6ee5 add message snipe feature 2024-07-01 21:20:39 +01:00
727a4c9a6f set name when user joins guild 2024-07-01 19:41:13 +01:00
88 changed files with 4280 additions and 110 deletions

26
pom.xml
View File

@ -108,6 +108,22 @@
<version>5.2.4</version>
</dependency>
<!-- Redis for caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Libraries -->
<dependency>
@ -143,6 +159,16 @@
<artifactId>spotify-web-api-java</artifactId>
<version>8.4.0</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<dependency>
<groupId>uk.co.conoregan</groupId>
<artifactId>themoviedbapi</artifactId>
<version>2.1.1</version>
</dependency>
<!-- Test Dependencies -->
<dependency>

34
privacy-policy.txt Normal file
View File

@ -0,0 +1,34 @@
Privacy Policy
1. Introduction
This Privacy Policy explains how Bat ("we", "us", "our") collects, uses, and protects your information when you use our Discord bot (the "Service").
2. Information We Collect
User Data: When you interact with our bot, we may collect your Discord user ID, server ID, and any messages or commands you send to the bot.
Usage Data: We may collect data on how you interact with the bot, such as commands used and features accessed.
3. How We Use Your Information
Service Operation: To provide, maintain, and improve the Service.
Communication: To respond to your inquiries and provide customer support.
Analytics: To analyze usage trends and improve the Service.
4. Data Sharing
We do not sell or rent your information to third parties. We may share your information with third-party service providers who assist us in operating the Service, under confidentiality agreements.
5. Data Security
We implement appropriate technical and organizational measures to protect your information from unauthorized access, loss, or misuse.
6. Data Retention
We retain your information for as long as necessary to fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required or permitted by law.
7. Your Rights
You have the right to access, correct, or delete your personal information. To exercise these rights, please contact us at bat@fascinated.cc.
8. Changes to This Privacy Policy
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on our https://discord.gg/yjj2U3ctEG.
9. Contact Us
If you have any questions about this Privacy Policy, please contact us at bat@fascinated.cc.

View File

@ -41,7 +41,6 @@ public class BatApplication {
// Start the app
SpringApplication.run(BatApplication.class, args);
log.info("APP IS RUNNING IN %s MODE!!!!!!!!!".formatted(Config.isProduction() ? "PRODUCTION" : "DEVELOPMENT"));
}

View File

@ -6,12 +6,7 @@ package cc.fascinated.bat;
public class Consts {
public static final String INVITE_URL = "https://discord.com/oauth2/authorize?client_id=1254161119975833652&permissions=8&integration_type=0&scope=bot+applications.commands";
public static final String SUPPORT_INVITE_URL = "https://discord.gg/invite/yjj2U3ctEG";
public static String BOT_OWNER = "474221560031608833";
public static String ADMIN_GUILD = "1203163422498361404";
static {
if (System.getenv("ADMIN_GUILD") != null) {
ADMIN_GUILD = System.getenv("ADMIN_GUILD");
}
}
public static final String BOT_OWNER = "474221560031608833";
public static final String PRIVACY_POLICY_URL = "https://git.fascinated.cc/Fascinated/Bat/raw/branch/master/privacy-policy.txt";
public static final String TERMS_OF_SERVICE_URL = "https://git.fascinated.cc/Fascinated/Bat/raw/branch/master/terms-of-service.txt";
}

View File

@ -16,8 +16,12 @@ public enum Category {
GENERAL(Emoji.fromUnicode("U+2699"), "General", false),
FUN(Emoji.fromFormatted("U+1F973"), "Fun", false),
SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false),
MODERATION(Emoji.fromFormatted("U+1F6E0"), "Moderation", false),
UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false),
MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false),
MOVIES_TV(Emoji.fromFormatted("U+1F3A5"), "Movies & TV", false),
MESSAGES(Emoji.fromFormatted("U+1F4A3"), "Snipe", false),
LOGS(Emoji.fromFormatted("U+1F4D1"), "Logs", false),
BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false),
BOT_ADMIN(null, null, true);

View File

@ -0,0 +1,40 @@
package cc.fascinated.bat.common;
import lombok.NonNull;
/**
* @author Fascinated (fascinated7)
*/
public class EmbedDescriptionBuilder {
/**
* Where the description is stored
*/
private final StringBuilder builder = new StringBuilder();
public EmbedDescriptionBuilder(String title) {
builder.append("**").append(title).append("**").append("\n");
}
@NonNull
public EmbedDescriptionBuilder appendLine(@NonNull String line, boolean arrow) {
builder.append(arrow ? "" : "").append(line).append("\n");
return this;
}
@NonNull
public EmbedDescriptionBuilder appendSubtitle(@NonNull String title) {
builder.append("**").append(title).append("**").append("\n");
return this;
}
@NonNull
public EmbedDescriptionBuilder emptyLine() {
builder.append("\n");
return this;
}
@NonNull
public String build() {
return builder.toString();
}
}

View File

@ -0,0 +1,21 @@
package cc.fascinated.bat.common;
/**
* @author Fascinated (fascinated7)
*/
public class EnumUtils {
/**
* Gets the name of the enum
*
* @param e the enum
* @return the name
*/
public static String getEnumName(Enum<?> e) {
String[] split = e.name().split("_");
StringBuilder builder = new StringBuilder();
for (String s : split) {
builder.append(s.substring(0, 1).toUpperCase()).append(s.substring(1).toLowerCase()).append(" ");
}
return builder.toString().trim();
}
}

View File

@ -0,0 +1,39 @@
package cc.fascinated.bat.common;
import cc.fascinated.bat.BatApplication;
import cc.fascinated.bat.model.token.paste.PasteUploadToken;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
/**
* @author Fascinated (fascinated7)
*/
@Log4j2
public class PasteUtils {
private static final String PASTE_URL = "https://paste.fascinated.cc/";
private static final String PASTE_UPLOAD_URL = PASTE_URL + "api/upload";
private static final HttpClient httpClient = HttpClient.newHttpClient();
/**
* Uploads a paste to the paste server
*
* @param content the content of the paste
* @return the paste upload token
*/
@SneakyThrows
public static PasteUploadToken uploadPaste(String content) {
HttpResponse<String> response = httpClient.send(HttpRequest.newBuilder()
.uri(URI.create(PASTE_UPLOAD_URL))
.POST(HttpRequest.BodyPublishers.ofString(content))
.build(), HttpResponse.BodyHandlers.ofString());
PasteUploadToken paste = BatApplication.GSON.fromJson(response.body(), PasteUploadToken.class);
paste.setUrl(PASTE_URL + paste.getKey());
log.info("Created paste with key \"{}\" ({})", paste.getKey(), paste.getUrl());
return paste;
}
}

View File

@ -4,6 +4,8 @@ import cc.fascinated.bat.BatApplication;
import lombok.Getter;
import lombok.SneakyThrows;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
@ -13,6 +15,7 @@ import java.util.Map;
*/
@Getter
public abstract class ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(ProfileHolder.class);
/**
* The profiles for the holder
*/

View File

@ -17,4 +17,21 @@ public class StringUtils {
}
return stringBuilder.toString();
}
/**
* Escapes meta characters in a string
*
* @param inputString the input string
* @return the string with escaped meta characters
*/
public static String escapeMetaCharacters(String inputString){
final String[] metaCharacters = {"\\","^","$","{","}","[","]","(",")",".","*","+","?","|","<",">","-","&","%"};
for (String metaCharacter : metaCharacters) {
if (inputString.contains(metaCharacter)) {
inputString = inputString.replace(metaCharacter, "\\" + metaCharacter);
}
}
return inputString;
}
}

View File

@ -31,20 +31,26 @@ public class WebRequest {
* @return the response
*/
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
ResponseEntity<T> responseEntity = CLIENT.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {
}) // Don't throw exceptions on error
.toEntity(clazz);
try {
ResponseEntity<T> responseEntity = CLIENT.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {
}) // Don't throw exceptions on error
.toEntity(clazz);
if (responseEntity.getStatusCode().isError()) {
if (responseEntity.getStatusCode().isError()) {
return null;
}
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return responseEntity.getBody();
} catch (RateLimitException e) {
throw e;
} catch (Exception e) {
return null;
}
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return responseEntity.getBody();
}
/**

View File

@ -1,20 +1,37 @@
package cc.fascinated.bat.config;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component @Getter
public class Config {
public static Config INSTANCE;
/**
* Is the app running in a production environment?
*/
@Getter
private static final boolean production;
/**
* The ID of the admin guild
*/
private final String adminGuild;
static {
// Are we running on production?
String appEnv = System.getenv("APP_ENV");
production = appEnv != null && (appEnv.equals("production"));
}
@Autowired
public Config(@Value("${bat.admin-guild}") String adminGuild) {
INSTANCE = this;
this.adminGuild = adminGuild;
}
}

View File

@ -1,23 +0,0 @@
package cc.fascinated.bat.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
/**
* @author Fascinated (fascinated7)
*/
@Configuration
public class MongoConfig {
@Bean
public MappingMongoConverter mongoConverter(MongoDatabaseFactory mongoFactory, MongoMappingContext mongoMappingContext) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoFactory);
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
mongoConverter.setMapKeyDotReplacement("-DOT");
return mongoConverter;
}
}

View File

@ -0,0 +1,73 @@
package cc.fascinated.bat.config;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author Fascinated (fascinated7)
*/
@Configuration
@Log4j2(topic = "Redis")
public class RedisConfig {
/**
* The Redis server host.
*/
@Value("${spring.data.redis.host}")
private String host;
/**
* The Redis server port.
*/
@Value("${spring.data.redis.port}")
private int port;
/**
* The Redis database index.
*/
@Value("${spring.data.redis.database}")
private int database;
/**
* The optional Redis password.
*/
@Value("${spring.data.redis.auth}")
private String auth;
/**
* Build the config to use for Redis.
*
* @return the config
* @see RedisTemplate for config
*/
@Bean @NonNull
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
/**
* Build the connection factory to use
* when making connections to Redis.
*
* @return the built factory
* @see JedisConnectionFactory for factory
*/
@Bean @NonNull
public JedisConnectionFactory jedisConnectionFactory() {
log.info("Connecting to Redis at {}:{}/{}", host, port, database);
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
config.setDatabase(database);
if (!auth.trim().isEmpty()) { // Auth with our provided password
log.info("Using auth...");
config.setPassword(auth);
}
return new JedisConnectionFactory(config);
}
}

View File

@ -2,18 +2,35 @@ package cc.fascinated.bat.event;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberLeaderboardToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberPlayerScoreToken;
import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.channel.ChannelCreateEvent;
import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent;
import net.dv8tion.jda.api.events.channel.update.GenericChannelUpdateEvent;
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent;
import net.dv8tion.jda.api.events.guild.voice.GenericGuildVoiceEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import java.util.List;
/**
* @author Fascinated (fascinated7)
@ -45,7 +62,7 @@ public interface EventListener {
* @param guild the guild the user left
* @param user the user that left the guild
*/
default void onGuildMemberLeave(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberRemoveEvent event) {
default void onGuildMemberLeave(@NonNull BatGuild guild, BatUser user, @NonNull GuildMemberRemoveEvent event) {
}
/**
@ -57,6 +74,25 @@ public interface EventListener {
default void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
}
/**
* Called when a user updates a message
*
* @param guild the guild that the message was updated in
* @param user the user that updated the message
*/
default void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage,
@NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) {
}
/**
* Called when a user deletes a message
*
* @param guild the guild that the message was deleted in
* @param user the user that deleted the message
*/
default void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) {
}
/**
* Called when a user selects a string
*
@ -105,6 +141,107 @@ public interface EventListener {
default void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
}
/**
* Called when a user gets roles added to them
*
* @param guild the guild that the user added the role in
* @param user the user that added the role
* @param rolesAdded the roles that were added
*/
default void onGuildMemberRoleAdd(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleAddEvent event) {
}
/**
* Called when a user gets roles removed from them
*
* @param guild the guild that the user removed the role in
* @param user the user that removed the role
* @param rolesAdded the roles that were removed
*/
default void onGuildMemberRoleRemove(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleRemoveEvent event) {
}
/**
* Called when a channel is created
*
* @param guild the guild that the channel was created in
*/
default void onChannelCreate(@NonNull BatGuild guild, @NonNull ChannelCreateEvent event) {
}
/**
* Called when a channel is deleted
*
* @param guild the guild that the channel was deleted in
*/
default void onChannelDelete(@NonNull BatGuild guild, @NonNull ChannelDeleteEvent event) {
}
/**
* Called when a user is banned from a guild
*
* @param guild the guild that the user was banned from
* @param user the user that was banned
*/
default void onGuildMemberBan(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildBanEvent event) {
}
/**
* Called when a user is unbanned from a guild
*
* @param guild the guild that the user was unbanned from
* @param user the user that was unbanned
*/
default void onGuildMemberUnban(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildUnbanEvent event) {
}
/**
* Called when a user gets timed out in a guild (gets muted)
*
* @param guild the guild that the user timed out in
* @param user the user that timed out
*/
default void onGuildMemberTimeout(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberUpdateTimeOutEvent event) {
}
/**
* Called when a channels state is updated
*
* @param guild the guild that the channel was updated in
* @param event the event that was fired
*/
default void onGenericChannelUpdate(@NonNull BatGuild guild, @NonNull GenericChannelUpdateEvent<?> event) {
}
/**
* Called when a user updates their username
*
* @param user the user that updated their name
* @param oldName the old username
* @param newName the new username
*/
default void onUserUpdateName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateNameEvent event) {
}
/**
* Called when a user updates their avatar
*
* @param user the user that updated their avatar
* @param oldAvatarUrl the old avatar url
* @param newAvatarUrl the new avatar url
*/
default void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) {
}
/**
* Called when a user joins or leaves a voice channel
*
* @param guild the guild that the user joined or left the voice channel in
* @param user the user that joined or left the voice channel
*/
default void onGuildVoiceUpdate(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GenericGuildVoiceEvent event) {
}
/**
* Called when Spring is shutting down
*/

View File

@ -49,7 +49,11 @@ public class AutoRoleListener implements EventListener {
event.getGuild().addRoleToMember(event.getMember(), role).queue();
}
toRemove.forEach(profile::removeRole);
log.info("Gave user \"{}\" {} auto roles{}", user.getId(), profile.getRoles().size(), toRemove.isEmpty() ? ""
: " and removed %s invalid roles".formatted(toRemove.size()));
log.info("Gave user \"{}\" {} auto roles in guild \"{}\"{}",
user.getId(),
profile.getRoles().size(),
guild.getName(),
toRemove.isEmpty() ? "" : " and removed %s invalid roles from the profile".formatted(toRemove.size())
);
}
}

View File

@ -3,6 +3,7 @@ package cc.fascinated.bat.features.base;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.commands.botadmin.premium.PremiumAdminCommand;
import cc.fascinated.bat.features.base.commands.fun.EightBallCommand;
import cc.fascinated.bat.features.base.commands.fun.image.ImageCommand;
import cc.fascinated.bat.features.base.commands.general.*;
import cc.fascinated.bat.features.base.commands.general.avatar.AvatarCommand;
@ -39,5 +40,6 @@ public class BaseFeature extends Feature {
super.registerCommand(commandService, context.getBean(AvatarCommand.class));
super.registerCommand(commandService, context.getBean(ImageCommand.class));
super.registerCommand(commandService, context.getBean(FeatureCommand.class));
super.registerCommand(commandService, context.getBean(EightBallCommand.class));
}
}

View File

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.base.commands.fun;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "8ball", description = "Ask the magic 8ball a question")
public class EightBallCommand extends BatCommand {
private final String[] responses = new String[]{
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes, definitely",
"You may rely on it",
"As I see it, yes",
"Most likely",
"Outlook good",
"Yes",
"Signs point to yes",
"Reply hazy, try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful"
};
public EightBallCommand() {
super.addOption(OptionType.STRING, "question", "The question you want to ask the 8ball", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping questionOption = event.getOption("question");
if (questionOption == null) {
return;
}
String question = questionOption.getAsString();
String response = responses[(int) (Math.random() * responses.length)];
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You asked: `%s`\n\n:8ball: The magic 8ball says: `%s`".formatted(question, response))
.build())
.queue();
}
}

View File

@ -2,6 +2,7 @@ package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.TimeUtils;
@ -42,15 +43,16 @@ public class BotStatsCommand extends BatCommand {
JDA jda = DiscordService.JDA;
event.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
"**Bot Statistics**\n" +
"Guilds: **%s**\n".formatted(NumberFormatter.format(jda.getGuilds().size())) +
"Users: **%s**\n".formatted(NumberFormatter.format(jda.getUsers().size())) +
"Gateway Ping: **%sms**\n".formatted(jda.getGatewayPing()) +
"\n" +
"**Bat Statistics**\n" +
"Uptime: **%s**\n".formatted(TimeUtils.format(bean.getUptime())) +
"Cached Guilds: **%s**\n".formatted(NumberFormatter.format(guildService.getGuilds().size())) +
"Cached Users: **%s**".formatted(NumberFormatter.format(userService.getUsers().size()))
new EmbedDescriptionBuilder("Bat Statistics")
.appendLine("Guilds: **%s**".formatted(NumberFormatter.format(jda.getGuilds().size())), true)
.appendLine("Users: **%s**".formatted(NumberFormatter.format(jda.getUsers().size())), true)
.appendLine("Gateway Ping: **%sms**".formatted(jda.getGatewayPing()), true)
.emptyLine()
.appendSubtitle("Bot Statistics")
.appendLine("Uptime: **%s**".formatted(TimeUtils.format(bean.getUptime())), true)
.appendLine("Cached Guilds: **%s**".formatted(NumberFormatter.format(guildService.getGuilds().size())), true)
.appendLine("Cached Users: **%s**".formatted(NumberFormatter.format(userService.getUsers().size())), true)
.build()
).build()).queue();
}
}

View File

@ -63,30 +63,30 @@ public class HelpCommand extends BatCommand implements EventListener {
return;
}
String commands = "";
StringBuilder commands = new StringBuilder();
List<BatCommand> categoryCommands = commandService.getCommandsByCategory(category, true);
if (categoryCommands.isEmpty()) {
commands = "No commands available in this category.";
commands = new StringBuilder("No commands available in this category.");
} else {
for (BatCommand command : categoryCommands) {
if (!command.getSubCommands().isEmpty()) {
for (Map.Entry<String, BatSubCommand> entry : command.getSubCommands().entrySet()) {
BatSubCommand subCommand = entry.getValue();
SubcommandData commandData = subCommand.getCommandData();
commands += "</%s %s:%s> - %s\n".formatted(
commands.append("</%s %s:%s> - %s\n".formatted(
command.getCommandInfo().name(),
commandData.getName(),
subCommand.getCommandSnowflake(),
commandData.getDescription()
);
));
}
continue;
}
commands += "</%s:%s> - %s\n".formatted(
commands.append("</%s:%s> - %s\n".formatted(
command.getCommandInfo().name(),
command.getCommandSnowflake(),
command.getCommandInfo().description()
);
));
}
}
@ -98,7 +98,7 @@ public class HelpCommand extends BatCommand implements EventListener {
categoryCommands.size() == 1 ? "" : "s",
subCommands,
subCommands == 1 ? "" : "s",
commands
commands.toString()
)).build()).queue();
}
@ -108,18 +108,27 @@ public class HelpCommand extends BatCommand implements EventListener {
* @return The home embed
*/
private MessageEmbed createHomeEmbed() {
String categories = "";
StringBuilder categories = new StringBuilder();
for (Category category : Category.getCategories()) {
long commandCount = commandService.getCommandsByCategory(category, true).size();
categories += "➜ %s - **%s Command%s**\n".formatted(
categories.append("➜ %s - **%s Command%s**\n".formatted(
category.getName(),
commandCount,
commandCount == 1 ? "" : "s"
);
));
}
return EmbedUtils.genericEmbed()
.setDescription("Here are the available command categories: \n\n" + categories)
.setDescription("""
**Welcome to the Bat Help Menu!**
%s
*View our [TOS](%s) and [Privacy Policy](%s) for more information.*
""".formatted(
categories.toString(),
Consts.TERMS_OF_SERVICE_URL,
Consts.PRIVACY_POLICY_URL
))
.build();
}

View File

@ -2,6 +2,7 @@ package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
@ -18,18 +19,19 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "vote", description = "Vote for the bot", guildOnly = false)
public class VoteCommand extends BatCommand {
private static final String[] VOTE_LINKS = new String[]{
"https://top.gg/bot/1254161119975833652/vote"
"https://top.gg/bot/1254161119975833652/vote",
"https://discordbotlist.com/bots/bat/upvote"
};
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
StringBuilder builder = new StringBuilder();
builder.append("You can vote for the bot by clicking the following links:\n\n");
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Vote Links");
description.appendLine("Vote for the bot on the following websites to support us!", false);
for (String link : VOTE_LINKS) {
builder.append("%s\n".formatted(link));
description.appendLine(link, true);
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(builder.toString())
.setDescription(description.build())
.build()
).queue();
}

View File

@ -8,7 +8,6 @@ import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;

View File

@ -26,7 +26,7 @@ public class PremiumCommand extends BatCommand {
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Premium Information");
if (premium.hasPremium()) {
embed.addField("Premium", premium.hasPremium() ? "Yes" : "No", true);
embed.addField("Started", "<t:%d>".formatted(premium.getActivatedAt().toInstant().toEpochMilli() / 1000), true);
embed.addField("Started", "<t:%d>".formatted(premium.getActivatedAt().toInstant().getEpochSecond()), true);
embed.addField("Expires", premium.isInfinite() ? "Never" : "<t:%d>"
.formatted(premium.getExpiresAt().toInstant().toEpochMilli() / 1000), true);
} else {

View File

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.drag;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.drag.command.DragCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class DragFeature extends Feature {
@Autowired
public DragFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Drag", true,Category.GENERAL);
super.registerCommand(commandService, context.getBean(DragCommand.class));
}
}

View File

@ -0,0 +1,50 @@
package cc.fascinated.bat.features.drag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.interactions.InteractionHook;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@RequiredArgsConstructor
@Getter @Setter
public class DragRequest {
/**
* The date the request was made
*/
private final Date requestDate = new Date();
/**
* The user that wants to join the voice channel
*/
private final Member member;
/**
* The user that the member wants to join
*/
private final Member target;
/**
* The voice channel the user wants to join
*/
private final VoiceChannel voiceChannel;
/**
* The interaction hook that the request was made from
*/
private final InteractionHook interactionHook;
/**
* The request message sent in the voice channel
*/
private Message requestMessage;
}

View File

@ -0,0 +1,21 @@
package cc.fascinated.bat.features.drag.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import io.sentry.protocol.App;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "drag", description = "Drag command")
public class DragCommand extends BatCommand {
@Autowired
public DragCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(RequestSubCommand.class));
}
}

View File

@ -0,0 +1,152 @@
package cc.fascinated.bat.features.drag.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TimerUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.drag.DragRequest;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.GuildVoiceState;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
/**
* Handles requests to be moved to a voice channel.
* Author: Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "request", description = "Request to be moved to a voice channel")
public class RequestSubCommand extends BatSubCommand implements EventListener {
/**
* A list of join requests
*/
public static final Set<DragRequest> JOIN_REQUESTS = new HashSet<>();
private final long requestTimeout = Duration.ofMinutes(30).toMillis();
private final long checkInterval = Duration.ofSeconds(10).toMillis();
public RequestSubCommand() {
super.addOption(OptionType.USER, "user", "The user you want to join", true);
TimerUtils.scheduleRepeating(() -> {
Set<DragRequest> toRemove = new HashSet<>();
for (DragRequest joinRequest : JOIN_REQUESTS) {
if (System.currentTimeMillis() - joinRequest.getRequestDate().getTime() < requestTimeout) {
return;
}
// The request has timed out
joinRequest.getInteractionHook().editOriginalEmbeds(EmbedUtils.errorEmbed()
.setDescription("The request to join %s's voice channel has timed out.".formatted(joinRequest.getTarget().getAsMention()))
.build()).queue();
joinRequest.getVoiceChannel().sendMessageEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s's request to join your voice channel has timed out.".formatted(joinRequest.getMember().getAsMention()))
.build()).queue();
joinRequest.getRequestMessage().delete().queue();
toRemove.add(joinRequest);
}
JOIN_REQUESTS.removeAll(toRemove);
}, checkInterval, checkInterval);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
GuildVoiceState voiceState = member.getVoiceState();
// Check if the user is in a voice channel
if (voiceState == null || voiceState.getChannel() == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You are not in a voice channel.")
.build())
.setEphemeral(true)
.queue();
return;
}
OptionMapping userOption = event.getOption("user");
if (userOption == null) return;
// Check if the user is in a voice channel
Member target = userOption.getAsMember();
if (target == null || target.getId().equals(member.getId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You cannot request to join your own voice channel.")
.build())
.setEphemeral(true)
.queue();
return;
}
// Check if the target user is in a voice channel
GuildVoiceState targetVoiceState = target.getVoiceState();
if (targetVoiceState == null || targetVoiceState.getChannel() == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The user %s is not in a voice channel.".formatted(target.getAsMention()))
.build())
.setEphemeral(true)
.queue();
return;
}
VoiceChannel targetChannel = targetVoiceState.getChannel().asVoiceChannel();
// User is already in the target channel
if (voiceState.getChannel().getId().equals(targetChannel.getId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You are already in the voice channel %s.".formatted(voiceState.getChannel().getAsMention()))
.build())
.setEphemeral(true)
.queue();
return;
}
// Check if the user has already requested to join the target channel
DragRequest existingRequest = JOIN_REQUESTS.stream()
.filter(request -> request.getMember().getId().equals(member.getId()) && request.getVoiceChannel().getId().equals(targetChannel.getId()))
.findFirst()
.orElse(null);
if (existingRequest != null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have already requested to join %s's voice channel.".formatted(target.getAsMention()))
.build())
.setEphemeral(true)
.queue();
return;
}
// Add the request to the list
JOIN_REQUESTS.add(new DragRequest(member, target, targetChannel, event.getHook()));
// Send the request to the target user
targetChannel.sendMessage(target.getAsMention()).queue();
targetChannel.sendMessageEmbeds(EmbedUtils.successEmbed()
.setDescription("User %s has requested to join your voice channel.".formatted(member.getAsMention()))
.build())
.addComponents(ActionRow.of(
Button.primary("drag-request-accept", "Accept"),
Button.danger("drag-request-decline", "Decline")
))
.queue(message -> {
JOIN_REQUESTS.stream()
.filter(r -> r.getVoiceChannel().getId().equals(targetChannel.getId()))
.findFirst().ifPresent(request -> request.setRequestMessage(message));
});
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Request to join %s's voice channel has been sent.".formatted(target.getAsMention()))
.build())
.setComponents(ActionRow.of(Button.secondary("drag-request-cancel", "Cancel")))
.queue();
}
}

View File

@ -0,0 +1,43 @@
package cc.fascinated.bat.features.drag.listeners.request;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.drag.DragRequest;
import cc.fascinated.bat.features.drag.command.RequestSubCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class RequestListener implements EventListener {
@Override
public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
if (!event.getComponentId().equals("drag-request-cancel")) {
return;
}
Optional<DragRequest> optionalDragRequest = RequestSubCommand.JOIN_REQUESTS.stream()
.filter(request -> request.getMember().getId().equals(event.getUser().getId()))
.findFirst();
if (optionalDragRequest.isEmpty()) {
return;
}
DragRequest dragRequest = optionalDragRequest.get();
InteractionHook interactionHook = dragRequest.getInteractionHook();
interactionHook.editOriginalEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have cancelled your request to join %s's voice channel.".formatted(dragRequest.getTarget().getAsMention()))
.build()).queue(message -> message.editMessageComponents().queue());
dragRequest.getVoiceChannel().sendMessageEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s has cancelled their request to join your voice channel.".formatted(dragRequest.getMember().getAsMention()))
.build()).queue();
dragRequest.getRequestMessage().delete().queue();
RequestSubCommand.JOIN_REQUESTS.remove(dragRequest);
}
}

View File

@ -0,0 +1,54 @@
package cc.fascinated.bat.features.drag.listeners.request;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.drag.DragRequest;
import cc.fascinated.bat.features.drag.command.RequestSubCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class TargetChannelListener implements EventListener {
@Override
public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
User buttonUser = event.getUser();
Member member = guild.getDiscordGuild().getMember(buttonUser);
if (member == null) return;
DragRequest joinRequest = RequestSubCommand.JOIN_REQUESTS.stream()
.filter(request -> request.getVoiceChannel().getId().equals(event.getChannel().getId()))
.findFirst()
.orElse(null);
if (joinRequest == null) return;
if (event.getComponentId().equals("drag-request-accept")) {
joinRequest.getVoiceChannel().getGuild().moveVoiceMember(joinRequest.getMember(), joinRequest.getVoiceChannel()).queue();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have accepted %s's request to join your voice channel!".formatted(joinRequest.getMember().getAsMention()))
.build())
.queue();
} else if (event.getComponentId().equals("drag-request-decline")) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have declined %s's request to join your voice channel!".formatted(joinRequest.getMember().getAsMention()))
.build())
.queue();
joinRequest.getInteractionHook().retrieveOriginal().queue(message -> {
message.editMessageEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s has declined your request to join their voice channel.".formatted(joinRequest.getTarget().getAsMention()))
.build()).queue();
message.editMessageComponents().queue();
});
}
RequestSubCommand.JOIN_REQUESTS.remove(joinRequest);
// Remove the buttons from the embed
event.getInteraction().getMessage().editMessageComponents().queue();
}
}

View File

@ -0,0 +1,34 @@
package cc.fascinated.bat.features.logging;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor @Getter
public enum LogCategory {
MESSAGE("Message"),
MEMBER("Member"),
CHANNEL("Channel");
/**
* The name of the log category
*/
private final String name;
/**
* Gets the log category by the name
*
* @param name - the name
* @return the log category, or null if it doesn't exist
*/
public static LogCategory getLogCategory(String name) {
for (LogCategory logCategory : values()) {
if (logCategory.getName().equalsIgnoreCase(name)) {
return logCategory;
}
}
return null;
}
}

View File

@ -0,0 +1,61 @@
package cc.fascinated.bat.features.logging;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.PasteUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.features.logging.command.LogsCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class LogFeature extends Feature {
@Autowired
public LogFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Logging", false, Category.LOGS);
super.registerCommand(commandService, context.getBean(LogsCommand.class));
}
/**
* Sends a log to the log channel
*
* @param guild the guild to send the log in
* @param type the type of log
* @param embed the embed to send
*/
public void sendLog(BatGuild guild, LogType type, MessageEmbed embed) {
FeatureProfile featureProfile = guild.getFeatureProfile();
if (featureProfile.isFeatureDisabled(this)) { // The feature is disabled
return;
}
LogProfile logProfile = guild.getLogProfile();
if (!logProfile.hasLogChannel(type)) { // The guild has no log channel for this type
return;
}
TextChannel logChannel = logProfile.getLogChannel(type);
if (logChannel == null) { // The log channel has been removed
return;
}
logChannel.sendMessageEmbeds(embed).queue();
}
/**
* Formats the content to be sent in the log
*
* @param content the content to format
* @return the formatted content
*/
public String formatContent(String content) {
return content.length() > 512 ? PasteUtils.uploadPaste(content).getUrl() : "\n```\n%s\n```".formatted(content);
}
}

View File

@ -0,0 +1,97 @@
package cc.fascinated.bat.features.logging;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
public class LogProfile extends Serializable {
/**
* The log channels for this profile
*/
private final Map<LogType, TextChannel> logChannels = new HashMap<>();
/**
* Checks if the log channel for the specified log type exists
*
* @param logType - the log type
* @return true if it exists, false otherwise
*/
public boolean hasLogChannel(LogType logType) {
return this.logChannels.containsKey(logType);
}
/**
* Gets the log channel for the specified log type
*
* @param logType - the log type
* @return the log channel, or null if it doesn't exist
*/
public TextChannel getLogChannel(LogType logType) {
TextChannel textChannel = this.logChannels.get(logType);
if (textChannel == null) {
return null;
}
// Ensure the channel exists
if (DiscordService.JDA.getTextChannelById(textChannel.getId()) == null) {
this.logChannels.remove(logType);
return null;
}
return textChannel;
}
/**
* Sets the log channel for the specified log type
*
* @param logType - the log type
* @param channel - the channel
*/
public void setLogChannel(LogType logType, TextChannel channel) {
this.logChannels.put(logType, channel);
}
/**
* Removes the log channel for the specified log type
*
* @param logType - the log type
*/
public void removeLogChannel(LogType logType) {
this.logChannels.remove(logType);
}
@Override
public void load(Document document, Gson gson) {
JDA jda = DiscordService.JDA;
for (LogType logType : LogType.values()) {
if (document.containsKey(logType.name())) {
TextChannel channel = jda.getTextChannelById(document.getString(logType.name()));
if (channel == null) {
return;
}
this.logChannels.put(logType, channel);
}
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (Map.Entry<LogType, TextChannel> entry : this.logChannels.entrySet()) {
document.append(entry.getKey().name(), entry.getValue().getId());
}
return document;
}
@Override
public void reset() {
this.logChannels.clear();
}
}

View File

@ -0,0 +1,82 @@
package cc.fascinated.bat.features.logging;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor @Getter
public enum LogType {
/**
* Message Events
*/
MESSAGE_DELETE(LogCategory.MESSAGE, "Message Delete"),
MESSAGE_EDIT(LogCategory.MESSAGE,"Message Edit"),
/**
* Member Events
*/
MEMBER_JOIN(LogCategory.MEMBER, "Member Join"),
MEMBER_LEAVE(LogCategory.MEMBER, "Member Leave"),
MEMBER_NICKNAME_UPDATE(LogCategory.MEMBER, "Member Nickname Update"),
MEMBER_GLOBAL_NAME_UPDATE(LogCategory.MEMBER, "Member Global Name Update"),
MEMBER_USERNAME_UPDATE(LogCategory.MEMBER, "Member Username Update"),
MEMBER_AVATAR_UPDATE(LogCategory.MEMBER, "Member Avatar Update"),
MEMBER_ROLE_UPDATE(LogCategory.MEMBER, "Member Role Update"),
MEMBER_BAN(LogCategory.MEMBER, "Member Ban"),
MEMBER_UNBAN(LogCategory.MEMBER, "Member Unban"),
MEMBER_TIMEOUT(LogCategory.MEMBER, "Member Timeout"),
/**
* Channel Events
*/
CHANNEL_CREATE(LogCategory.CHANNEL, "Channel Create"),
CHANNEL_DELETE(LogCategory.CHANNEL, "Channel Delete"),
VOICE_CHANNEL_JOIN(LogCategory.CHANNEL, "Voice Channel Join"),
VOICE_CHANNEL_LEAVE(LogCategory.CHANNEL, "Voice Channel Leave");
/**
* The category of the log type
*/
private final LogCategory category;
/**
* The name of the log type
*/
private final String name;
/**
* Gets the log type by the name
*
* @param name - the name
* @return the log type, or null if it doesn't exist
*/
public static LogType getLogType(String name) {
for (LogType logType : values()) {
if (logType.getName().equalsIgnoreCase(name)) {
return logType;
}
}
return null;
}
/**
* Gets the log types by the category
*
* @param category - the category
* @return the log types
*/
public static List<LogType> getLogTypesByCategory(String category) {
List<LogType> logTypes = new ArrayList<>();
for (LogType logType : values()) {
if (logType.getCategory().getName().equalsIgnoreCase(category)) {
logTypes.add(logType);
}
}
return logTypes;
}
}

View File

@ -0,0 +1,53 @@
package cc.fascinated.bat.features.logging.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.logging.LogCategory;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("logs:list.sub")
@CommandInfo(name = "list", description = "See all the log types and their channels")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
LogProfile profile = guild.getLogProfile();
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Log Channels");
description.appendLine("""
Set the log channel for:
- A specific event, use `/logs set <event> <channel>`
- A specific category by using `/logs set <category> <channel>`
- All log types by using `/logs set all <channel>`
To remove a log channel, it's the same as setting it,
but with `/logs remove` instead of `/logs set`""", false);
description.emptyLine();
for (int i = 0; i < LogCategory.values().length; i++) {
LogCategory category = LogCategory.values()[i];
if (i != 0) {
description.emptyLine();
}
description.appendLine("**__%s__**".formatted(category.getName()), false);
for (LogType logType : LogType.values()) {
if (logType.getCategory() == category) {
TextChannel logChannel = profile.getLogChannel(logType);
description.appendLine("%s: %s".formatted(logType.getName(), logChannel == null ? "Not Set" : logChannel.getAsMention()), true);
}
}
}
event.replyEmbeds(EmbedUtils.genericEmbed().setDescription(description.build()).build()).queue();
}
}

View File

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.logging.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "logs", description = "Edit logging settings", requiredPermissions = Permission.MANAGE_SERVER)
public class LogsCommand extends BatCommand {
@Autowired
public LogsCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(SetSubCommand.class));
super.addSubCommand(context.getBean(RemoveSubCommand.class));
super.addSubCommand(context.getBean(ListSubCommand.class));
}
}

View File

@ -0,0 +1,98 @@
package cc.fascinated.bat.features.logging.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.logging.LogCategory;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Component("logs:remove.sub")
@CommandInfo(name = "remove", description = "Remove the channel for a log type")
public class RemoveSubCommand extends BatSubCommand {
public RemoveSubCommand() {
super.addOption(OptionType.STRING, "type", "The type of log to remove", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping typeOption = event.getOption("type");
if (typeOption == null) {
return;
}
String type = typeOption.getAsString();
LogProfile profile = guild.getLogProfile();
// Remove the log channel for all log types
if (type.equalsIgnoreCase("all")) {
for (LogType logType : LogType.values()) {
profile.removeLogChannel(logType);
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the log channel for all log types")
.build()).queue();
return;
}
// Remove the log channel for a specific log category
LogCategory logCategory = LogCategory.getLogCategory(type);
if (logCategory != null) {
List<LogType> category = LogType.getLogTypesByCategory(type);
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Log Channel");
description.appendLine("Successfully removed the log channel for the `%s` category"
.formatted(logCategory.getName()), false);
description.emptyLine();
int removed = 0;
for (LogType logType : category) {
if (!profile.hasLogChannel(logType)) {
continue;
}
description.appendLine(logType.getName(), true);
profile.removeLogChannel(logType);
removed++;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(removed == 0 ? "No log channels were removed for the `%s` category".formatted(logCategory.getName()) : description.build())
.build()).queue();
return;
}
// Remove the log channel for a specific log type
LogType logType = LogType.getLogType(type);
if (logType == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid log type")
.build()).queue();
return;
}
if (!profile.hasLogChannel(logType)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The log channel for `%s` is not set".formatted(logType.getName()))
.build()).queue();
return;
}
profile.removeLogChannel(logType);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the log channel for `%s`".formatted(logType.getName()))
.build()).queue();
}
}

View File

@ -0,0 +1,104 @@
package cc.fascinated.bat.features.logging.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.logging.LogCategory;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Component("logs:set.sub")
@CommandInfo(name = "set", description = "Set the channel for a log type")
public class SetSubCommand extends BatSubCommand {
public SetSubCommand() {
super.addOption(OptionType.STRING, "type", "The type of log to set", true);
super.addOption(OptionType.CHANNEL, "channel", "The channel to set the log to", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping typeOption = event.getOption("type");
if (typeOption == null) {
return;
}
OptionMapping channelOption = event.getOption("channel");
if (channelOption == null) {
return;
}
String type = typeOption.getAsString();
TextChannel targetChannel = channelOption.getAsChannel().asTextChannel();
LogProfile profile = guild.getLogProfile();
// Set the log channel for all log types
if (type.equalsIgnoreCase("all")) {
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Log Channel");
description.appendLine("Successfully set the log channel for all log types to %s".formatted(targetChannel.getAsMention()), false);
description.emptyLine();
for (int i = 0; i < LogCategory.values().length; i++) {
LogCategory category = LogCategory.values()[i];
if (i != 0) {
description.emptyLine();
}
description.appendLine("**__%s__**".formatted(category.getName()), false);
for (LogType logType : LogType.getLogTypesByCategory(category.getName())) {
description.appendLine(logType.getName(), true);
profile.setLogChannel(logType, targetChannel);
}
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
return;
}
// Set the log channel for a specific log category
LogCategory logCategory = LogCategory.getLogCategory(type);
if (logCategory != null) {
List<LogType> category = LogType.getLogTypesByCategory(type);
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Log Channel");
description.appendLine("Successfully set the log channel for the `%s` category to %s"
.formatted(logCategory.getName(), targetChannel.getAsMention()), false);
description.emptyLine();
for (LogType logType : category) {
description.appendLine(logType.getName(), true);
profile.setLogChannel(logType, targetChannel);
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
return;
}
// Set the log channel for a specific log type
LogType logType = LogType.getLogType(type);
if (logType == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid log type")
.build()).queue();
return;
}
profile.setLogChannel(logType, targetChannel);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the log channel for `%s` to %s".formatted(logType.getName(), targetChannel.getAsMention()))
.build()).queue();
}
}

View File

@ -0,0 +1,94 @@
package cc.fascinated.bat.features.logging.listeners;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.EnumUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.logging.LogFeature;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.entities.channel.unions.ChannelUnion;
import net.dv8tion.jda.api.events.channel.ChannelCreateEvent;
import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent;
import net.dv8tion.jda.api.events.guild.voice.GenericGuildVoiceEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2
public class ChannelListener implements EventListener {
/**
* A map of users and the last voice channel they were in
*/
private final Map<BatUser, VoiceChannel> lastVoiceChannel = new HashMap<>();
private final LogFeature logFeature;
@Autowired
public ChannelListener(@NonNull ApplicationContext context) {
this.logFeature = context.getBean(LogFeature.class);
}
@Override
public void onChannelCreate(@NonNull BatGuild guild, @NonNull ChannelCreateEvent event) {
log.info("Channel \"{}\" was created in guild \"{}\"", event.getChannel().getName(), guild.getName());
logFeature.sendLog(guild, LogType.CHANNEL_CREATE, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("%s Channel Created".formatted(EnumUtils.getEnumName(event.getChannel().getType())))
.appendLine("Channel: %s".formatted(event.getChannel().getAsMention()), true)
.appendLine("Name: %s".formatted(event.getChannel().getName()), true)
.build())
.build());
}
@Override
public void onChannelDelete(@NonNull BatGuild guild, @NonNull ChannelDeleteEvent event) {
log.info("Channel \"{}\" was deleted in guild \"{}\"", event.getChannel().getName(), guild.getName());
ChannelUnion channel = event.getChannel();
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("%s Channel Deleted".formatted(EnumUtils.getEnumName(channel.getType())))
.appendLine("Name: #%s".formatted(channel.getName()), true);
if (channel.getType().isMessage()) {
TextChannel textChannel = channel.asTextChannel();
description.appendLine("Topic: %s".formatted(textChannel.getTopic()), true);
}
logFeature.sendLog(guild, LogType.CHANNEL_DELETE, EmbedUtils.errorEmbed().setDescription(description.build()).build());
}
@Override
public void onGuildVoiceUpdate(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GenericGuildVoiceEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();
if (channel != null) {
VoiceChannel voiceChannel = channel.asVoiceChannel();
lastVoiceChannel.put(user, voiceChannel);
}
VoiceChannel voiceChannel = lastVoiceChannel.get(user);
if (voiceChannel == null) {
return;
}
boolean joined = voiceChannel.getMembers().contains(event.getMember());
if (!joined) {
lastVoiceChannel.remove(user);
}
log.info("User \"{}\" {} voice channel \"{}\" in guild \"{}\"", user.getId(), joined ? "joined" : "left", voiceChannel.getName(), guild.getName());
String description = new EmbedDescriptionBuilder("User %s Voice Channel".formatted(joined ? "Joined" : "Left"))
.appendLine("User: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Channel: %s".formatted(voiceChannel.getAsMention()), true)
.build();
if (joined) {
logFeature.sendLog(guild, LogType.VOICE_CHANNEL_JOIN, EmbedUtils.successEmbed().setDescription(description).build());
return;
}
logFeature.sendLog(guild, LogType.VOICE_CHANNEL_LEAVE, EmbedUtils.errorEmbed().setDescription(description).build());
}
}

View File

@ -0,0 +1,229 @@
package cc.fascinated.bat.features.logging.listeners;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.logging.LogFeature;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class MemberListener implements EventListener {
private static final Logger log = LoggerFactory.getLogger(MemberListener.class);
private final LogFeature logFeature;
private final GuildService guildService;
@Autowired
public MemberListener(@NonNull ApplicationContext context, @NonNull GuildService guildService) {
this.logFeature = context.getBean(LogFeature.class);
this.guildService = guildService;
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" joined the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_JOIN, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Joined")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Username: %s".formatted(user.getDiscordUser().getName()), true)
.appendLine("Joined Discord: <t:%s:R>".formatted(user.getDiscordUser().getTimeCreated().toEpochSecond()), true)
.build())
.setThumbnail(user.getDiscordUser().getEffectiveAvatarUrl())
.build());
}
@Override
public void onGuildMemberLeave(@NonNull BatGuild guild, BatUser user, @NonNull GuildMemberRemoveEvent event) {
if (user == null || user.getDiscordUser().isBot()) return;
log.info("User \"{}\" left the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_LEAVE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Left")
.appendLine("Member: <@%s>".formatted(user.getId()), true)
.appendLine("Username: %s".formatted(user.getName()), true)
.build())
.build());
}
@Override
public void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their nickname from \"{}\" to \"{}\" in the guild \"{}\"", user.getName(), oldName, newName, guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_NICKNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Nickname Updated")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Old Nickname: `%s`".formatted(oldName == null ? user.getName() : oldName), true)
.appendLine("New Nickname: `%s`".formatted(newName == null ? "Removed Nickname" : newName), true)
.build())
.build());
}
@Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their global name from \"{}\" to \"{}\"", user.getName(), oldName, newName);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_GLOBAL_NAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Name Updated")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Old Name: `%s`".formatted(oldName == null ? user.getName() : oldName), true)
.appendLine("New Name: `%s`".formatted(newName == null ? "Removed Name" : newName), true)
.build())
.build());
}
}
@Override
public void onUserUpdateName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateNameEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their username from \"{}\" to \"{}\"", user.getName(), oldName, newName);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Username Updated")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Old Username: `%s`".formatted(oldName), true)
.appendLine("New Username: `%s`".formatted(newName), true)
.build())
.build());
}
}
@Override
public void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" changed their avatar to \"{}\"", user.getName(), newAvatarUrl);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
if (!guild.isMember(user.getDiscordUser())) continue; // User is not in the guild
logFeature.sendLog(batGuild, LogType.MEMBER_USERNAME_UPDATE, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Avatar Updated")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Old Avatar: %s".formatted(oldAvatarUrl == null ? "None" : "[avatar](%s)".formatted(oldAvatarUrl)), true)
.appendLine("New Avatar: [avatar](%s)".formatted(newAvatarUrl), true)
.build())
.build());
}
}
@Override
public void onGuildMemberRoleAdd(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleAddEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was given {} roles in the guild \"{}\"", user.getName(), rolesAdded.size(), guild.getDiscordGuild().getName());
StringBuilder roles = new StringBuilder();
for (Role role : rolesAdded) {
roles.append(role.getAsMention()).append(", ");
}
String s = rolesAdded.size() > 1 ? "s" : "";
logFeature.sendLog(guild, LogType.MEMBER_ROLE_UPDATE, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Role%s Added".formatted(s))
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Role%s Added: %s".formatted(s, roles.substring(0, roles.length() - 2)), true)
.build())
.build());
}
@Override
public void onGuildMemberRoleRemove(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleRemoveEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" had {} roles removed in the guild \"{}\"", user.getName(), rolesAdded.size(), guild.getDiscordGuild().getName());
StringBuilder roles = new StringBuilder();
for (Role role : rolesAdded) {
roles.append(role.getAsMention()).append(", ");
}
String s = rolesAdded.size() > 1 ? "s" : "";
logFeature.sendLog(guild, LogType.MEMBER_ROLE_UPDATE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Role%s Removed".formatted(s))
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Role%s Removed: %s".formatted(s, roles.substring(0, roles.length() - 2)), true)
.build())
.build());
}
@Override
public void onGuildMemberBan(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildBanEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was banned from the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_BAN, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Banned")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.build())
.build());
}
@Override
public void onGuildMemberUnban(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildUnbanEvent event) {
if (user.getDiscordUser().isBot()) return;
log.info("User \"{}\" was unbanned from the guild \"{}\"", user.getName(), guild.getDiscordGuild().getName());
logFeature.sendLog(guild, LogType.MEMBER_UNBAN, EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Unbanned")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.build())
.build());
}
@Override
public void onGuildMemberTimeout(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberUpdateTimeOutEvent event) {
OffsetDateTime timeoutEnd = event.getNewTimeOutEnd();
if (user.getDiscordUser().isBot() || timeoutEnd == null) return;
log.info("User \"{}\" was timed out until \"{}\"", user.getName(), timeoutEnd);
long seconds = timeoutEnd.toInstant().getEpochSecond();
logFeature.sendLog(guild, LogType.MEMBER_TIMEOUT, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Timed Out")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Timeout End: <t:%s>".formatted(seconds), true)
.appendLine("Relative End: <t:%s:R>".formatted(seconds), true)
.build())
.build());
}
}

View File

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.logging.listeners;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.logging.LogFeature;
import cc.fascinated.bat.features.logging.LogType;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2
public class MessageListener implements EventListener {
private final LogFeature logFeature;
@Autowired
public MessageListener(@NonNull ApplicationContext context) {
this.logFeature = context.getBean(LogFeature.class);
}
@Override
public void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) {
if (user == null || user.getDiscordUser().isBot() || message.getAuthor().isBot()) return;
log.info("User \"{}\" deleted a message in guild \"{}\"", user.getDiscordUser().getGlobalName(), guild.getName());
logFeature.sendLog(guild, LogType.MESSAGE_DELETE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Message Deleted")
.appendLine("Author: %s".formatted(message.getAuthor().getAsMention()), true)
.appendLine("Channel: %s".formatted(message.getChannel().getAsMention()), true)
.appendLine("Content: %s".formatted(logFeature.formatContent(message.getContent())), true)
.build())
.build());
}
@Override
public void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage,
@NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) {
if (user.getDiscordUser().isBot() || newMessage.getAuthor().isBot() || oldMessage == null) return;
log.info("User \"{}\" edited a message in guild \"{}\"", user.getDiscordUser().getGlobalName(), guild.getName());
logFeature.sendLog(guild, LogType.MESSAGE_EDIT, EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Message Edited")
.appendLine("Author: %s".formatted(newMessage.getAuthor().getAsMention()), true)
.appendLine("Channel: %s".formatted(newMessage.getChannel().getAsMention()), true)
.appendLine("Old Content: %s".formatted(logFeature.formatContent(oldMessage.getContent())), true)
.appendLine("New Content: %s".formatted(logFeature.formatContent(newMessage.getContent())), true)
.appendLine("*[Jump to Message](%s)*".formatted(newMessage.getMessageUrl()), false)
.build())
.build());
}
}

View File

@ -0,0 +1,134 @@
package cc.fascinated.bat.features.messagesnipe;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.messagesnipe.command.MessageSnipeCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class MessageSnipeFeature extends Feature implements EventListener {
/**
* The sniped messages for each guild
*/
private static final Map<BatGuild, List<SnipedMessage>> snipedMessages = new HashMap<>();
@Autowired
public MessageSnipeFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Message Snipe", false, Category.MESSAGES);
super.registerCommand(commandService, context.getBean(MessageSnipeCommand.class));
}
/**
* Clears the sniped messages for the given guild
*
* @param guild the guild
* @return if the sniped messages were cleared
*/
public static boolean clearSnipedMessages(BatGuild guild) {
if (snipedMessages.containsKey(guild)) {
snipedMessages.remove(guild);
return true;
}
return false;
}
/**
* Gets the sniped messages for the given guild
*
* @param guild the guild
* @return the sniped messages for the given guild
*/
public static SnipedMessage getDeletedMessage(BatGuild guild, String channelId) {
List<SnipedMessage> messages = snipedMessages.getOrDefault(guild, new ArrayList<>()).stream().filter(message -> message.getDeletedDate() != null)
.sorted(Comparator.comparing(SnipedMessage::getDeletedDate).reversed()).toList();
for (SnipedMessage message : messages) {
if (message.getDeletedDate() != null // Check if the message was deleted
&& message.getMessage().getChannel().getId().equals(channelId)) {
return message;
}
}
return null;
}
/**
* Gets the sniped message with the given id
*
* @param guild the guild
* @param messageId the id of the message
* @return the sniped message with the given id
*/
private SnipedMessage getSnipedMessage(BatGuild guild, String messageId) {
List<SnipedMessage> messages = snipedMessages.getOrDefault(guild, new ArrayList<>());
for (SnipedMessage message : messages) {
if (message.getMessage().getId().equals(messageId)) {
return message;
}
}
return null;
}
@Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
if (guild.getFeatureProfile().isFeatureDisabled(this)) {
return;
}
List<SnipedMessage> messages = snipedMessages.getOrDefault(guild, new ArrayList<>());
if (messages.size() >= 10) {
messages.remove(0);
}
messages.add(new SnipedMessage(event.getMessage(), null));
snipedMessages.put(guild, messages);
}
@Override
public void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) {
if (guild.getFeatureProfile().isFeatureDisabled(this)) {
return;
}
List<SnipedMessage> messages = snipedMessages.getOrDefault(guild, new ArrayList<>());
if (messages.size() >= 10) {
messages.remove(0);
}
SnipedMessage snipedMessage = getSnipedMessage(guild, event.getMessageId());
if (snipedMessage == null) {
return;
}
snipedMessage.setDeletedDate(new Date());
}
@Override
public void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage,
@NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) {
if (guild.getFeatureProfile().isFeatureDisabled(this)) {
return;
}
List<SnipedMessage> messages = snipedMessages.getOrDefault(guild, new ArrayList<>());
if (messages.size() >= 10) {
messages.remove(0);
}
SnipedMessage snipedMessage = getSnipedMessage(guild, event.getMessageId());
if (snipedMessage == null) {
return;
}
snipedMessage.setMessage(event.getMessage());
}
}

View File

@ -0,0 +1,26 @@
package cc.fascinated.bat.features.messagesnipe;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Message;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class SnipedMessage {
/**
* The message that was sniped
*/
private Message message;
/**
* The date when the message was deleted
*/
private Date deletedDate;
}

View File

@ -0,0 +1,36 @@
package cc.fascinated.bat.features.messagesnipe.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.messagesnipe.MessageSnipeFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("messagesnipe:clear.sub")
@CommandInfo(name = "clear", description = "Clears the known sniped messages for this guild", requiredPermissions = Permission.MESSAGE_MANAGE)
public class ClearSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
boolean cleared = MessageSnipeFeature.clearSnipedMessages(guild);
if (!cleared) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no messages to clear in this guild")
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully cleared the sniped messages for this guild")
.build()).queue();
}
}

View File

@ -0,0 +1,45 @@
package cc.fascinated.bat.features.messagesnipe.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.PasteUtils;
import cc.fascinated.bat.features.messagesnipe.MessageSnipeFeature;
import cc.fascinated.bat.features.messagesnipe.SnipedMessage;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "deleted", description = "Snipe the last deleted message in this channel")
public class DeletedSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
SnipedMessage message = MessageSnipeFeature.getDeletedMessage(guild, channel.getId());
if (message == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no deleted messages to snipe in this channel")
.build()).queue();
return;
}
User author = message.getMessage().getAuthor();
String content = message.getMessage().getContentStripped();
String formattedContent = content.length() > 512 ? PasteUtils.uploadPaste(content).getUrl() : "```\n%s\n```".formatted(content);
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(new EmbedDescriptionBuilder("Deleted Message Snipe")
.appendLine("Author: **%s** (%s)".formatted(author.getAsMention(), author.getId()), true)
.appendLine("Deleted: <t:%d:R>".formatted(message.getDeletedDate().getTime() / 1000), true)
.appendLine("Content: %s".formatted(formattedContent), true)
.build()).build()).queue();
}
}

View File

@ -0,0 +1,19 @@
package cc.fascinated.bat.features.messagesnipe.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "snipe", description = "Snipe messages")
public class MessageSnipeCommand extends BatCommand {
public MessageSnipeCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(DeletedSubCommand.class));
super.addSubCommand(context.getBean(ClearSubCommand.class));
}
}

View File

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.moderation;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.moderation.command.PurgeCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class ModerationFeature extends Feature {
@Autowired
public ModerationFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Moderation", true,Category.MODERATION);
super.registerCommand(commandService, context.getBean(PurgeCommand.class));
}
}

View File

@ -0,0 +1,67 @@
package cc.fascinated.bat.features.moderation.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "purge", description = "Purge messages from a channel", requiredPermissions = Permission.MESSAGE_MANAGE)
public class PurgeCommand extends BatCommand {
private final long MESSAGE_DELETE_DELAY = TimeUnit.SECONDS.toMillis(10);
public PurgeCommand() {
super.addOption(OptionType.INTEGER, "amount", "The amount of messages to remove", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping amountOption = event.getOption("amount");
if (amountOption == null) {
return;
}
int amount = amountOption.getAsInt();
if (amount < 2 || amount > 100) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You can only purge between 2 and 100 messages")
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Purging `%s` messages...".formatted(amount))
.build()).queue(then -> {
TextChannel textChannel = (TextChannel) channel;
textChannel.getHistory().retrievePast(amount + 1).queue(messages -> {
// Filter out the command message
then.retrieveOriginal().queue(original -> {
if (original == null) return;
List<Message> toRemove = messages.stream().filter(message -> !original.getId().equals(message.getId())).toList();
textChannel.deleteMessages(toRemove).queue(done -> then.editOriginalEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully purged `%s` messages\n\n*This message will be removed <t:%s:R>*".formatted(
amount,
(System.currentTimeMillis() + MESSAGE_DELETE_DELAY) / 1000
))
.build()).queue(message -> message.delete().queueAfter(MESSAGE_DELETE_DELAY, TimeUnit.MILLISECONDS)));
});
});
});
}
}

View File

@ -46,7 +46,7 @@ public class UserSubCommand extends BatSubCommand {
builder.append("%s has no name history".formatted(target.getDiscordUser().getAsMention()));
} else {
for (TrackedName trackedName : profile.getNameHistorySorted()) {
builder.append("`%s` - <t:%s>\n".formatted(trackedName.getName(), trackedName.getChangedDate().toInstant().toEpochMilli()/1000));
builder.append("`%s` - <t:%s>\n".formatted(trackedName.getName(), trackedName.getChangedDate().toInstant().getEpochSecond()));
}
}

View File

@ -0,0 +1,47 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.service.DiscordService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor @Getter
public class Reminder {
/**
* What we should remind the user of
*/
private final String reminder;
/**
* The channel ID to send the reminder to
*/
private final String channelId;
/**
* The date the reminder should end
*/
private final Date endDate;
/**
* Check if the reminder is expired
*
* @return If the reminder is expired
*/
public boolean isExpired() {
return System.currentTimeMillis() >= endDate.getTime();
}
/**
* Get the channel to send the reminder to
*
* @return The channel
*/
public TextChannel getChannel() {
return DiscordService.JDA.getTextChannelById(channelId);
}
}

View File

@ -0,0 +1,81 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.reminder.command.ReminderCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "Reminder Feature")
@DependsOn("discordService")
public class ReminderFeature extends Feature {
public static final int MAX_REMINDERS = 5; // 5 reminders
public static final long MAX_REMINDER_LENGTH = TimeUnit.DAYS.toMillis(30); // 1 month
private final GuildService guildService;
@Autowired
public ReminderFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Reminder", true, Category.GENERAL);
this.guildService = guildService;
super.registerCommand(commandService, context.getBean(ReminderCommand.class));
}
@Scheduled(cron = "*/30 * * * * *")
public void checkReminders() {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
ReminderProfile reminderProfile = batGuild.getProfile(ReminderProfile.class);
if (reminderProfile == null) {
continue;
}
for (Map.Entry<User, List<Reminder>> entry : reminderProfile.getReminders().entrySet()) {
User user = entry.getKey();
List<Reminder> toRemove = new ArrayList<>();
List<Reminder> reminders = entry.getValue();
for (Reminder reminder : reminders) {
if (!reminder.isExpired()) {
continue;
}
toRemove.add(reminder);
TextChannel channel = reminder.getChannel();
if (channel != null) {
channel.sendMessage("Hey %s! ⏰ It's time for your reminder: `%s`".formatted(
user.getAsMention(),
reminder.getReminder()
)).queue();
}
}
toRemove.forEach(reminder -> reminderProfile.removeReminder(user, reminder));
}
}
}
}

View File

@ -0,0 +1,143 @@
package cc.fascinated.bat.features.reminder;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.*;
/**
* @author Fascinated (fascinated7)
*/
@Getter
public class ReminderProfile extends Serializable {
/**
* The reminders in the guild
*/
private final Map<User, List<Reminder>> reminders = new HashMap<>();
/*
* Get the amount of reminders a user has
*
* @param user The user to get the reminders for
* @return The amount of reminders the user has
*/
public int getReminderCount(User user) {
List<Reminder> reminderList = reminders.get(user);
return reminderList == null ? 0 : reminderList.size();
}
/*
* Remove all reminders for a user
*
* @param user The user to remove the reminders for
*/
public void removeReminders(User user) {
reminders.remove(user);
}
/*
* Check if a user has reminders
*
* @param user The user to check for
* @return If the user has reminders
*/
public boolean hasReminders(User user) {
return reminders.containsKey(user);
}
/*
* Get the reminders for a user
*
* @param user The user to get the reminders for
* @return The reminders for the user
*/
public List<Reminder> getReminders(User user) {
return reminders.get(user);
}
/*
* Add a reminder for a user
*
* @param user The user to add the reminder for
* @param reminder The reminder to add
*/
public Reminder addReminder(User user, TextChannel channel, String reason, Date endDate) {
List<Reminder> reminderList = reminders.get(user);
if (reminderList == null) {
reminderList = new ArrayList<>();
}
Reminder reminder = new Reminder(reason, channel.getId(), endDate);
reminderList.add(reminder);
reminders.put(user, reminderList);
return reminder;
}
/*
* Remove a reminder for a user
*
* @param user The user to remove the reminder for
* @param reminder The reminder to remove
*/
public void removeReminder(User user, Reminder reminder) {
List<Reminder> reminderList = reminders.get(user);
if (reminderList == null) {
return;
}
reminderList.remove(reminder);
reminders.put(user, reminderList);
if (reminderList.isEmpty()) {
reminders.remove(user);
}
}
@Override
public void load(Document document, Gson gson) {
for (String key : document.keySet()) {
User user = DiscordService.JDA.getUserById(key);
if (user == null) {
continue;
}
List<Reminder> reminderList = new ArrayList<>();
for (Document reminderDocument : document.getList(key, Document.class)) {
reminderList.add(new Reminder(
reminderDocument.getString("reminder"),
reminderDocument.getString("channelId"),
reminderDocument.getDate("endDate")
));
}
reminders.put(user, reminderList);
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (Map.Entry<User, List<Reminder>> entry : reminders.entrySet()) {
List<Document> reminderDocuments = new ArrayList<>();
List<Reminder> value = entry.getValue();
if (value == null || value.isEmpty()) {
continue;
}
for (Reminder reminder : value) {
Document reminderDocument = new Document();
reminderDocument.append("reminder", reminder.getReminder());
reminderDocument.append("channelId", reminder.getChannelId());
reminderDocument.append("endDate", reminder.getEndDate());
reminderDocuments.add(reminderDocument);
}
document.append(entry.getKey().getId(), reminderDocuments);
}
return document;
}
@Override
public void reset() {
reminders.clear();
}
}

View File

@ -0,0 +1,37 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("reminder:clear.sub")
@CommandInfo(name = "clear", description = "Clear all your active reminders.")
public class ClearSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (!profile.hasReminders(user.getDiscordUser())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have any active reminders.")
.build()).queue();
return;
}
int reminderCount = profile.getReminderCount(user.getDiscordUser());
profile.removeReminders(user.getDiscordUser());
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully cleared %s reminders.".formatted(reminderCount))
.build()
).queue();
}
}

View File

@ -0,0 +1,46 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.reminder.Reminder;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("reminder:list.sub")
@CommandInfo(name = "list", description = "View your active reminders.")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (!profile.hasReminders(user.getDiscordUser())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You do not have any active reminders.")
.build()).queue();
return;
}
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Active Reminders");
for (Reminder reminder : profile.getReminders(user.getDiscordUser())) {
description.appendLine("%s - <t:%s:R> %s".formatted(
reminder.getReminder(),
reminder.getEndDate().toInstant().getEpochSecond(),
reminder.getChannel().getAsMention()
), true);
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(description.build())
.build()
).queue();
}
}

View File

@ -0,0 +1,22 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "reminder", description = "Set or view reminders.")
public class ReminderCommand extends BatCommand {
@Autowired
public ReminderCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(SetSubCommand.class));
super.addSubCommand(context.getBean(ListSubCommand.class));
super.addSubCommand(context.getBean(ClearSubCommand.class));
}
}

View File

@ -0,0 +1,80 @@
package cc.fascinated.bat.features.reminder.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.TimeUtils;
import cc.fascinated.bat.features.reminder.Reminder;
import cc.fascinated.bat.features.reminder.ReminderFeature;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Component("reminder:set.sub")
@CommandInfo(name = "set", description = "Set a reminder.")
public class SetSubCommand extends BatSubCommand {
public SetSubCommand() {
super.addOption(OptionType.STRING, "reminder", "The reminder to set.", true);
super.addOption(OptionType.STRING, "time", "After how long should the reminder be sent. (eg: 5m)", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ReminderProfile profile = guild.getReminderProfile();
if (profile.getReminderCount(user.getDiscordUser()) >= ReminderFeature.MAX_REMINDERS) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have reached the maximum amount of reminders.")
.build()).queue();
return;
}
OptionMapping reminderOption = event.getOption("reminder");
if (reminderOption == null) {
return;
}
OptionMapping timeOption = event.getOption("time");
if (timeOption == null) {
return;
}
String reminderText = reminderOption.getAsString();
long time = TimeUtils.fromString(timeOption.getAsString());
if (time == 0) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid time format.")
.build()).queue();
return;
}
if (time < TimeUnit.MINUTES.toMillis(1)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The time must be at least 1 minute.")
.build()).queue();
return;
}
if (time > ReminderFeature.MAX_REMINDER_LENGTH) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The time must be at most %s.".formatted(TimeUtils.format(ReminderFeature.MAX_REMINDER_LENGTH)))
.build()).queue();
return;
}
Reminder reminder = profile.addReminder(user.getDiscordUser(), event.getChannel().asTextChannel(), reminderText, new Date(System.currentTimeMillis() + time));
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Reminder for `%s` set, you will be reminded <t:%s:R>".formatted(reminderText,
reminder.getEndDate().toInstant().getEpochSecond()))
.build()).queue();
}
}

View File

@ -87,7 +87,7 @@ public class ScoreSaberCommand extends BatCommand {
.addField("Rank", "#" + NumberFormatter.formatCommas(account.getRank()), true)
.addField("Country Rank", "#" + NumberFormatter.formatCommas(account.getCountryRank()), true)
.addField("PP", NumberFormatter.formatCommas(account.getPp()), true)
.addField("Joined", "<t:%s>".formatted(DateUtils.getDateFromString(account.getFirstSeen()).toInstant().toEpochMilli() / 1000), true)
.addField("Joined", "<t:%s>".formatted(DateUtils.getDateFromString(account.getFirstSeen()).toInstant().getEpochSecond()), true)
.setTimestamp(LocalDateTime.now())
.setFooter(fetchTime > 3 ? "Fetched in %sms".formatted(fetchTime) : "Cached", "https://flagcdn.com/h120/%s.png".formatted(account.getCountry().toLowerCase()))
.setColor(Colors.DEFAULT)

View File

@ -29,7 +29,7 @@ import se.michaelthelin.spotify.model_objects.specification.Track;
public class SpotifyFeature extends Feature {
@Autowired
public SpotifyFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Spotify", true,Category.MUSIC);
super("Spotify", true, Category.MUSIC);
super.registerCommand(commandService, context.getBean(SpotifyCommand.class));
}

View File

@ -22,6 +22,7 @@ import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
@ -40,7 +41,9 @@ public class CurrentSubCommand extends BatSubCommand implements EventListener {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(SpotifyFeature.currentSong(spotifyService, user).build()).addComponents(createActions()).queue();
event.replyEmbeds(SpotifyFeature.currentSong(spotifyService, user).build()).addComponents(createActions()).queue(message -> {
message.editOriginalComponents(new ArrayList<>()).queueAfter(5, TimeUnit.MINUTES); // Remove the buttons after 5 minutes
});
}
@Override @SneakyThrows

View File

@ -0,0 +1,102 @@
package cc.fascinated.bat.features.tmdb;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.tmdb.command.TMDBCommand;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.TMDBService;
import info.movito.themoviedbapi.model.core.Movie;
import info.movito.themoviedbapi.model.core.TvSeries;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author Nick (okNick)
*/
@Component
public class TMDBFeature extends Feature {
@Autowired
public TMDBFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("TMDB", true, Category.MOVIES_TV);
super.registerCommand(commandService, context.getBean(TMDBCommand.class));
}
/**
* Create an embed for a movie page.
*
* @param tmdbService The TMDB service.
* @param query The query to search for.
* @param language The language to search in.
* @param primaryReleaseYear The primary release year to filter by.
* @param region The region to search in.
* @param year The year to filter by.
* @param movie The movie index.
* @param adult Whether to include adult content.
* @return The movie page embed.
*/
public static EmbedBuilder pageMovie(@NonNull TMDBService tmdbService, @NonNull String query, String language, String primaryReleaseYear, String region, String year, int movie, boolean adult) {
List<Movie> movieList = tmdbService.lookupMovies(query, adult, language, primaryReleaseYear, region, year);
if (movieList.isEmpty()) {
return EmbedUtils.errorEmbed()
.setDescription("No movieList found with the provided query + options!");
}
// Adjust movie index to stay within bounds
if (movie >= movieList.size()) {
movie = movieList.size() - 1; // Set to the last movie if index exceeds list size
}
return EmbedUtils.genericEmbed()
.setAuthor(movieList.get(movie).getTitle(), "https://www.themoviedb.org/movie/%s".formatted(movieList.get(movie).getId()))
.setThumbnail("https://media.themoviedb.org/t/p/w220_and_h330_face%s".formatted(movieList.get(movie).getPosterPath()))
.setDescription(movieList.get(movie).getOverview())
.addField("Release Date", movieList.get(movie).getReleaseDate(), true)
.addField("Rating", NumberFormatter.format(movieList.get(movie).getVoteAverage()) + "/10", true)
.addField("Language", movieList.get(movie).getOriginalLanguage(), true)
.setFooter("Page %s of %s".formatted(movie + 1, movieList.size()));
}
/**
* Create an embed for a series page.
*
* @param tmdbService The TMDB service.
* @param query The query to search for.
* @param language The language to search in.
* @param firstAirDateYear The first air date year to filter by.
* @param year The year to filter by.
* @param series The series index.
* @param adult Whether to include adult content.
* @return The series page embed.
*/
public static EmbedBuilder pageSeries(@NonNull TMDBService tmdbService, @NonNull String query, String language, int firstAirDateYear, int year, int series, boolean adult) {
List<TvSeries> seriesList = tmdbService.lookupSeries(query, adult, language, firstAirDateYear, year);
if (seriesList.isEmpty()) {
return EmbedUtils.errorEmbed()
.setDescription("No series found with the provided query + options!");
}
// Adjust series index to stay within bounds
if (series >= seriesList.size()) {
series = seriesList.size() - 1; // Set to the last series if index exceeds list size
}
return EmbedUtils.genericEmbed()
.setAuthor(seriesList.get(series).getName(), "https://www.themoviedb.org/tv/%s".formatted(seriesList.get(series).getId()))
.setThumbnail("https://media.themoviedb.org/t/p/w220_and_h330_face%s".formatted(seriesList.get(series).getPosterPath()))
.setDescription(seriesList.get(series).getOverview())
.addField("First Air Date", seriesList.get(series).getFirstAirDate(), true)
.addField("Rating", NumberFormatter.format(seriesList.get(series).getVoteAverage()) + "/10", true)
.addField("Language", seriesList.get(series).getOriginalLanguage(), true)
.setFooter("Page %s of %s".formatted(series + 1, seriesList.size()));
}
}

View File

@ -0,0 +1,134 @@
package cc.fascinated.bat.features.tmdb.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.tmdb.TMDBFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.TMDBService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@CommandInfo(name = "movie", description = "Get information about a movie")
public class MovieSubCommand extends BatSubCommand implements EventListener {
private final TMDBService tmdbService;
private final Map<String, Map<String, String>> userCommands; // Map to store user commands and their parameters
@Autowired
public MovieSubCommand(@NonNull TMDBService tmdbService) {
this.tmdbService = tmdbService;
this.userCommands = new HashMap<>();
super.addOption(OptionType.STRING, "title", "The title of the movie", true);
super.addOption(OptionType.STRING, "language", "A locale code (en-US) to lookup movies in a specific language", false);
super.addOption(OptionType.STRING, "primary_release_year", "Filter the results so that only the primary release dates have this value", false);
super.addOption(OptionType.STRING, "region", "An ISO 3166-1 code (US) to lookup movies from a specific region", false);
super.addOption(OptionType.STRING, "year", "Filter the results release dates to matches that include this value", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping titleOption = event.getOption("title");
if (titleOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a title to search for!")
.build())
.queue();
return;
}
// Determine if the channel is NSFW. If so, allow adult content
boolean adult = false;
if (event.getChannel() instanceof TextChannel textChannel) {
adult = textChannel.isNSFW();
}
OptionMapping languageOption = event.getOption("language");
OptionMapping primaryReleaseYearOption = event.getOption("primary_release_year");
OptionMapping regionOption = event.getOption("region");
OptionMapping yearOption = event.getOption("year");
// Store user command and parameters for later use
Map<String, String> params = new HashMap<>();
params.put("title", titleOption.getAsString());
if (languageOption != null) params.put("language", languageOption.getAsString());
if (primaryReleaseYearOption != null) params.put("primary_release_year", primaryReleaseYearOption.getAsString());
if (regionOption != null) params.put("region", regionOption.getAsString());
if (yearOption != null) params.put("year", yearOption.getAsString());
params.put("adult", String.valueOf(adult));
userCommands.put(user.getId(), params);
event.replyEmbeds(TMDBFeature.pageMovie(
tmdbService,
titleOption.getAsString(),
(languageOption != null ? languageOption.getAsString() : null),
(primaryReleaseYearOption != null ? primaryReleaseYearOption.getAsString() : null),
(regionOption != null ? regionOption.getAsString() : null),
(yearOption != null ? yearOption.getAsString() : null),
0, // Initial page number
adult
).build()
).addActionRow(
Button.primary("backMovie", "Back").withEmoji(Emoji.fromFormatted("⬅️")),
Button.primary("nextMovie", "Next").withEmoji(Emoji.fromFormatted("➡️"))
).queue();
}
@Override
public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
Map<String, String> params = userCommands.get(user.getId());
if (params == null) {
return;
}
int currentPage = Integer.parseInt(params.getOrDefault("page", "0"));
boolean adult = Boolean.parseBoolean(params.get("adult"));
// Retrieve stored parameters
String title = params.get("title");
String language = params.get("language");
String primaryReleaseYear = params.get("primary_release_year");
String region = params.get("region");
String year = params.get("year");
if (event.getComponentId().equals("backMovie")) {
currentPage--;
if (currentPage < 0) {
currentPage = 0; // Ensure currentPage doesn't go negative
}
} else if (event.getComponentId().equals("nextMovie")) {
currentPage++;
}
params.put("page", String.valueOf(currentPage));
event.editMessageEmbeds(TMDBFeature.pageMovie(
tmdbService,
title,
language,
primaryReleaseYear,
region,
year,
currentPage,
adult
).build()).queue();
}
}

View File

@ -0,0 +1,128 @@
package cc.fascinated.bat.features.tmdb.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.tmdb.TMDBFeature;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.TMDBService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@CommandInfo(name = "series", description = "Get information about a series")
public class SeriesSubCommand extends BatSubCommand implements EventListener {
private final TMDBService tmdbService;
private final Map<String, Map<String, String>> userCommands; // Map to store user commands and their parameters
@Autowired
public SeriesSubCommand(@NonNull TMDBService tmdbService) {
this.tmdbService = tmdbService;
this.userCommands = new HashMap<>();
super.addOption(OptionType.STRING, "title", "The title of the series", true);
super.addOption(OptionType.STRING, "language", "A locale code (en-US) to lookup movies in a specific language", false);
super.addOption(OptionType.INTEGER, "first_air_year", "Filter the results so that only the first air year has this value", false);
super.addOption(OptionType.INTEGER, "year", "Filter the results release dates to matches that include this value", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping titleOption = event.getOption("title");
if (titleOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a title to search for!")
.build())
.queue();
return;
}
// Determine if the channel is NSFW. If so, allow adult content
boolean adult = false;
if (event.getChannel() instanceof TextChannel textChannel) {
adult = textChannel.isNSFW();
}
OptionMapping languageOption = event.getOption("language");
OptionMapping firstAirYearOption = event.getOption("first_air_year");
OptionMapping yearOption = event.getOption("year");
// Store user command and parameters for later use
Map<String, String> params = new HashMap<>();
params.put("title", titleOption.getAsString());
if (languageOption != null) params.put("language", languageOption.getAsString());
if (firstAirYearOption != null) params.put("first_air_year", String.valueOf(firstAirYearOption.getAsInt()));
if (yearOption != null) params.put("year", String.valueOf(yearOption.getAsInt()));
params.put("adult", String.valueOf(adult));
userCommands.put(user.getId(), params);
event.replyEmbeds(TMDBFeature.pageSeries(
tmdbService,
titleOption.getAsString(),
(languageOption != null ? languageOption.getAsString() : null),
(firstAirYearOption != null ? firstAirYearOption.getAsInt() : -1),
(yearOption != null ? yearOption.getAsInt() : -1),
0, // Initial page number
adult
).build()
).addActionRow(
Button.primary("backSeries", "Back").withEmoji(Emoji.fromFormatted("⬅️")),
Button.primary("nextSeries", "Next").withEmoji(Emoji.fromFormatted("➡️"))
).queue();
}
@Override
public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) {
Map<String, String> params = userCommands.get(user.getId());
if (params == null) {
return;
}
int currentPage = Integer.parseInt(params.getOrDefault("page", "0"));
boolean adult = Boolean.parseBoolean(params.get("adult"));
// Retrieve stored parameters
String title = params.get("title");
String language = params.get("language");
int firstAirYear = (params.get("first_air_year") != null ? Integer.parseInt(params.get("first_air_year")) : -1);
int year = (params.get("year") != null ? Integer.parseInt(params.get("year")) : -1);
if (event.getComponentId().equals("backSeries")) {
currentPage--;
if (currentPage < 0) {
currentPage = 0; // Ensure currentPage doesn't go negative
}
} else if (event.getComponentId().equals("nextSeries")) {
currentPage++;
}
params.put("page", String.valueOf(currentPage));
event.editMessageEmbeds(TMDBFeature.pageSeries(
tmdbService,
title,
language,
firstAirYear,
year,
currentPage,
adult
).build()).queue();
}
}

View File

@ -0,0 +1,21 @@
package cc.fascinated.bat.features.tmdb.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "tmdb", description = "Get information about movies and TV shows", guildOnly = false)
public class TMDBCommand extends BatCommand {
@Autowired
public TMDBCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(MovieSubCommand.class));
super.addSubCommand(context.getBean(SeriesSubCommand.class));
}
}

View File

@ -0,0 +1,50 @@
package cc.fascinated.bat.features.welcomer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import net.dv8tion.jda.api.EmbedBuilder;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class WelcomerEmbed {
/**
* The title of the embed
*/
private String title;
/**
* The description of the embed
*/
@NonNull private String description;
/**
* The color of the embed
*/
@NonNull private String color;
/**
* Should we ping the user before sending the message?
*/
private boolean pingBeforeSend;
/**
* Builds the embed and replaces the placeholders
*
* @return The built embed
*/
public EmbedBuilder buildEmbed(Object... replacements) {
EmbedBuilder embedBuilder = new EmbedBuilder();
if (title != null) {
embedBuilder.setTitle(WelcomerPlaceholders.replaceAllPlaceholders(title, replacements));
}
embedBuilder.setDescription(WelcomerPlaceholders.replaceAllPlaceholders(description, replacements));
embedBuilder.setColor(Integer.parseInt(color, 16));
return embedBuilder;
}
}

View File

@ -0,0 +1,23 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.welcomer.command.WelcomerCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class WelcomerFeature extends Feature {
@Autowired
public WelcomerFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Welcomer", true,Category.SERVER);
super.registerCommand(commandService, context.getBean(WelcomerCommand.class));
}
}

View File

@ -0,0 +1,32 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class WelcomerListener implements EventListener {
private final WelcomerFeature welcomerFeature;
@Autowired
public WelcomerListener(WelcomerFeature welcomerFeature) {
this.welcomerFeature = welcomerFeature;
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
if (guild.getFeatureProfile().isFeatureDisabled(welcomerFeature)) { // Check if the feature is disabled
return;
}
WelcomerProfile profile = guild.getWelcomerProfile();
profile.sendWelcomeMessage(guild, user);
}
}

View File

@ -0,0 +1,27 @@
package cc.fascinated.bat.features.welcomer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class WelcomerMessage {
/**
* The message to send
*/
private String message;
/**
* Builds the message and replaces the placeholders
*
* @return The built message
*/
public String buildMessage(Object... replacements) {
return WelcomerPlaceholders.replaceAllPlaceholders(message, replacements);
}
}

View File

@ -0,0 +1,82 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public enum WelcomerPlaceholders {
USER_MENTION("{user_mention}", BatUser.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatUser) object).getDiscordUser().getAsMention();
}
},
USER_NAME("{user_name}", BatUser.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatUser) object).getName();
}
},
GUILD_NAME("{guild_name}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return ((BatGuild) object).getName();
}
},
JOIN_DATE("{join_date}", null) {
@Override
public String replacePlaceholder(Object object) {
return "<t:%s>".formatted(new Date().toInstant().getEpochSecond());
}
};
/**
* The placeholder string that will get replaced
*/
private final String placeholder;
/**
* The class that the placeholder is associated with
*/
private final Class<?> clazz;
/**
* Replaces the placeholder with the string based on the overridden method
*
* @param object The object to replace the placeholder with
* @return The string with the placeholder replaced
*/
public String replacePlaceholder(Object object) {
if (clazz != null && !clazz.isInstance(object)) {
throw new IllegalArgumentException("Object is not an instance of " + clazz.getName());
}
return null;
}
/**
* Replaces all placeholders in a message with the objects provided
*
* @param message The message to replace the placeholders in
* @param objects The objects to replace the placeholders with
* @return The message with the placeholders replaced
*/
public static String replaceAllPlaceholders(String message, Object... objects) {
for (WelcomerPlaceholders placeholder : values()) {
for (Object object : objects) {
if (placeholder.getClazz() != null && !placeholder.getClazz().isInstance(object)) {
continue;
}
message = message.replace(placeholder.getPlaceholder(), placeholder.replacePlaceholder(object));
}
}
return message;
}
}

View File

@ -0,0 +1,146 @@
package cc.fascinated.bat.features.welcomer;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Getter @Setter
public class WelcomerProfile extends Serializable {
/**
* The welcomer message, null if we're using an embed
*/
private WelcomerMessage welcomerMessage;
/**
* The welcomer embed, null if we're using a message
*/
private WelcomerEmbed welcomerEmbed;
/**
* The channel to send the welcomer messages to
*/
private TextChannel channel;
/**
* Gets the welcomer message
*
* @return The welcomer message, false if we're using an embed
*/
public boolean isEmbed() {
return welcomerEmbed != null;
}
/**
* Gets the welcomer message
*
* @return The welcomer message, false if we're using an embed
*/
public boolean isMessage() {
return welcomerMessage != null;
}
/**
* Sets the welcomer message
* <p>
* This will disable the embed if it's enabled
* </p>
*/
public void setMessage(String message) {
welcomerMessage = new WelcomerMessage(message);
welcomerEmbed = null;
}
/**
* Sets the welcomer embed
* <p>
* This will disable the message if it's enabled
* <p>
*/
public void setEmbed(String title, String description, String color, boolean pingBeforeSend) {
welcomerEmbed = new WelcomerEmbed(title, description, color, pingBeforeSend);
welcomerMessage = null;
}
/**
* Sends the welcome message to the user
*
* @param guild The guild to send the message in
* @param user The user to send the message to
*/
public void sendWelcomeMessage(BatGuild guild, BatUser user) {
if (this.channel == null || (!this.isMessage() && !this.isEmbed())) {
return;
}
if (welcomerEmbed != null) {
if (welcomerEmbed.isPingBeforeSend()) { // Ping the user before sending the message
this.channel.sendMessage(user.getDiscordUser().getAsMention()).queue();
}
this.channel.sendMessageEmbeds(welcomerEmbed.buildEmbed(guild, user).build()).queue();
return;
}
if (welcomerMessage != null) {
this.channel.sendMessage(welcomerMessage.buildMessage(guild, user)).queue();
}
}
@Override
public void load(Document document, Gson gson) {
Document welcomerMessageDocument = document.get("welcomerMessage", Document.class);
if (welcomerMessageDocument != null) {
welcomerMessage = new WelcomerMessage(
welcomerMessageDocument.getString("message")
);
}
Document welcomerEmbedDocument = document.get("welcomerEmbed", Document.class);
if (welcomerEmbedDocument != null) {
welcomerEmbed = new WelcomerEmbed(
welcomerEmbedDocument.getString("title"),
welcomerEmbedDocument.getString("description"),
welcomerEmbedDocument.getString("color"),
welcomerEmbedDocument.getBoolean("pingBeforeSend")
);
}
String channelId = document.getString("channelId");
if (channelId != null) {
TextChannel textChannel = DiscordService.JDA.getTextChannelById(channelId);
if (textChannel != null) {
channel = textChannel;
}
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
if (welcomerMessage != null) {
document.put("welcomerMessage", new Document("message", welcomerMessage.getMessage()));
}
if (welcomerEmbed != null) {
document.put("welcomerEmbed", new Document()
.append("title", welcomerEmbed.getTitle())
.append("description", welcomerEmbed.getDescription())
.append("color", welcomerEmbed.getColor())
.append("pingBeforeSend", welcomerEmbed.isPingBeforeSend())
);
}
if (channel != null) {
document.put("channelId", channel.getId());
}
return document;
}
@Override
public void reset() {
welcomerMessage = null;
welcomerEmbed = null;
}
}

View File

@ -0,0 +1,41 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:channel.sub")
@CommandInfo(name = "channel", description = "Set the welcomer channel")
public class ChannelSubCommand extends BatSubCommand {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel to send the welcomer messages to", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping channelOption = event.getOption("channel");
if (channelOption == null) {
return;
}
TextChannel textChannel = channelOption.getAsChannel().asTextChannel();
profile.setChannel(textChannel);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The welcomer channel has been set to %s".formatted(textChannel.getAsMention()))
.build()).queue();
}
}

View File

@ -0,0 +1,64 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerPlaceholders;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:current.sub")
@CommandInfo(name = "current", description = "View the current welcomer configuration")
public class CurrentSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
if (!profile.isEmbed() && !profile.isMessage()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The welcomer is not configured.\n\n" + getPlaceholders(guild, user))
.build()).queue();
return;
}
if (profile.isEmbed()) {
event.replyEmbeds(profile.getWelcomerEmbed().buildEmbed(guild, user)
.appendDescription("\n\n" + getPlaceholders(guild, user))
.build()).queue();
return;
}
if (profile.isMessage()) {
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("**Preview:** %s\n*note: the real message won't be an embed*\n\n%s".formatted(
profile.getWelcomerMessage().buildMessage(guild, user),
getPlaceholders(guild, user)
))
.build()).queue();
}
}
/**
* Get the placeholders that the user can use
*
* @param replacements What to replace the placeholders using
* @return The placeholders
*/
public String getPlaceholders(Object... replacements) {
StringBuilder builder = new StringBuilder();
builder.append("**Available Placeholders:**\n");
for (WelcomerPlaceholders placeholder : WelcomerPlaceholders.values()) {
String placeholderString = placeholder.getPlaceholder();
builder.append("`").append(placeholderString).append("` - ")
.append(WelcomerPlaceholders.replaceAllPlaceholders(placeholderString, replacements)).append("\n");
}
return builder.toString();
}
}

View File

@ -0,0 +1,98 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.awt.*;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:embed.sub")
@CommandInfo(name = "embed", description = "Set the welcomer embed (this will remove the welcomer plain message if set)")
public class EmbedSubCommand extends BatSubCommand {
public EmbedSubCommand() {
super.addOption(OptionType.BOOLEAN, "ping-before-send", "Should we ping the user before sending the message?", true);
super.addOption(OptionType.STRING, "description", "The description of the embed", true);
super.addOption(OptionType.STRING, "color", "The color of the embed", true);
super.addOption(OptionType.STRING, "title", "The title of the embed (only set if you want a title)", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping titleOption = event.getOption("title");
OptionMapping descriptionOption = event.getOption("description");
OptionMapping colorOption = event.getOption("color");
OptionMapping pingBeforeSendOption = event.getOption("ping-before-send");
if (descriptionOption == null || colorOption == null || pingBeforeSendOption == null) {
return;
}
String title = titleOption == null ? null : titleOption.getAsString();
String description = descriptionOption.getAsString();
String color = colorOption.getAsString();
boolean pingBeforeSend = pingBeforeSendOption.getAsBoolean();
// Remove # if the user added it
color = color.replace("#", "");
// Validate the input
if (color.length() != 6 || Color.decode("#" + color).getRGB() == -1){
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The color must be a valid hex color code\n" +
"You can use this website to get a hex color code: https://htmlcolorcodes.com")
.build()).queue();
return;
}
if (title != null && title.length() > 128) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The title must be less than 128 characters")
.build()).queue();
return;
}
if (description.length() > 512) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The description must be less than 512 characters")
.build()).queue();
return;
}
boolean isMessageEnabled = profile.isMessage();
profile.setEmbed(title, description, color, pingBeforeSend);
EmbedDescriptionBuilder successDescription = new EmbedDescriptionBuilder("Welcomer Embed")
.appendLine("%s Successfully set the welcomer embed!".formatted(Emojis.CHECK_MARK_EMOJI), false);
if (isMessageEnabled) {
successDescription.appendLine("*This has removed the plain message welcomer*", false);
}
successDescription.emptyLine();
successDescription.appendLine("**Configuration:**", false);
if (title != null) {
successDescription.appendLine("Title: `%s`".formatted(title), true);
}
successDescription.appendLine("Description: `%s`".formatted(description), true);
successDescription.appendLine("Color: `#%s`".formatted(color), true);
successDescription.appendLine("Ping Before Send: %s".formatted(pingBeforeSend ? Emojis.CHECK_MARK_EMOJI.getFormatted() + " *(Preview won't ping you)*" : Emojis.CROSS_MARK_EMOJI), true);
successDescription.emptyLine();
successDescription.appendLine("**Preview Below:**", false);
event.replyEmbeds(
EmbedUtils.successEmbed()
.setDescription(successDescription.build())
.build(),
profile.getWelcomerEmbed().buildEmbed(guild, user).build()
).queue();
}
}

View File

@ -0,0 +1,52 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerPlaceholders;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:message.sub")
@CommandInfo(name = "message", description = "Set the welcomer message (this will remove the welcomer embed if set)")
public class MessageSubCommand extends BatSubCommand {
public MessageSubCommand() {
super.addOption(OptionType.STRING, "message", "The message to send", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
OptionMapping messageOption = event.getOption("message");
if (messageOption == null) {
return;
}
String message = messageOption.getAsString();
boolean isEmbedEnabled = profile.isEmbed();
profile.setMessage(message);
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Welcomer Message")
.appendLine("%s Set the message to `%s`".formatted(Emojis.CHECK_MARK_EMOJI, message), false);
if (isEmbedEnabled) {
description.appendLine("*This has removed the embed welcomer*", false);
}
description.emptyLine();
description.appendLine("**Preview:** %s".formatted(WelcomerPlaceholders.replaceAllPlaceholders(message, guild, user)), false);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
}
}

View File

@ -0,0 +1,36 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("welcomer:reset.sub")
@CommandInfo(name = "reset", description = "Clear the welcomer configuration")
public class ResetSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
WelcomerProfile profile = guild.getWelcomerProfile();
if (!profile.isEmbed() && !profile.isMessage()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The welcomer is not configured")
.build()).queue();
return;
}
profile.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The welcomer configuration has been reset")
.build()).queue();
}
}

View File

@ -0,0 +1,25 @@
package cc.fascinated.bat.features.welcomer.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "welcomer", description = "Configure the welcomer on your server", requiredPermissions = Permission.MANAGE_SERVER)
public class WelcomerCommand extends BatCommand {
@Autowired
public WelcomerCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(MessageSubCommand.class));
super.addSubCommand(context.getBean(EmbedSubCommand.class));
super.addSubCommand(context.getBean(CurrentSubCommand.class));
super.addSubCommand(context.getBean(ChannelSubCommand.class));
super.addSubCommand(context.getBean(ResetSubCommand.class));
}
}

View File

@ -5,7 +5,10 @@ import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.MongoService;
@ -17,7 +20,6 @@ import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
import java.util.HashMap;
@ -28,7 +30,6 @@ import java.util.Map;
*/
@Getter
@Setter
@Document(collection = "guilds")
public class BatGuild extends ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(BatGuild.class);
/**
@ -48,11 +49,21 @@ public class BatGuild extends ProfileHolder {
*/
private Date createdAt;
/**
* The guild as the JDA Guild
*/
private Guild guild;
public BatGuild(@NonNull String id, @NonNull org.bson.Document document) {
this.id = id;
this.document = document;
boolean newAccount = this.document.isEmpty();
this.createdAt = newAccount ? new Date() : document.getDate("createdAt");
Guild guild = DiscordService.JDA.getGuildById(id);
if (guild != null) {
this.guild = guild;
}
}
/**
@ -70,7 +81,10 @@ public class BatGuild extends ProfileHolder {
* @return the guild
*/
public Guild getDiscordGuild() {
return DiscordService.JDA.getGuildById(id);
if (guild == null) {
guild = DiscordService.JDA.getGuildById(id);
}
return guild;
}
/**
@ -109,10 +123,38 @@ public class BatGuild extends ProfileHolder {
return getProfile(BirthdayProfile.class);
}
/**
* Gets the log profile
*
* @return the log profile
*/
public LogProfile getLogProfile() {
return getProfile(LogProfile.class);
}
/**
* Gets the reminder profile
*
* @return the reminder profile
*/
public ReminderProfile getReminderProfile() {
return getProfile(ReminderProfile.class);
}
/**
* Gets the welcomer profile
*
* @return the welcomer profile
*/
public WelcomerProfile getWelcomerProfile() {
return getProfile(WelcomerProfile.class);
}
/**
* Saves the user
*/
public void save() {
org.bson.Document document = new org.bson.Document();
document.put("_id", id);
document.put("createdAt", createdAt);
@ -124,7 +166,7 @@ public class BatGuild extends ProfileHolder {
MongoService.INSTANCE.getGuildsCollection().replaceOne(
new org.bson.Document("_id", id),
this.getDocument(),
document,
new ReplaceOptions().upsert(true)
);
}

View File

@ -13,10 +13,7 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
import java.util.HashMap;
@ -28,9 +25,7 @@ import java.util.Map;
@RequiredArgsConstructor
@Getter
@Setter
@Document(collection = "users")
public class BatUser extends ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(BatUser.class);
/**
* The document that belongs to this user
*/
@ -53,6 +48,11 @@ public class BatUser extends ProfileHolder {
*/
private Date createdAt;
/**
* The discord user associated with this user
*/
private User user;
public BatUser(@NonNull String id, @NonNull org.bson.Document document) {
this.id = id;
this.document = document;
@ -61,6 +61,7 @@ public class BatUser extends ProfileHolder {
User user = DiscordService.JDA.getUserById(id);
if (user != null) {
this.user = user;
this.globalName = user.getGlobalName();
}
}
@ -78,7 +79,10 @@ public class BatUser extends ProfileHolder {
* @return the guild
*/
public User getDiscordUser() {
return DiscordService.JDA.getUserById(id);
if (user == null) {
user = DiscordService.JDA.getUserById(id);
}
return user;
}
/**
@ -103,6 +107,7 @@ public class BatUser extends ProfileHolder {
* Saves the user
*/
public void save() {
org.bson.Document document = new org.bson.Document();
document.put("_id", id);
document.put("createdAt", createdAt);
@ -114,7 +119,7 @@ public class BatUser extends ProfileHolder {
MongoService.INSTANCE.getUsersCollection().replaceOne(
new org.bson.Document("_id", id),
this.getDocument(),
document,
new ReplaceOptions().upsert(true)
);
}

View File

@ -0,0 +1,79 @@
package cc.fascinated.bat.model;
import cc.fascinated.bat.service.DiscordService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.springframework.data.redis.core.RedisHash;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter @Setter
@RedisHash(value = "DiscordMessage", timeToLive = 86400) // 24 hours
public class DiscordMessage {
/**
* The snowflake ID of the message
*/
private final String id;
/**
* The timestamp when the message was sent
*/
private final long timestamp;
/**
* The snowflake ID of the channel the message was sent in
*/
private final String channelId;
/**
* The snowflake ID of the guild the message was sent in
*/
private final String guildId;
/**
* The snowflake ID of the author of the message
*/
private final String authorId;
/**
* The content of the message
*/
private String content;
/**
* Whether the message was deleted
*/
private boolean deleted;
/**
* Gets the author of the message
*
* @return the author
*/
public User getAuthor() {
return DiscordService.JDA.getUserById(this.authorId);
}
/**
* Gets the channel the message was sent in
*
* @return the channel
*/
public TextChannel getChannel() {
return DiscordService.JDA.getTextChannelById(this.channelId);
}
/**
* Gets the URL of the message
*
* @return the URL
*/
public String getMessageUrl() {
return "https://discord.com/channels/%s/%s/%s".formatted(this.guildId, this.channelId, this.id);
}
}

View File

@ -0,0 +1,22 @@
package cc.fascinated.bat.model.token.paste;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter @Setter
public class PasteUploadToken {
/**
* The key of the paste
*/
private final String key;
/**
* The url of the paste
*/
private String url;
}

View File

@ -0,0 +1,9 @@
package cc.fascinated.bat.repository;
import cc.fascinated.bat.model.DiscordMessage;
import org.springframework.data.repository.CrudRepository;
/**
* @author Fascinated (fascinated7)
*/
public interface DiscordMessageRepository extends CrudRepository<DiscordMessage, String> {}

View File

@ -75,7 +75,7 @@ public class CommandService extends ListenerAdapter {
JDA jda = DiscordService.JDA;
long before = System.currentTimeMillis();
Guild adminGuild = jda.getGuildById(Consts.ADMIN_GUILD);
Guild adminGuild = jda.getGuildById(Config.INSTANCE.getAdminGuild());
if (!Config.isProduction()) {
if (adminGuild == null) {
log.error("Unable to find the admin guild to register commands");
@ -138,6 +138,7 @@ public class CommandService extends ListenerAdapter {
@Override
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
long before = System.currentTimeMillis();
Guild discordGuild = event.getGuild();
if (event.getUser().isBot()) {
return;
@ -230,9 +231,10 @@ public class CommandService extends ListenerAdapter {
}
}
log.info("Executing command \"{}\" for user \"{}\"", commandName, user.getDiscordUser().getName());
executor.execute(guild, user, ranInsideGuild ? event.getChannel().asTextChannel() : event.getChannel().asPrivateChannel(),
event.getMember(), event.getInteraction());
log.info("Executed command \"{}\" for user \"{}\" (took: {}ms)", commandName, user.getDiscordUser().getName(),
System.currentTimeMillis() - before);
} catch (Exception ex) {
log.error("An error occurred while executing command \"{}\"", commandName, ex);
event.replyEmbeds(EmbedUtils.errorEmbed()

View File

@ -0,0 +1,104 @@
package cc.fascinated.bat.service;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage;
import cc.fascinated.bat.repository.DiscordMessageRepository;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* @author Fascinated (fascinated7)
*/
@Service
@Log4j2
@DependsOn("discordService")
public class DiscordMessageService extends ListenerAdapter {
private final DiscordMessageRepository discordMessageRepository;
private final GuildService guildService;
private final UserService userService;
@Autowired
public DiscordMessageService(DiscordMessageRepository discordMessageRepository, @NonNull GuildService guildService,
@NonNull UserService userService) {
this.discordMessageRepository = discordMessageRepository;
this.guildService = guildService;
this.userService = userService;
DiscordService.JDA.addEventListener(this);
}
/**
* Gets the message with the given id
*
* @param messageId the id of the message
* @return the message with the given id
*/
public DiscordMessage getMessage(String messageId) {
Optional<DiscordMessage> optionalMessage = discordMessageRepository.findById(messageId);
return optionalMessage.orElse(null);
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
discordMessageRepository.save(new DiscordMessage(
event.getMessageId(),
event.getMessage().getTimeCreated().toInstant().toEpochMilli(),
event.getChannel().getId(),
event.getGuild().getId(),
event.getAuthor().getId(),
event.getMessage().getContentStripped(),
false
));
}
@Override
public void onMessageUpdate(@NotNull MessageUpdateEvent event) {
Optional<DiscordMessage> message = discordMessageRepository.findById(event.getMessageId());
DiscordMessage oldMessage = message.orElse(null);
if (oldMessage != null) {
if (oldMessage.getContent().equals(event.getMessage().getContentStripped())) {
return;
}
discordMessageRepository.delete(oldMessage);
}
DiscordMessage newMessage = new DiscordMessage(
event.getMessageId(),
event.getMessage().getTimeCreated().toInstant().toEpochMilli(),
event.getChannel().getId(),
event.getGuild().getId(),
event.getAuthor().getId(),
event.getMessage().getContentStripped(),
false
);
discordMessageRepository.save(newMessage);
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getAuthor().getId());
for (EventListener listener : EventService.LISTENERS) {
listener.onGuildMessageEdit(guild, user, oldMessage, newMessage, event);
}
}
@Override
public void onMessageDelete(@NotNull MessageDeleteEvent event) {
Optional<DiscordMessage> optionalMessage = discordMessageRepository.findById(event.getMessageId());
if (optionalMessage.isEmpty()) {
return;
}
DiscordMessage message = optionalMessage.get();
message.setDeleted(true);
discordMessageRepository.save(message);
}
}

View File

@ -2,7 +2,9 @@ package cc.fascinated.bat.service;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.common.TimerUtils;
import cc.fascinated.bat.config.Config;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
@ -11,6 +13,7 @@ import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import java.util.EnumSet;
@ -32,24 +35,27 @@ public class DiscordService {
"{guilds} guilds",
"{users} users",
"your ScoreSaber scores",
"/help for help"
"/help for help",
"/features to toggle features"
);
@Autowired
public DiscordService(
@NonNull ApplicationContext context,
@Value("${discord.token}") String token
) throws Exception {
context.getBean(Config.class); // Ensure the config is loaded
log.info("Starting Discord bot...");
JDA = JDABuilder.create(token, EnumSet.of(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_EMOJIS_AND_STICKERS,
GatewayIntent.GUILD_PRESENCES
GatewayIntent.GUILD_PRESENCES,
GatewayIntent.GUILD_VOICE_STATES
))
.disableCache(
CacheFlag.ACTIVITY,
CacheFlag.VOICE_STATE,
CacheFlag.CLIENT_STATUS,
CacheFlag.SCHEDULED_EVENTS
).build()

View File

@ -3,16 +3,28 @@ package cc.fascinated.bat.service;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.DiscordMessage;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.channel.ChannelCreateEvent;
import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent;
import net.dv8tion.jda.api.events.guild.GuildBanEvent;
import net.dv8tion.jda.api.events.guild.GuildUnbanEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
@ -36,15 +48,18 @@ public class EventService extends ListenerAdapter {
public static final Set<EventListener> LISTENERS = new HashSet<>();
private final GuildService guildService;
private final UserService userService;
private final DiscordMessageService discordMessageService;
@Autowired
public EventService(@NonNull GuildService guildService, @NonNull UserService userService, @NonNull ApplicationContext context) {
public EventService(@NonNull ApplicationContext context, @NonNull GuildService guildService, @NonNull UserService userService,
@NonNull DiscordMessageService discordMessageService) {
this.guildService = guildService;
this.userService = userService;
this.discordMessageService = discordMessageService;
DiscordService.JDA.addEventListener(this);
context.getBeansOfType(EventListener.class).values().forEach(this::registerListeners);
log.info("Registered {} listeners.", LISTENERS.size());
log.info("Registered {} event listeners.", LISTENERS.size());
}
/**
@ -95,6 +110,17 @@ public class EventService extends ListenerAdapter {
}
}
@Override
public void onMessageDelete(@NotNull MessageDeleteEvent event) {
BatGuild guild = guildService.getGuild(event.getGuild().getId());
DiscordMessage message = discordMessageService.getMessage(event.getMessageId());
BatUser user = message == null ? null : userService.getUser(message.getAuthorId());
for (EventListener listener : LISTENERS) {
listener.onGuildMessageDelete(guild, user, message, event);
}
}
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event) {
if (event.getUser().isBot()) {
@ -158,4 +184,124 @@ public class EventService extends ListenerAdapter {
listener.onGuildMemberUpdateNickname(guild, user, event.getOldNickname(), event.getNewNickname(), event);
}
}
@Override
public void onGuildMemberRoleAdd(@NotNull GuildMemberRoleAddEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberRoleAdd(guild, user, event.getRoles(), event);
}
}
@Override
public void onGuildMemberRoleRemove(@NotNull GuildMemberRoleRemoveEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberRoleRemove(guild, user, event.getRoles(), event);
}
}
@Override
public void onChannelCreate(@NotNull ChannelCreateEvent event) {
BatGuild guild = guildService.getGuild(event.getGuild().getId());
for (EventListener listener : LISTENERS) {
listener.onChannelCreate(guild, event);
}
}
@Override
public void onChannelDelete(@NotNull ChannelDeleteEvent event) {
BatGuild guild = guildService.getGuild(event.getGuild().getId());
for (EventListener listener : LISTENERS) {
listener.onChannelDelete(guild, event);
}
}
@Override
public void onGuildBan(@NotNull GuildBanEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberBan(guild, user, event);
}
}
@Override
public void onGuildUnban(@NotNull GuildUnbanEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberUnban(guild, user, event);
}
}
@Override
public void onGuildMemberUpdateTimeOut(@NotNull GuildMemberUpdateTimeOutEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberTimeout(guild, user, event);
}
}
@Override
public void onUserUpdateName(@NotNull UserUpdateNameEvent event) {
if (event.getUser().isBot()) {
return;
}
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onUserUpdateName(user, event.getOldName(), event.getNewName(), event);
}
}
@Override
public void onUserUpdateAvatar(@NotNull UserUpdateAvatarEvent event) {
if (event.getUser().isBot()) {
return;
}
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onUserUpdateAvatar(user, event.getOldAvatarUrl(), event.getNewAvatarUrl(), event);
}
}
@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
if (event.getEntity().getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getEntity().getGuild().getId());
BatUser user = userService.getUser(event.getEntity().getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildVoiceUpdate(guild, user, event);
}
}
}

View File

@ -12,12 +12,12 @@ import org.springframework.stereotype.Service;
@Service
public class MongoService {
public static MongoService INSTANCE;
private final MongoTemplate mongo;
private final MongoTemplate mongoTemplate;
@Autowired
public MongoService(MongoTemplate mongo) {
INSTANCE = this;
this.mongo = mongo;
this.mongoTemplate = mongo;
}
/**
@ -26,7 +26,7 @@ public class MongoService {
* @return The guilds collection
*/
public MongoCollection<Document> getGuildsCollection() {
return mongo.getCollection("guilds");
return mongoTemplate.getCollection("guilds");
}
/**
@ -35,6 +35,6 @@ public class MongoService {
* @return The users collection
*/
public MongoCollection<Document> getUsersCollection() {
return mongo.getCollection("users");
return mongoTemplate.getCollection("users");
}
}

View File

@ -0,0 +1,77 @@
package cc.fascinated.bat.service;
import info.movito.themoviedbapi.TmdbApi;
import info.movito.themoviedbapi.model.core.Movie;
import info.movito.themoviedbapi.model.core.MovieResultsPage;
import info.movito.themoviedbapi.model.core.TvSeries;
import info.movito.themoviedbapi.model.core.TvSeriesResultsPage;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Nick (okNick)
*/
@Service
@Getter
@Log4j2(topic = "TMDB Service")
public class TMDBService {
/**
* The API key.
*/
private final String apiKey;
/**
* The TMDB API instance.
*/
private final TmdbApi tmdbApi;
public TMDBService(@Value("${tmdb.api-key}") String apiKey) {
this.apiKey = apiKey;
this.tmdbApi = new TmdbApi(apiKey);
}
/**
* Lookup movies based on the provided query and options.
*
* @param query The query to search for
* @param includeAdult Whether to include adult content
* @param language The language to search in
* @param primaryReleaseYear The primary release year to filter by
* @param region The region to search in
* @param year The year to filter by
* @return The list of movies found with the provided query and options
*/
@SneakyThrows
public List<Movie> lookupMovies(String query, boolean includeAdult, String language, String primaryReleaseYear, String region, String year) {
MovieResultsPage movies = tmdbApi.getSearch().searchMovie(query, includeAdult, language, primaryReleaseYear, 1, region, year);
if (movies.getTotalResults() == 0) {
return null;
}
return movies.getResults();
}
/**
* Lookup series based on the provided query and options.
*
* @param query The query to search for
* @param includeAdult Whether to include adult content
* @param language The language to search in
* @param firstAirDateYear The first air date year to filter by
* @param year The year to filter by
* @return The list of series found with the provided query and options
*/
@SneakyThrows
public List<TvSeries> lookupSeries(String query, boolean includeAdult, String language, int firstAirDateYear, int year) {
TvSeriesResultsPage series = tmdbApi.getSearch().searchTv(query, firstAirDateYear, includeAdult, language, 1, year);
if (series.getTotalResults() == 0) {
return null;
}
return series.getResults();
}
}

View File

@ -2,11 +2,14 @@ package cc.fascinated.bat.service;
import cc.fascinated.bat.common.TimerUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import com.mongodb.client.model.Filters;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
@ -55,23 +58,28 @@ public class UserService implements EventListener {
if (users.containsKey(id)) {
return users.get(id);
}
if (DiscordService.JDA.getUserById(id) == null) {
log.warn("Attempted to get user with ID \"{}\" but it does not exist", id);
User user = DiscordService.JDA.getUserById(id);
if (user == null) {
log.warn("Attempted to get user with ID \"{}\" but they do not exist", id);
return null;
}
if (user.isBot()) {
log.warn("Attempted to get user with ID \"{}\" but they are a bot", id);
return null;
}
// User is not cached
Document document = MongoService.INSTANCE.getUsersCollection().find(Filters.eq("_id", id)).first();
if (document != null) {
BatUser user = new BatUser(id, document);
users.put(id, user);
log.info("Loaded user \"{}\" in {}ms", user.getName(),System.currentTimeMillis() - before);
return user;
BatUser batUser = new BatUser(id, document);
users.put(id, batUser);
log.info("Loaded user \"{}\" in {}ms", batUser.getName(),System.currentTimeMillis() - before);
return batUser;
}
// New user
BatUser user = new BatUser(id, new Document());
users.put(id, user);
BatUser batUser = new BatUser(id, new Document());
users.put(id, batUser);
log.info("Created user \"{}\" - \"{}\"", user.getName(), user.getId());
return user;
return batUser;
}
@Override
@ -83,9 +91,13 @@ public class UserService implements EventListener {
log.info("Saved all users.");
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
user.setGlobalName(event.getUser().getName()); // Ensure the user's name is up-to-date
}
@Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
log.info("User \"{}\" changed their name from \"{}\" to \"{}\"", user.getName(), oldName, newName);
user.setGlobalName(newName);
}
}

View File

@ -2,6 +2,12 @@
discord:
token: "oh my goodnesssssssssss"
# Bat Configuration
bat:
# This is where commands will be registered (whilst in development mode)
# also where bot owner only commands will be registered
admin-guild: 1203163422498361404
# Sentry Configuration
sentry:
dsn: "CHANGE_ME"
@ -20,6 +26,11 @@ spotify:
client-id: "spotify-client-id"
client-secret: "spotify-client-secret"
# TMDB Configuration
tmdb:
# API Read Access Token
api-key: "api-read-access-token"
# Spring Configuration
spring:
data:
@ -27,4 +38,11 @@ spring:
mongodb:
uri: "mongodb://bat:p4$$w0rd@localhost:27017"
database: "bat"
auto-index-creation: true # Automatically create collection indexes
auto-index-creation: true # Automatically create collection indexes
# Redis - This is used for caching
redis:
host: "localhost"
port: 6379
database: 0
auth: "" # Leave blank for no auth

30
terms-of-service.txt Normal file
View File

@ -0,0 +1,30 @@
Terms of Service (ToS)
1. Introduction
Welcome to Bat! These Terms of Service ("ToS") govern your use of our Discord bot (the "Service"). By using the Service, you agree to be bound by these terms. If you do not agree, do not use the Service.
2. Use of Service
Eligibility: You must comply with Discords Terms of Service.
License: We grant you a limited, non-exclusive, non-transferable, and revocable license to use the Service.
Prohibited Conduct: You agree not to misuse the Service. Prohibited actions include, but are not limited to, spamming, harassment, or engaging in any illegal activities.
3. Content
User Content: You are responsible for any content you post or share using the Service. We do not claim ownership of your content, but you grant us a license to use it for the purpose of operating the Service.
Bot Content: We strive to ensure our bot provides accurate and helpful content, but we do not guarantee its accuracy.
4. Privacy Policy
Your privacy is important to us. Please refer to our Privacy Policy to understand how we collect, use, and protect your information.
5. Limitation of Liability
To the fullest extent permitted by law, we shall not be liable for any indirect, incidental, special, or consequential damages arising out of or in connection with your use of the Service.
6. Termination
We reserve the right to suspend or terminate your access to the Service at any time, with or without cause or notice.
7. Changes to the ToS
We may modify these ToS at any time. We will notify you of any changes by posting the new ToS on our https://discord.gg/yjj2U3ctEG.
8. Contact Us
If you have any questions about these ToS, please contact us at bat@fascinated.cc.