Compare commits

126 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
4bf099d25e Merge remote-tracking branch 'origin/master' 2024-07-01 19:40:38 +01:00
20c5f71cd4 fix user leaving guild npe 2024-07-01 19:40:32 +01:00
a6e490dbe5 fix npe 2024-07-01 19:40:16 +01:00
Lee
87bf0b9f67 Merge pull request 'Configure Renovate' (#3) from renovate/configure into master
Reviewed-on: Fascinated/Bat#3
2024-07-01 15:01:23 +00:00
419bdf1fea Add renovate.json 2024-07-01 15:00:36 +00:00
f2b2dbc794 rename interaction to event 2024-07-01 15:27:39 +01:00
8361f3c784 update member count command to look similar to botstats command 2024-07-01 15:23:19 +01:00
52349a17c3 cleanup imports 2024-07-01 15:21:09 +01:00
c1f9bfec6a don't remove birthdays for members that have left 2024-07-01 15:20:49 +01:00
be7f8a9057 only check guilds that we're in 2024-07-01 15:16:06 +01:00
c93e112ebf might fix, who knows 2024-07-01 15:14:42 +01:00
7485bd2ec8 fix premium admin command 2024-07-01 01:46:56 +01:00
ed83175a39 add env for admin guild 2024-07-01 01:44:22 +01:00
a001f2dd4c add valid guild and user id check 2024-07-01 01:41:40 +01:00
b1785ce373 impl pre shutdown saving 2024-07-01 01:33:52 +01:00
b1f5db9b2d maybe fix this? 2024-07-01 01:26:10 +01:00
d372c41c98 big ass refactor to handle loading guilds and users without spring to make it more futureproof 2024-07-01 01:12:32 +01:00
f566c3bcb5 add emojis to feature states 2024-06-30 08:34:59 +01:00
6403c57db5 add checks for some events to see if the feature is enabled and more cleanup 2024-06-30 08:24:14 +01:00
22d4558d84 use an enum for feature states 2024-06-30 08:10:49 +01:00
ea546f02ca cleanup features and move all misc commands into a base feature 2024-06-30 08:00:03 +01:00
5b1ddb145f make feature command less ugly and update feature disabled message 2024-06-30 07:41:31 +01:00
5ce5ef6898 format numbers on botstats command 2024-06-30 07:34:49 +01:00
729e0b482b update number formatter 2024-06-30 07:34:03 +01:00
5aa56c2955 rename feature command 2024-06-30 05:16:00 +01:00
ee6456e4d8 add feature toggling 2024-06-30 05:15:37 +01:00
93350f1506 remove useless config option 2024-06-30 04:16:49 +01:00
702aead53a cleanup dev mode 2024-06-30 04:13:54 +01:00
b7f2b6a3d7 fix birthday date validation 2024-06-30 03:47:34 +01:00
b66114503c fix npe 2024-06-30 03:39:38 +01:00
50391e5344 add name history tracking 2024-06-30 03:36:00 +01:00
91ecc9882c cleanup 2024-06-30 02:36:17 +01:00
06a2584e63 fix migrations 2024-06-30 01:03:10 +01:00
29affe2f12 update birthdays to have a view command and a private command to be able to hide your birthday 2024-06-30 00:35:52 +01:00
86c7afac42 many changes 2024-06-29 22:38:53 +01:00
b0949d17e6 add skip spotify command and show song when pausing and resuming the song 2024-06-29 21:36:45 +01:00
df44ae90b9 update member count command 2024-06-29 18:35:53 +01:00
4821e2a4fa add emojis to the spotify commands 2024-06-29 16:54:39 +01:00
4cb34fbb9a add duck image command 2024-06-29 13:23:12 +01:00
d824f957fe add vote command 2024-06-29 13:23:05 +01:00
d2d898a5b8 disable spotify linking (until they accept out application) 2024-06-29 13:08:13 +01:00
320eab34a3 Merge remote-tracking branch 'origin/master' 2024-06-29 12:45:35 +01:00
433dfb4693 make linking and unlinking only viewable to the user for spotify commands 2024-06-29 12:45:16 +01:00
Lee
71158fd477 Merge pull request 'Fix NPE caused by default avatars + make MemberCountCommand message consistent' (#2) from okNick/Bat:master into master
Reviewed-on: Fascinated/Bat#2
2024-06-28 20:38:26 +00:00
eb8408eb8f Fix NPE caused by default avatars + make MemberCountCommand message consistent 2024-06-28 15:32:46 -05:00
20905a7962 fix message 2024-06-28 20:05:32 +01:00
827e1bed4f cleanup spotify 2024-06-28 19:59:29 +01:00
f4d3752de7 fix image command category 2024-06-28 19:53:49 +01:00
f737b7d571 merge cat, dog, and fox commands into 1 2024-06-28 19:37:10 +01:00
dc18c9fe7a remove debug and change some command messages 2024-06-28 19:23:37 +01:00
107ec43149 actually fix it 2024-06-28 19:11:37 +01:00
e17c8a08c2 fix for sub commands since discord doesn't allow not showing them in dms 2024-06-28 19:07:01 +01:00
fa205d7ff2 allow banner and avatar command to be global 2024-06-28 19:03:02 +01:00
Lee
290c25ee43 Merge pull request 'master' (#1) from okNick/Bat:master into master
Reviewed-on: Fascinated/Bat#1
2024-06-28 17:56:18 +00:00
188 changed files with 7385 additions and 1148 deletions

49
pom.xml
View File

@ -85,6 +85,45 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
<version>7.10.0</version>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-bom</artifactId>
<version>5.2.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongock-springboot-v3</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>io.mongock</groupId>
<artifactId>mongodb-springdata-v4-driver</artifactId>
<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>
@ -120,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.

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>Fascinated/renovate-config"
]
}

View File

@ -1,7 +1,12 @@
package cc.fascinated.bat;
import cc.fascinated.bat.config.Config;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.service.EventService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.mongock.runner.springboot.EnableMongock;
import jakarta.annotation.PreDestroy;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
@ -15,7 +20,8 @@ import java.nio.file.StandardCopyOption;
import java.util.Objects;
@EnableScheduling
@SpringBootApplication
@SpringBootApplication(scanBasePackages = "cc.fascinated.bat")
@EnableMongock
@Log4j2(topic = "Bat")
public class BatApplication {
public static Gson GSON = new GsonBuilder().create();
@ -35,5 +41,14 @@ public class BatApplication {
// Start the app
SpringApplication.run(BatApplication.class, args);
log.info("APP IS RUNNING IN %s MODE!!!!!!!!!".formatted(Config.isProduction() ? "PRODUCTION" : "DEVELOPMENT"));
}
@PreDestroy
public void onShutdown() {
log.info("Shutting down...");
for (EventListener listener : EventService.LISTENERS) {
listener.onSpringShutdown();
}
}
}

View File

@ -6,6 +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";
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

@ -0,0 +1,45 @@
package cc.fascinated.bat;
import cc.fascinated.bat.service.DiscordService;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.emoji.Emoji;
/**
* @author Fascinated (fascinated7)
*/
@Log4j2
public class Emojis {
public static final Emoji SPOTIFY_EMOJI;
public static final Emoji CHECK_MARK_EMOJI;
public static final Emoji CROSS_MARK_EMOJI;
public static final Emoji SAD_FACE_EMOJI;
public static final Emoji PAUSE_EMOJI;
public static final Emoji PLAY_EMOJI;
public static final Emoji SKIP_EMOJI;
/**
* Presence Status Emojis
*/
public static final Emoji ONLINE_EMOJI;
public static final Emoji IDLE_EMOJI;
public static final Emoji DND_EMOJI;
public static final Emoji OFFLINE_EMOJI;
static {
log.info("Loading emojis...");
JDA jda = DiscordService.JDA;
SPOTIFY_EMOJI = jda.getEmojiById("1256629771975266479");
CHECK_MARK_EMOJI = jda.getEmojiById("1256633734065557676");
CROSS_MARK_EMOJI = jda.getEmojiById("1256634487429922897");
SAD_FACE_EMOJI = jda.getEmojiById("1256636078258131055");
ONLINE_EMOJI = jda.getEmojiById("1256662465668710430");
IDLE_EMOJI = jda.getEmojiById("1256662632203685991");
DND_EMOJI = jda.getEmojiById("1256662572933845032");
OFFLINE_EMOJI = jda.getEmojiById("1256662679402053662");
PAUSE_EMOJI = Emoji.fromUnicode("");
PLAY_EMOJI = Emoji.fromUnicode("");
SKIP_EMOJI = Emoji.fromUnicode("");
log.info("Loaded emojis!");
}
}

View File

@ -1,5 +1,6 @@
package cc.fascinated.bat.command;
import cc.fascinated.bat.features.Feature;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@ -38,6 +39,11 @@ public abstract class BatCommand implements BatCommandExecutor {
*/
private Category category;
/**
* The feature that the command belongs to
*/
private Feature feature;
/**
* Whether the command can only be used by the bot owner
*/

View File

@ -12,20 +12,20 @@ import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
*/
public interface BatCommandExecutor {
/**
* Executes the command using a slash command interaction.
* Executes the command using a slash command event.
*
* @param guild the bat guild the command was executed in (null if the command was executed in a DM)
* @param user the bat user that executed the command
* @param channel the channel the command was executed in
* @param member the member that executed the command
* @param interaction the slash command interaction
* @param event the slash command event
*/
default void execute(
BatGuild guild,
@NonNull BatUser user,
@NonNull MessageChannel channel,
Member member,
@NonNull SlashCommandInteraction interaction
@NonNull SlashCommandInteraction event
) {
}
}

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

@ -1,29 +0,0 @@
package cc.fascinated.bat.command.impl.server;
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.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
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 Nick (okNick)
*/
@Component
@CommandInfo(name = "membercount", description = "View the member count of the server!")
public class MemberCountCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
EmbedBuilder embed = EmbedUtils.genericEmbed().setAuthor("Member Count");
Guild discordGuild = guild.getDiscordGuild();
embed.setDescription(discordGuild.getName() + " has a total of " + discordGuild.getMembers().size() + " members.");
interaction.replyEmbeds(embed.build()).queue();
}
}

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,83 @@
package cc.fascinated.bat.common;
import java.text.DecimalFormat;
import java.util.Locale;
/**
* @author Fascinated (fascinated7)
*/
public class NumberFormatter {
/**
* The suffixes for the numbers
*/
private static final String[] SUFFIXES = new String[] { "K", "M", "B", "T", "Q", "QT", "S", "SP", "O", "N", "D", "UD", "DD", "TD" };
private static final DecimalFormat FORMAT = new DecimalFormat("###.##");
/**
* Format the provided double
*
* @param input the value to format
* @return the formatted double, in the format of xx.xx[suffix]
*/
public static String format(double input) {
if (Double.isNaN(input)) {
return "ERROR";
}
if (Double.isInfinite(input) || input == Double.MAX_VALUE) {
return "";
}
if (1000 > input) {
return FORMAT.format(input);
}
double power = (int) Math.log10(input);
int index = (int) Math.floor(power / 3) - 1;
double factor = input / Math.pow(10, 3 + index * 3);
if (index >= SUFFIXES.length) {
return "ERROR";
}
return FORMAT.format(factor) + SUFFIXES[index];
}
/**
* Format the provided double with commas
*
* @param input the value to format
* @return the formatted double, in the format of xx,xxx,xxx
*/
public static String formatCommas(double input) {
return String.format("%,.0f", input);
}
/**
* Turns a provided string into a double, for example 1M -> 1000000.00
* Accepts decimal and negative values and is not case-sensitive
*
* @param input the string to convert
* @return the value the string represents
*/
public static double fromString(String input) {
if ((input = input.trim()).isEmpty()) {
return -1D;
}
try {
double value = Double.parseDouble(input); // parse pure numbers
if (Double.isNaN(value) || Double.isInfinite(value)) {
return -1;
}
return value;
} catch (NumberFormatException ignored) {
input = input.toUpperCase(Locale.UK);
for (int i = SUFFIXES.length - 1; i > 0; i--) {
String suffix = SUFFIXES[i];
if (!input.endsWith(suffix)) {
continue;
}
String amount = input.substring(0, input.length() - suffix.length());
if (!amount.isEmpty()) {
return Double.parseDouble(amount) * Math.pow(10, 3 + i * 3);
}
}
}
return -1;
}
}

View File

@ -1,27 +0,0 @@
package cc.fascinated.bat.common;
import lombok.experimental.UtilityClass;
import java.text.NumberFormat;
/**
* @author Fascinated (fascinated7)
*/
@UtilityClass
public class NumberUtils {
/**
* Formats a number with commas.
* <p>
* Example: 1000 -> 1,000 | Example: 1000.5 -> 1,000.5
* </p>
*
* @param number the number to format
* @return the formatted number
*/
public static String formatNumberCommas(double number) {
NumberFormat format = NumberFormat.getNumberInstance();
format.setGroupingUsed(true);
format.setMaximumFractionDigits(2);
return format.format(number);
}
}

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

@ -1,26 +0,0 @@
package cc.fascinated.bat.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public abstract class Profile {
/**
* The key of the profile.
*/
private String profileKey;
public Profile() {
}
/**
* Resets the profile
*/
public abstract void reset();
}

View File

@ -1,6 +1,11 @@
package cc.fascinated.bat.common;
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;
@ -9,11 +14,12 @@ import java.util.Map;
* @author Fascinated (fascinated7)
*/
@Getter
public class ProfileHolder {
public abstract class ProfileHolder {
private static final Logger log = LoggerFactory.getLogger(ProfileHolder.class);
/**
* The profiles for the holder
*/
private Map<String, Profile> profiles;
private final Map<String, Serializable> profiles = new HashMap<>();
/**
* Gets a profile for the holder
@ -22,19 +28,25 @@ public class ProfileHolder {
* @param <T> The type of the profile
* @return The profile
*/
public <T extends Profile> T getProfile(Class<T> clazz) {
if (profiles == null) {
profiles = new HashMap<>();
}
public abstract <T extends Serializable> T getProfile(Class<T> clazz);
Profile profile = profiles.values().stream().filter(p -> p.getClass().equals(clazz)).findFirst().orElse(null);
/**
* Gets the profiles for the holder
* using the provided document
*
* @return the profiles
*/
@SneakyThrows
protected <T extends Serializable> T getProfileFromDocument(Class<T> clazz, Document document) {
Serializable profile = getProfiles().get(clazz.getSimpleName());
if (profile == null) {
try {
profile = clazz.newInstance();
profiles.put(profile.getProfileKey(), profile);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
T newProfile = clazz.cast(clazz.getDeclaredConstructors()[0].newInstance());
Document profiles = document.get("profiles", new org.bson.Document());
Document profileDocument = (Document) profiles.get(clazz.getSimpleName());
newProfile.load(profileDocument == null ? new Document() : profileDocument, BatApplication.GSON);
getProfiles().put(clazz.getSimpleName(), newProfile);
return newProfile;
}
return clazz.cast(profile);
}

View File

@ -0,0 +1,36 @@
package cc.fascinated.bat.common;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.bson.Document;
/**
* @author Fascinated (fascinated7)
*/
@Setter
@Getter
@NoArgsConstructor
public abstract class Serializable {
/**
* Load data from the provided document into this profile
*
* @param document the document to load data from
* @param gson the GSON instance to use
*/
public abstract void load(Document document, Gson gson);
/**
* Serialize this profile into a Bson document
*
* @param gson the GSON instance to use
* @return the serialized document
*/
public abstract Document serialize(Gson gson);
/**
* Resets the profile to its default state
*/
public abstract void reset();
}

View File

@ -0,0 +1,69 @@
package cc.fascinated.bat.common;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.SpotifyService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import se.michaelthelin.spotify.model_objects.specification.Track;
/**
* @author Fascinated (fascinated7)
*/
@Log4j2
public class SpotifyUtils {
/**
* Gets the URL of the track that is currently playing.
*
* @param currentlyPlaying The currently playing object.
* @return The URL of the track that is currently playing.
*/
public static String getTrackUrl(CurrentlyPlaying currentlyPlaying) {
return "https://open.spotify.com/track/" + currentlyPlaying.getItem().getId();
}
/**
* Gets the formatted time of the currently playing track
*
* @param currentlyPlaying the currently playing track
* @return the formatted time
*/
public static String getFormattedTime(@NonNull CurrentlyPlaying currentlyPlaying) {
Track track = (Track) currentlyPlaying.getItem();
int currentMinutes = currentlyPlaying.getProgress_ms() / 1000 / 60;
int currentSeconds = currentlyPlaying.getProgress_ms() / 1000 % 60;
int totalMinutes = track.getDurationMs() / 1000 / 60;
int totalSeconds = track.getDurationMs() / 1000 % 60;
return "`%02d:%02d`/`%02d:%02d`".formatted(currentMinutes, currentSeconds, totalMinutes, totalSeconds);
}
/**
* Get the next track that is playing
*
* @param user The user to get the track for
* @param oldName The name of the old track
* @return The new track
*/
public static CurrentlyPlaying getNewTrack(@NonNull SpotifyService spotifyService, @NonNull BatUser user, @NonNull String oldName) {
int checks = 0;
try {
Thread.sleep(150);
while (checks < 10) {
CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user);
Track track = (Track) currentlyPlaying.getItem();
if (track.getName().equals(oldName)) {
Thread.sleep(250);
checks++;
} else {
log.info("Found new track \"{}\" in {} check{}", track.getName(), checks, checks == 1 ? "" : "s");
return currentlyPlaying;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}

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

@ -0,0 +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

@ -27,7 +27,7 @@ public class SpotifyController {
* @return the response entity
*/
@GetMapping(value = "/callback")
public ResponseEntity<String> authorizationCallback(@RequestParam String code) {
public ResponseEntity<String> authorizationCallback(@RequestParam(required = false) String code) {
return ResponseEntity.ok(spotifyService.authorize(code));
}
}

View File

@ -2,16 +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)
@ -43,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) {
}
/**
@ -55,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
*
@ -81,4 +119,132 @@ public interface EventListener {
*/
default void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) {
}
/**
* Called when a user updates their global name
*
* @param user the user that updated their global name
* @param oldName the old global name
* @param newName the new global name
*/
default void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
}
/**
* Called when a user updates their nickname in a guild
*
* @param guild the guild that the user updated their nickname in
* @param user the user that updated their nickname
* @param oldName the old nickname
* @param newName the new nickname
*/
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
*/
default void onSpringShutdown() {
}
}

View File

@ -0,0 +1,10 @@
package cc.fascinated.bat.exception;
/**
* @author Fascinated (fascinated7)
*/
public class BatException extends Exception {
public BatException(String message) {
super(message);
}
}

View File

@ -5,7 +5,6 @@ import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}

View File

@ -0,0 +1,13 @@
package cc.fascinated.bat.exception.spotify;
import lombok.experimental.StandardException;
/**
* @author Fascinated (fascinated7)
*/
@StandardException
public class SpotifyTokenRefreshException extends RuntimeException {
public SpotifyTokenRefreshException(String message) {
super(message);
}
}

View File

@ -20,6 +20,11 @@ public abstract class Feature {
*/
private final String name;
/**
* The description of the feature
*/
public final boolean canBeDisabled;
/**
* The category of the feature
*/
@ -32,7 +37,11 @@ public abstract class Feature {
* @param command The command to register
*/
public void registerCommand(@NonNull CommandService commandService, @NonNull BatCommand command) {
command.setCategory(category);
// If the command using the default category then set the category to the feature's category
if (command.getCategory() == Category.GENERAL) {
command.setCategory(this.category);
}
command.setFeature(this);
commandService.registerCommand(command);
}
}

View File

@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
@Component
public class AfkFeature extends Feature {
public AfkFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AFK", Category.GENERAL);
super("AFK", true, Category.GENERAL);
registerCommand(commandService, context.getBean(AfkCommand.class));
}

View File

@ -4,10 +4,8 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -15,13 +13,6 @@ import org.springframework.stereotype.Component;
*/
@Component
public class AfkReturnListener implements EventListener {
private final GuildService guildService;
@Autowired
public AfkReturnListener(@NonNull GuildService guildService) {
this.guildService = guildService;
}
@Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
AfkProfile profile = guild.getProfile(AfkProfile.class);
@ -29,7 +20,6 @@ public class AfkReturnListener implements EventListener {
return;
}
profile.removeAfkUser(guild, user.getId());
guildService.saveGuild(guild);
event.getMessage().reply("Welcome back, %s! You are no longer AFK.".formatted(user.getDiscordUser().getAsMention())).queue();
}
}

View File

@ -6,14 +6,12 @@ import cc.fascinated.bat.common.MemberUtils;
import cc.fascinated.bat.features.afk.profile.AfkProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -22,26 +20,21 @@ import org.springframework.stereotype.Component;
@Component
@CommandInfo(name = "afk", description = "Sets your AFK status")
public class AfkCommand extends BatCommand {
private final GuildService guildService;
@Autowired
public AfkCommand(@NonNull GuildService guildService) {
this.guildService = guildService;
public AfkCommand() {
super.addOption(OptionType.STRING, "reason", "The reason for being AFK", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AfkProfile profile = guild.getProfile(AfkProfile.class);
String reason = null;
OptionMapping reasonOption = interaction.getOption("reason");
OptionMapping reasonOption = event.getOption("reason");
if (reasonOption != null) {
reason = reasonOption.getAsString();
}
profile.addAfkUser(guild, member.getId(), reason);
guildService.saveGuild(guild);
interaction.reply("You are now AFK: %s%s".formatted(
event.reply("You are now AFK: %s%s".formatted(
profile.getAfkReason(member.getId()),
MemberUtils.hasPermissionToEdit(guild, user) ? "" :
"\n\n*I do not have enough permissions to edit your user, and therefore cannot update your nickname*"

View File

@ -1,9 +1,12 @@
package cc.fascinated.bat.features.afk.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import com.google.gson.Gson;
import lombok.NoArgsConstructor;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import org.bson.Document;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@ -13,7 +16,8 @@ import java.util.Map;
* @author Fascinated (fascinated7)
*/
@Component
public class AfkProfile extends Profile {
@NoArgsConstructor
public class AfkProfile extends Serializable {
private static final String DEFAULT_REASON = "Away";
/**
@ -98,4 +102,21 @@ public class AfkProfile extends Profile {
public void reset() {
afkUsers = new HashMap<>();
}
@Override
public void load(Document document, Gson gson) {
afkUsers = new HashMap<>();
for (String key : document.keySet()) {
afkUsers.put(key, document.getString(key));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : afkUsers.keySet()) {
document.put(key, afkUsers.get(key));
}
return document;
}
}

View File

@ -16,7 +16,7 @@ import org.springframework.stereotype.Component;
public class AutoRoleFeature extends Feature {
@Autowired
public AutoRoleFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("AutoRole", Category.SERVER);
super("Auto Role",true, Category.SERVER);
registerCommand(commandService, context.getBean(AutoRoleCommand.class));
}

View File

@ -4,10 +4,12 @@ import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
@ -19,8 +21,20 @@ import java.util.List;
@Component
@Log4j2
public class AutoRoleListener implements EventListener {
private final FeatureService featureService;
@Autowired
public AutoRoleListener(@NonNull FeatureService featureService) {
this.featureService = featureService;
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
AutoRoleFeature autoRoleFeature = featureService.getFeature(AutoRoleFeature.class);
if (!guild.getFeatureProfile().isFeatureEnabled(autoRoleFeature)) { // Check if the feature is enabled
return;
}
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) {
return;
@ -35,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

@ -7,7 +7,6 @@ import cc.fascinated.bat.common.RoleUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
@ -24,30 +23,27 @@ import org.springframework.stereotype.Component;
@Component("autoroles:add.sub")
@CommandInfo(name = "add", description = "Adds a role to the auto roles list")
public class AddSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public AddSubCommand(@NonNull GuildService guildService) {
public AddSubCommand() {
super.addOption(OptionType.ROLE, "role", "The role to add", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
// Check if the guild has reached the maximum auto roles count
int maxRoleSlots = AutoRoleProfile.getMaxRoleSlots(guild);
if (profile.getRoleSlotsInUse() >= maxRoleSlots) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The guild can only have a maximum of %d auto roles"
.formatted(maxRoleSlots))
.build()).queue();
return;
}
OptionMapping option = interaction.getOption("role");
OptionMapping option = event.getOption("role");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a role to add")
.build()).queue();
return;
@ -56,7 +52,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the role is already in the auto roles list
if (profile.hasRole(role.getId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The role %s is already in the auto roles list".formatted(role.getAsMention()))
.build()).queue();
return;
@ -64,7 +60,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the bot has permission to give the role
if (!RoleUtils.hasPermissionToGiveRole(guild, guild.getDiscordGuild().getSelfMember(), role)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("I do not have permission to give the role %s".formatted(role.getAsMention()))
.build()).queue();
return;
@ -72,7 +68,7 @@ public class AddSubCommand extends BatSubCommand {
// Check if the role is higher than the user adding the role
if (!RoleUtils.hasPermissionToGiveRole(guild, member, role)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You cannot add a role that is higher than you")
.build()).queue();
return;
@ -80,8 +76,7 @@ public class AddSubCommand extends BatSubCommand {
// Add the role to the auto roles list
profile.addRole(role.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have added %s to the auto roles list".formatted(role.getAsMention()))
.build()).queue();
}

View File

@ -6,12 +6,10 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@ -20,20 +18,12 @@ import org.springframework.stereotype.Component;
@Component("autoroles:clear.sub")
@CommandInfo(name = "clear", description = "Clears all auto roles")
public class ClearSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ClearSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
profile.reset();
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully cleared all auto roles")
.build()).queue();
}

View File

@ -20,10 +20,10 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "list", description = "Lists all auto roles")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
if (profile.getRoles().isEmpty()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There are no auto roles set")
.build()).queue();
return;
@ -41,6 +41,6 @@ public class ListSubCommand extends BatSubCommand {
EmbedBuilder embed = EmbedUtils.genericEmbed();
embed.setAuthor("Auto Role List");
embed.setDescription(roles.toString());
interaction.replyEmbeds(embed.build()).queue();
event.replyEmbeds(embed.build()).queue();
}
}

View File

@ -6,7 +6,6 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.autorole.profile.AutoRoleProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
@ -23,20 +22,17 @@ import org.springframework.stereotype.Component;
@Component("autoroles:remove.sub")
@CommandInfo(name = "remove", description = "Removes a role from the auto roles list")
public class RemoveSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public RemoveSubCommand(GuildService guildService) {
public RemoveSubCommand() {
super.addOption(OptionType.ROLE, "role", "The role to remove", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
AutoRoleProfile profile = guild.getProfile(AutoRoleProfile.class);
OptionMapping option = interaction.getOption("role");
OptionMapping option = event.getOption("role");
if (option == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Please provide a role to remove")
.build()).queue();
return;
@ -44,15 +40,14 @@ public class RemoveSubCommand extends BatSubCommand {
Role role = option.getAsRole();
if (!profile.hasRole(role.getId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The role %s is not in the auto roles list".formatted(role.getAsMention()))
.build()).queue();
return;
}
profile.removeRole(role.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the role %s from the auto roles list".formatted(role.getAsMention()))
.build()).queue();
}

View File

@ -1,11 +1,14 @@
package cc.fascinated.bat.features.autorole.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Role;
import org.bson.Document;
import java.util.ArrayList;
import java.util.List;
@ -15,7 +18,8 @@ import java.util.List;
*/
@Setter
@Getter
public class AutoRoleProfile extends Profile {
@NoArgsConstructor
public class AutoRoleProfile extends Serializable {
private static final int DEFAULT_MAX_ROLES = 10;
private static final int PREMIUM_MAX_ROLES = 25;
@ -24,10 +28,6 @@ public class AutoRoleProfile extends Profile {
*/
private List<String> roleIds;
public AutoRoleProfile() {
super("auto-role");
}
/**
* Gets the maximum amount of roles that can be set in the guild
*
@ -35,7 +35,7 @@ public class AutoRoleProfile extends Profile {
* @return the amount of role slots
*/
public static int getMaxRoleSlots(BatGuild guild) {
if (guild.getPremium().hasPremium()) {
if (guild.getPremiumProfile().hasPremium()) {
return PREMIUM_MAX_ROLES;
}
return DEFAULT_MAX_ROLES;
@ -110,4 +110,16 @@ public class AutoRoleProfile extends Profile {
public void reset() {
roleIds.clear();
}
@Override
public void load(Document document, Gson gson) {
roleIds = document.getList("roleIds", String.class);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("roleIds", roleIds);
return document;
}
}

View File

@ -0,0 +1,45 @@
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;
import cc.fascinated.bat.features.base.commands.general.banner.BannerCommand;
import cc.fascinated.bat.features.base.commands.server.MemberCountCommand;
import cc.fascinated.bat.features.base.commands.server.PremiumCommand;
import cc.fascinated.bat.features.base.commands.server.channel.ChannelCommand;
import cc.fascinated.bat.features.base.commands.server.feature.FeatureCommand;
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 BaseFeature extends Feature {
@Autowired
public BaseFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Base", false, Category.GENERAL);
super.registerCommand(commandService, context.getBean(PremiumCommand.class));
super.registerCommand(commandService, context.getBean(PremiumAdminCommand.class));
super.registerCommand(commandService, context.getBean(MemberCountCommand.class));
super.registerCommand(commandService, context.getBean(ChannelCommand.class));
super.registerCommand(commandService, context.getBean(VoteCommand.class));
super.registerCommand(commandService, context.getBean(PingCommand.class));
super.registerCommand(commandService, context.getBean(InviteCommand.class));
super.registerCommand(commandService, context.getBean(HelpCommand.class));
super.registerCommand(commandService, context.getBean(BotStatsCommand.class));
super.registerCommand(commandService, context.getBean(BannerCommand.class));
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

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;

View File

@ -1,9 +1,10 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
@ -29,26 +30,25 @@ public class RemoveSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping guildOption = interaction.getOption("guild");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) {
interaction.reply("Please provide a guild id").queue();
event.reply("Please provide a guild id").queue();
return;
}
String guildId = guildOption.getAsString();
BatGuild batGuild = guildService.getGuild(guildId);
if (batGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
BatGuild targetGuild = guildService.getGuild(guildId);
if (targetGuild == null) {
event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return;
}
BatGuild.Premium premium = batGuild.getPremium();
PremiumProfile premium = targetGuild.getPremiumProfile();
if (!premium.hasPremium()) {
interaction.reply("The guild does not have premium").queue();
event.reply("The guild does not have premium").queue();
return;
}
premium.removePremium();
guildService.saveGuild(batGuild);
interaction.reply("The guild **%s** has had its premium removed".formatted(guild.getName())).queue();
event.reply("The guild **%s** has had its premium removed".formatted(targetGuild.getName())).queue();
}
}

View File

@ -1,9 +1,10 @@
package cc.fascinated.bat.command.impl.botadmin.premium;
package cc.fascinated.bat.features.base.commands.botadmin.premium;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
@ -23,43 +24,42 @@ public class SetSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public SetSubCommand(GuildService guildService) {
public SetSubCommand(@NonNull GuildService guildService) {
this.guildService = guildService;
super.addOption(OptionType.STRING, "guild", "The guild id to set as premium", true);
super.addOption(OptionType.BOOLEAN, "infinite", "Whether the premium length should be infinite", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping guildOption = interaction.getOption("guild");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping guildOption = event.getOption("guild");
if (guildOption == null) {
interaction.reply("Please provide a guild id").queue();
event.reply("Please provide a guild id").queue();
return;
}
String guildId = guildOption.getAsString();
OptionMapping infiniteOption = interaction.getOption("infinite");
OptionMapping infiniteOption = event.getOption("infinite");
if (infiniteOption == null) {
interaction.reply("Please provide whether the premium length should be infinite").queue();
event.reply("Please provide whether the premium length should be infinite").queue();
return;
}
boolean infinite = infiniteOption.getAsBoolean();
BatGuild batGuild = guildService.getGuild(guildId);
if (batGuild == null) {
interaction.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
BatGuild targetGuild = guildService.getGuild(guildId);
if (targetGuild == null) {
event.reply("The guild with the id %s does not exist".formatted(guildId)).queue();
return;
}
BatGuild.Premium premium = batGuild.getPremium();
PremiumProfile premium = targetGuild.getPremiumProfile();
if (!infinite) {
premium.addTime();
} else {
premium.addInfiniteTime();
}
guildService.saveGuild(batGuild);
if (!infinite) {
interaction.reply("The guild **%s** has been set as premium until <t:%s>".formatted(guild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
event.reply("The guild **%s** has been set as premium until <t:%s>".formatted(targetGuild.getName(), premium.getExpiresAt().toInstant().toEpochMilli() / 1000)).queue();
} else {
interaction.reply("The guild **%s** has been set as premium indefinitely".formatted(guild.getName())).queue();
event.reply("The guild **%s** has been set as premium indefinitely".formatted(targetGuild.getName())).queue();
}
}
}

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

@ -1,7 +1,6 @@
package cc.fascinated.bat.command.impl.fun;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.WebRequest;
@ -18,17 +17,17 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "cat", description = "Get a random cat image", category = Category.FUN, guildOnly = false)
public class CatCommand extends BatCommand {
@CommandInfo(name = "cat", description = "Get a random cat image")
public class CatSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
CatImageToken[] responseEntity = WebRequest.getAsEntity("https://api.thecatapi.com/v1/images/search", CatImageToken[].class);
if (responseEntity == null || responseEntity.length == 0) {
interaction.reply("Failed to get a cat image!").queue();
event.reply("Failed to get a cat image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random cat image!")
.setImage(responseEntity[0].getUrl())
.build()).queue();

View File

@ -1,7 +1,6 @@
package cc.fascinated.bat.command.impl.fun;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.WebRequest;
@ -18,17 +17,17 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "dog", description = "Get a random dog image", category = Category.FUN, guildOnly = false)
public class DogCommand extends BatCommand {
@CommandInfo(name = "dog", description = "Get a random dog image")
public class DogSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomImage responseEntity = WebRequest.getAsEntity("https://dog.ceo/api/breeds/image/random", RandomImage.class);
if (responseEntity == null) {
interaction.reply("Failed to get a dog image!").queue();
event.reply("Failed to get a dog image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random dog image!")
.setImage(responseEntity.getMessage())
.build()).queue();

View File

@ -0,0 +1,35 @@
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.WebRequest;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.model.token.randomd.RandomDuck;
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
@CommandInfo(name = "duck", description = "Get a random duck image")
public class DuckSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomDuck responseEntity = WebRequest.getAsEntity("https://random-d.uk/api/v2/random", RandomDuck.class);
if (responseEntity == null) {
event.reply("Failed to get a duck image!").queue();
return;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random duck image!")
.setImage(responseEntity.getUrl())
.build()).queue();
}
}

View File

@ -1,7 +1,6 @@
package cc.fascinated.bat.command.impl.fun;
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.WebRequest;
@ -18,17 +17,17 @@ import org.springframework.stereotype.Component;
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "fox", description = "Get a random fox image", category = Category.FUN, guildOnly = false)
public class FoxCommand extends BatCommand {
@CommandInfo(name = "fox", description = "Get a random fox image")
public class FoxSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
RandomFoxToken responseEntity = WebRequest.getAsEntity("https://randomfox.ca/floof/", RandomFoxToken.class);
if (responseEntity == null) {
interaction.reply("Failed to get a fox image!").queue();
event.reply("Failed to get a fox image!").queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("Here's a random fox image!")
.setImage(responseEntity.getImage())
.build()).queue();

View File

@ -0,0 +1,24 @@
package cc.fascinated.bat.features.base.commands.fun.image;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
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 = "image", description = "View a random image", guildOnly = false, category = Category.FUN)
public class ImageCommand extends BatCommand {
@Autowired
public ImageCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(CatSubCommand.class));
super.addSubCommand(context.getBean(DogSubCommand.class));
super.addSubCommand(context.getBean(FoxSubCommand.class));
super.addSubCommand(context.getBean(DuckSubCommand.class));
}
}

View File

@ -1,8 +1,10 @@
package cc.fascinated.bat.command.impl;
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;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
@ -37,19 +39,20 @@ public class BotStatsCommand extends BatCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
JDA jda = DiscordService.JDA;
interaction.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
"**Bot Statistics**\n" +
"➜ Guilds: **%s**\n".formatted(jda.getGuilds().size()) +
"➜ Users: **%s**\n".formatted(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(guildService.getGuilds().size()) +
"➜ Cached Users: **%s**".formatted(userService.getUsers().size())
event.replyEmbeds(EmbedUtils.genericEmbed().setDescription(
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

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.Consts;
import cc.fascinated.bat.command.BatCommand;
@ -45,8 +45,8 @@ public class HelpCommand extends BatCommand implements EventListener {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
interaction.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(createHomeEmbed()).addComponents(createHomeActions()).queue();
}
@Override
@ -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

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.Consts;
import cc.fascinated.bat.command.BatCommand;
@ -19,8 +19,8 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "invite", description = "Invite the bot to your server!", guildOnly = false)
public class InviteCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
interaction.replyEmbeds(EmbedUtils.genericEmbed()
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("You can invite the bot to your server by clicking [here](%s)".formatted(Consts.INVITE_URL))
.build())
.setEphemeral(true)

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl;
package cc.fascinated.bat.features.base.commands.general;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -18,9 +18,9 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "ping", description = "Gets the ping of the bot", guildOnly = false)
public class PingCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
long time = System.currentTimeMillis();
interaction.reply("Pinging...").queue(response -> {
event.reply("Pinging...").queue(response -> {
response.editOriginal("Gateway response time: `%sms`\nAPI response time `%sms`".formatted(
DiscordService.JDA.getGatewayPing(),
System.currentTimeMillis() - time

View File

@ -0,0 +1,38 @@
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;
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
@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://discordbotlist.com/bots/bat/upvote"
};
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
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) {
description.appendLine(link, true);
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription(description.build())
.build()
).queue();
}
}

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "avatar", description = "View the avatar of the guild or a user")
@CommandInfo(name = "avatar", description = "View the avatar of the guild or a user", guildOnly = false)
public class AvatarCommand extends BatCommand {
@Autowired
public AvatarCommand(@NonNull ApplicationContext context) {

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -19,18 +19,18 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "guild", description = "View the avatar of the guild")
public class GuildSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ImageProxy icon = guild.getDiscordGuild().getIcon();
if (icon == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have an avatar!".formatted(guild.getName()))
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have an avatar!".formatted(guild.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(guild.getName()), null, guild.getDiscordGuild().getIconUrl())
.setImage(icon.getUrl(4096))
.build()

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.avatar;
package cc.fascinated.bat.features.base.commands.general.avatar;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the avatar of!")
.build())
.queue();
@ -36,9 +36,9 @@ public class UserSubCommand extends BatSubCommand {
}
User target = userOption.getAsUser();
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Avatar".formatted(target.getName()), null, target.getEffectiveAvatarUrl())
.setImage(target.getAvatar().getUrl(4096))
.setImage(target.getEffectiveAvatarUrl())
.build()
).queue();
}

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "banner", description = "View the banner of the guild or a user")
@CommandInfo(name = "banner", description = "View the banner of the guild or a user", guildOnly = false)
public class BannerCommand extends BatCommand {
@Autowired
public BannerCommand(@NonNull ApplicationContext context) {

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -19,18 +19,17 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "guild", description = "View the banner of the guild")
public class GuildSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
ImageProxy banner = guild.getDiscordGuild().getBanner();
if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a banner!".formatted(guild.getName()))
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(guild.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(guild.getName()))
.setImage(banner.getUrl(512))
.build()

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.banner;
package cc.fascinated.bat.features.base.commands.general.banner;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class UserSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
if (userOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a user to view the banner of!")
.build())
.queue();
@ -38,16 +38,15 @@ public class UserSubCommand extends BatSubCommand {
User target = userOption.getAsUser();
ImageProxy banner = target.retrieveProfile().complete().getBanner();
if (banner == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a banner!".formatted(target.getName()))
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("**%s** does not have a banner!".formatted(target.getName()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Banner".formatted(target.getName()))
.setImage(banner.getUrl(512))
.build()

View File

@ -0,0 +1,68 @@
package cc.fascinated.bat.features.base.commands.server;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
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.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;
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;
import java.util.HashMap;
import java.util.Map;
/**
* @author Nick (okNick)
*/
@Component
@CommandInfo(name = "membercount", description = "View the member count of the server!")
public class MemberCountCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Guild discordGuild = guild.getDiscordGuild();
int totalMembers = 0, totalUsers = 0, totalBots = 0;
Map<OnlineStatus, Integer> memberCounts = new HashMap<>();
for (Member guildMember : discordGuild.getMembers()) {
OnlineStatus status = guildMember.getOnlineStatus();
memberCounts.put(status, memberCounts.getOrDefault(status, 0) + 1);
if (guildMember.getUser().isBot()) {
totalBots++;
} else {
totalUsers++;
}
totalMembers++;
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("""
**Member Count**
Total Members: `%s`
Total Users: `%s`
Total Bots: `%s`
\s
**Member Presence**
%s Online: `%s`
%s Idle: `%s`
%s Do Not Disturb: `%s`
%s Offline: `%s`""".formatted(
NumberFormatter.format(totalMembers),
NumberFormatter.format(totalUsers),
NumberFormatter.format(totalBots),
Emojis.ONLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.ONLINE, 0)),
Emojis.IDLE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.IDLE, 0)),
Emojis.DND_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.DO_NOT_DISTURB, 0)),
Emojis.OFFLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.OFFLINE, 0))))
.build()).queue();
}
}

View File

@ -1,10 +1,11 @@
package cc.fascinated.bat.command.impl.server;
package cc.fascinated.bat.features.base.commands.server;
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 cc.fascinated.bat.premium.PremiumProfile;
import lombok.NonNull;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
@ -20,17 +21,17 @@ import org.springframework.stereotype.Component;
@CommandInfo(name = "premium", description = "View the premium information for the guild", requiredPermissions = Permission.ADMINISTRATOR)
public class PremiumCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BatGuild.Premium premium = guild.getPremium();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
PremiumProfile premium = guild.getPremiumProfile();
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 {
embed.setDescription("The guild does not have premium");
}
interaction.replyEmbeds(embed.build()).queue();
event.replyEmbeds(embed.build()).queue();
}
}

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -26,10 +26,10 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
@ -37,7 +37,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
if (textChannel.getTopic() == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build())
.queue();
@ -45,7 +45,7 @@ public class RemoveTopicSubCommand extends BatSubCommand {
}
textChannel.getManager().setTopic(null).queue();
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully removed the topic of <#%s>".formatted(textChannel.getId()))
.build()
).queue();

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -27,19 +27,19 @@ public class SetTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
return;
}
String topic = interaction.getOption("topic").getAsString();
String topic = event.getOption("topic").getAsString();
if (topic.length() > 1024) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The topic must be 1024 characters or less!")
.build())
.queue();
@ -47,7 +47,7 @@ public class SetTopicSubCommand extends BatSubCommand {
}
textChannel.getManager().setTopic(topic).queue();
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the topic of <#%s> to: \"%s\"".formatted(textChannel.getId(), topic))
.build()
).queue();

View File

@ -1,4 +1,4 @@
package cc.fascinated.bat.command.impl.server.channel;
package cc.fascinated.bat.features.base.commands.server.channel;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
@ -25,10 +25,10 @@ public class ViewTopicSubCommand extends BatSubCommand {
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
Channel target = interaction.getOption("channel") == null ? channel : interaction.getOption("channel").getAsChannel();
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Channel target = event.getOption("channel") == null ? channel : event.getOption("channel").getAsChannel();
if (!(target instanceof TextChannel textChannel)) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> is not a text channel!".formatted(target.getId()))
.build())
.queue();
@ -37,14 +37,14 @@ public class ViewTopicSubCommand extends BatSubCommand {
String topic = textChannel.getTopic();
if (topic == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("<#%s> does not have a topic!".formatted(textChannel.getId()))
.build())
.queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The topic of <#%s> is: \"%s\"".formatted(textChannel.getId(), topic))
.build()
).queue();

View File

@ -0,0 +1,61 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("feature:disable.sub")
@CommandInfo(name = "disable", description = "Disables a feature")
public class DisableSubCommand extends BatSubCommand {
@Autowired
public DisableSubCommand() {
super.addOption(OptionType.STRING, "feature", "The feature to disable", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
FeatureProfile featureProfile = guild.getFeatureProfile();
OptionMapping featureOption = event.getOption("feature");
if (featureOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a feature to enabled")
.build()).queue();
return;
}
String featureName = featureOption.getAsString();
if (!FeatureService.INSTANCE.isFeature(featureName)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("That feature does not exist")
.build()).queue();
return;
}
Feature feature = FeatureService.INSTANCE.getFeature(featureName);
if (featureProfile.isFeatureDisabled(feature)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The feature `%s` is already disabled".formatted(feature.getName()))
.build()).queue();
return;
}
featureProfile.disableFeature(feature);
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Successfully disabled the `%s` feature".formatted(feature.getName()))
.build()).queue();
}
}

View File

@ -0,0 +1,61 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("feature:enable.sub")
@CommandInfo(name = "enable", description = "Enables a feature")
public class EnableSubCommand extends BatSubCommand {
@Autowired
public EnableSubCommand() {
super.addOption(OptionType.STRING, "feature", "The feature to enable", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
FeatureProfile featureProfile = guild.getFeatureProfile();
OptionMapping featureOption = event.getOption("feature");
if (featureOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a feature to enabled")
.build()).queue();
return;
}
String featureName = featureOption.getAsString();
if (!FeatureService.INSTANCE.isFeature(featureName)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("That feature does not exist")
.build()).queue();
return;
}
Feature feature = FeatureService.INSTANCE.getFeature(featureName);
if (featureProfile.isFeatureEnabled(feature)) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The feature `%s` is already enabled".formatted(feature.getName()))
.build()).queue();
return;
}
featureProfile.enableFeature(feature);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully enabled the `%s` feature".formatted(feature.getName()))
.build()).queue();
}
}

View File

@ -0,0 +1,24 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.Category;
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 = "feature", description = "Configure features in your guild", requiredPermissions = Permission.ADMINISTRATOR, category = Category.SERVER)
public class FeatureCommand extends BatCommand {
@Autowired
public FeatureCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(EnableSubCommand.class));
super.addSubCommand(context.getBean(DisableSubCommand.class));
super.addSubCommand(context.getBean(ListSubCommand.class));
}
}

View File

@ -0,0 +1,39 @@
package cc.fascinated.bat.features.base.commands.server.feature;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.FeatureService;
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("feature:list.sub")
@CommandInfo(name = "list", description = "Lists the features and their states")
public class ListSubCommand extends BatSubCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
StringBuilder featureStates = new StringBuilder();
for (Feature feature : FeatureService.INSTANCE.getFeaturesSorted()) {
FeatureProfile featureProfile = guild.getFeatureProfile();
featureStates.append("%s `%s`\n".formatted(
featureProfile.getFeatureState(feature).getEmoji(),
feature.getName()
));
}
event.replyEmbeds(EmbedUtils.genericEmbed()
.setTitle("Feature List")
.setDescription(featureStates.toString())
.build()
).queue();
}
}

View File

@ -0,0 +1,120 @@
package cc.fascinated.bat.features.base.profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.Feature;
import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@NoArgsConstructor
public class FeatureProfile extends Serializable {
private static final FeatureState DEFAULT_STATE = FeatureState.ENABLED;
/**
* The feature states
*/
private Map<String, FeatureState> featureStates;
/**
* Gets the feature states
*
* @return the feature states
*/
public FeatureState getFeatureState(Feature feature) {
if (feature == null) {
return DEFAULT_STATE;
}
String featureName = feature.getName().toUpperCase();
if (!this.featureStates.containsKey(featureName)) {
this.featureStates.put(featureName, DEFAULT_STATE);
}
return this.featureStates.get(featureName);
}
/**
* Gets whether the feature is enabled
*
* @return the feature state
*/
public boolean isFeatureEnabled(Feature feature) {
return this.getFeatureState(feature) == FeatureState.ENABLED;
}
/**
* Gets whether the feature is disabled
*
* @return the feature state
*/
public boolean isFeatureDisabled(Feature feature) {
return this.getFeatureState(feature) == FeatureState.DISABLED;
}
/**
* Enables the feature
*
* @param feature the feature to enable
*/
public void enableFeature(Feature feature) {
this.setFeatureState(feature, FeatureState.ENABLED);
}
/**
* Disables the feature
*
* @param feature the feature to disable
*/
public void disableFeature(Feature feature) {
this.setFeatureState(feature, FeatureState.DISABLED);
}
/**
* Sets the feature state
*
* @param feature the feature to set the state for
* @param state the state to set
*/
public void setFeatureState(Feature feature, FeatureState state) {
this.featureStates.put(feature.getName().toUpperCase(), state);
}
@Override
public void reset() {
this.featureStates = null;
}
@Override
public void load(Document document, Gson gson) {
this.featureStates = new HashMap<>();
for (String key : document.keySet()) {
this.featureStates.put(key, FeatureState.valueOf(document.getString(key)));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : this.featureStates.keySet()) {
document.put(key, this.featureStates.get(key).name());
}
return document;
}
@AllArgsConstructor @Getter
public enum FeatureState {
ENABLED(":white_check_mark:"),
DISABLED(":x:");
/**
* The emoji for the feature state
*/
private final String emoji;
}
}

View File

@ -1,13 +1,16 @@
package cc.fascinated.bat.features.birthday;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.birthday.command.BirthdayCommand;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
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 net.dv8tion.jda.api.entities.Guild;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@ -16,11 +19,11 @@ import org.springframework.stereotype.Component;
* @author Fascinated (fascinated7)
*/
@Component
public class BirthdayFeature extends Feature {
public class BirthdayFeature extends Feature implements EventListener {
private final GuildService guildService;
public BirthdayFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Birthday", Category.UTILITY);
super("Birthday", true, Category.UTILITY);
this.guildService = guildService;
registerCommand(commandService, context.getBean(BirthdayCommand.class));
@ -29,11 +32,15 @@ public class BirthdayFeature extends Feature {
/**
* Check birthdays every day at midnight
*/
@Scheduled(cron = "0 0 0 * * *")
@Scheduled(cron = "0 1 0 * * *")
private void checkBirthdays() {
for (BatGuild guild : guildService.getAllGuilds()) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
profile.checkBirthdays(guild);
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) {
continue;
}
BirthdayProfile profile = batGuild.getBirthdayProfile();
profile.checkBirthdays(batGuild);
}
}
}

View File

@ -0,0 +1,64 @@
package cc.fascinated.bat.features.birthday;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.Setter;
import org.bson.Document;
import java.util.Calendar;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
public class UserBirthday extends Serializable {
/**
* The user's birthday
*/
private Date birthday;
/**
* If the birthday should be hidden
*/
private boolean hidden;
/**
* Calculates the age of the user
*
* @return the age of the user
*/
public int calculateAge() {
Calendar birthdayCalendar = Calendar.getInstance();
birthdayCalendar.setTime(this.getBirthday());
Calendar today = Calendar.getInstance();
int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR);
// Check if the birthday hasn't occurred yet this year
if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
@Override
public void load(Document document, Gson gson) {
this.birthday = document.getDate("birthday");
this.hidden = document.getBoolean("hidden", false);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
document.put("birthday", this.birthday);
document.put("hidden", this.hidden);
return document;
}
@Override
public void reset() {
}
}

View File

@ -19,5 +19,7 @@ public class BirthdayCommand extends BatCommand {
super.addSubCommand(context.getBean(RemoveSubCommand.class));
super.addSubCommand(context.getBean(ChannelSubCommand.class));
super.addSubCommand(context.getBean(MessageSubCommand.class));
super.addSubCommand(context.getBean(ViewSubCommand.class));
super.addSubCommand(context.getBean(PrivateSubCommand.class));
}
}

View File

@ -7,7 +7,6 @@ import cc.fascinated.bat.common.TextChannelUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
@ -26,26 +25,23 @@ import org.springframework.stereotype.Component;
@Component("birthday:channel.sub")
@CommandInfo(name = "channel", description = "Sets the birthday notification channel", requiredPermissions = Permission.MANAGE_SERVER)
public class ChannelSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public ChannelSubCommand(GuildService guildService) {
public ChannelSubCommand() {
super.addOption(OptionType.CHANNEL, "channel", "The channel birthdays will be sent in", false);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
OptionMapping option = interaction.getOption("channel");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping option = event.getOption("channel");
if (option == null) {
if (!TextChannelUtils.isValidChannel(profile.getChannelId())) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("There is no channel set for birthday notifications. Please provide a channel to set the birthday channel to")
.build()).queue();
return;
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
event.replyEmbeds(EmbedUtils.genericEmbed()
.setDescription("The current birthday channel is %s".formatted(TextChannelUtils.getChannelMention(profile.getChannelId())))
.build()).queue();
return;
@ -53,16 +49,14 @@ public class ChannelSubCommand extends BatSubCommand {
GuildChannelUnion targetChannel = option.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid channel type, please provide a text channel")
.build()).queue();
return;
}
profile.setChannelId(targetChannel.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the birthday channel to %s".formatted(targetChannel.asTextChannel().getAsMention()))
.build()).queue();
}

View File

@ -6,7 +6,6 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
@ -17,10 +16,6 @@ import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
@ -28,22 +23,17 @@ import java.util.Date;
@Component("birthday:message.sub")
@CommandInfo(name = "message", description = "Changes the message that is sent when it is a user's birthday", requiredPermissions = Permission.MANAGE_SERVER)
public class MessageSubCommand extends BatSubCommand {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy");
private final GuildService guildService;
@Autowired
public MessageSubCommand(GuildService guildService) {
public MessageSubCommand() {
super.addOption(OptionType.STRING, "message", "The message that is sent. (Placeholders: {user}, {age})", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
OptionMapping messageOption = interaction.getOption("message");
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping messageOption = event.getOption("message");
if (messageOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a message")
.build()).queue();
return;
@ -51,37 +41,21 @@ public class MessageSubCommand extends BatSubCommand {
String message = messageOption.getAsString();
if (message.length() > 2000) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The message must be less than 2000 characters")
.build()).queue();
return;
}
if (!message.contains("{user}") || !message.contains("{age}")) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The message must contain the placeholders {user} and {age}")
.build()).queue();
return;
}
profile.setMessage(message);
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have updated the birthday message!\n\n**Message:** %s".formatted(profile.getBirthdayMessage(user.getDiscordUser())))
.build()).queue();
}
/**
* Parses a birthday from the string
*
* @param birthday the date to parse
* @return the birthday
*/
private Date parseBirthday(String birthday) {
try {
return FORMATTER.parse(birthday);
} catch (ParseException ignored) {
}
return null;
}
}

View File

@ -0,0 +1,56 @@
package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("birthday:private.sub")
@CommandInfo(name = "private", description = "Changes whether your birthday is private or not")
public class PrivateSubCommand extends BatSubCommand {
@Autowired
public PrivateSubCommand() {
super.addOption(OptionType.BOOLEAN, "enabled", "Whether your birthday is private or not", true);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
OptionMapping enabledOption = event.getOption("enabled");
if (enabledOption == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide whether your birthday is private or not")
.build()).queue();
return;
}
boolean enabled = enabledOption.getAsBoolean();
UserBirthday birthday = profile.getBirthday(user.getId());
if (birthday == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You have not set your birthday yet")
.build()).queue();
return;
}
birthday.setHidden(enabled);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Your birthday privacy settings have been updated\n\n**Private:** " + (enabled ? "Yes" : "No"))
.build()).queue();
}
}

View File

@ -6,12 +6,10 @@ import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -21,21 +19,12 @@ import org.springframework.stereotype.Component;
@Component("birthday:remove.sub")
@CommandInfo(name = "remove", description = "Remove your birthday from this guild")
public class RemoveSubCommand extends BatSubCommand {
private final GuildService guildService;
@Autowired
public RemoveSubCommand(GuildService guildService) {
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
profile.removeBirthday(user.getId());
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Your birthday has been removed from this guild")
.build()).queue();
}

View File

@ -3,10 +3,10 @@ package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
@ -28,28 +28,25 @@ import java.util.Date;
@CommandInfo(name = "set", description = "Add your birthday to this guild")
public class SetSubCommand extends BatSubCommand {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd/MM/yyyy");
private final GuildService guildService;
@Autowired
public SetSubCommand(GuildService guildService) {
public SetSubCommand() {
super.addOption(OptionType.STRING, "birthday", "Your birthday (format: DAY/MONTH/YEAR - 01/05/2004)", true);
this.guildService = guildService;
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
BirthdayProfile profile = guild.getProfile(BirthdayProfile.class);
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
if (!profile.hasChannelSetup()) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Birthdays have not been enabled in this guild. Please ask an administrator to enable them.")
.build()).queue();
return;
}
OptionMapping birthdayOption = interaction.getOption("birthday");
OptionMapping birthdayOption = event.getOption("birthday");
if (birthdayOption == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a birthday")
.build()).queue();
return;
@ -57,16 +54,20 @@ public class SetSubCommand extends BatSubCommand {
String birthdayString = birthdayOption.getAsString();
Date birthday = parseBirthday(birthdayString);
if (birthday == null) {
interaction.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Invalid birthday format. Please use the format: DAY/MONTH/YEAR - 01/05/2004")
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("""
Invalid birthday format. Please use the following format:
DAY/MONTH/YEAR - 01/05/2004
""")
.build()).queue();
return;
}
profile.addBirthday(member.getId(), birthday);
guildService.saveGuild(guild);
interaction.replyEmbeds(EmbedUtils.successEmbed()
UserBirthday userBirthday = new UserBirthday();
userBirthday.setBirthday(birthday);
userBirthday.setHidden(false);
profile.addBirthday(member.getId(), userBirthday);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("You have updated your birthday!")
.build()).queue();
}
@ -79,9 +80,16 @@ public class SetSubCommand extends BatSubCommand {
*/
private Date parseBirthday(String birthday) {
try {
return FORMATTER.parse(birthday);
Date date = FORMATTER.parse(birthday);
if (date.after(new Date())) {
return null;
}
if (date.toInstant().toEpochMilli() < 0) {
return null;
}
return date;
} catch (ParseException ignored) {
return null;
}
return null;
}
}

View File

@ -0,0 +1,74 @@
package cc.fascinated.bat.features.birthday.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("birthday:view.sub")
@CommandInfo(name = "view", description = "Add your birthday to this guild")
public class ViewSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public ViewSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the birthday of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
BirthdayProfile profile = guild.getBirthdayProfile();
if (!profile.hasChannelSetup()) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("Birthdays have not been enabled in this guild. Please ask an administrator to enable them.")
.build()).queue();
return;
}
OptionMapping birthdayOption = event.getOption("user");
BatUser target = birthdayOption == null ? user : userService.getUser(birthdayOption.getAsUser().getId());
if (target == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You must provide a valid user")
.build()).queue();
return;
}
UserBirthday birthday = profile.getBirthday(target.getId());
if (birthday == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s does not have a birthday set".formatted(target.getDiscordUser().getAsMention()))
.build()).queue();
return;
}
if (birthday.isHidden() && !user.getId().equals(target.getId())) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s has their birthday set to private".formatted(target.getDiscordUser().getAsMention()))
.build()).queue();
return;
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s was born on <t:%s:D> they are `%s` years old!".formatted(
target.getDiscordUser().getAsMention(), birthday.getBirthday().toInstant().toEpochMilli()/1000,
birthday.calculateAge()
))
.build()).queue();
}
}

View File

@ -1,28 +1,36 @@
package cc.fascinated.bat.features.birthday.profile;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.model.BatGuild;
import com.google.gson.Gson;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.*;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Getter
@Setter
public class BirthdayProfile extends Profile {
@NoArgsConstructor
public class BirthdayProfile extends Serializable {
private static final String DEFAULT_MESSAGE = "Happy Birthday {user} :tada: :birthday: You are now {age} years old!";
/**
* The list of birthdays that are being tracked
*/
private Map<String, Date> birthdays;
private Map<String, UserBirthday> birthdays;
/**
* The channel ID of the birthday feed
@ -34,17 +42,13 @@ public class BirthdayProfile extends Profile {
*/
private String message = DEFAULT_MESSAGE;
public BirthdayProfile() {
super("birthday");
}
/**
* Adds a birthday to be tracked
*
* @param userId the id of the user to track
* @param birthday the birthday of the user
*/
public void addBirthday(String userId, Date birthday) {
public void addBirthday(String userId, UserBirthday birthday) {
if (birthdays == null) {
birthdays = new HashMap<>();
}
@ -69,7 +73,7 @@ public class BirthdayProfile extends Profile {
* @param userId the id of the user
* @return the birthday of the user
*/
public Date getBirthday(String userId) {
public UserBirthday getBirthday(String userId) {
if (birthdays == null) {
birthdays = new HashMap<>();
}
@ -85,33 +89,6 @@ public class BirthdayProfile extends Profile {
return channelId != null;
}
/**
* Calculates the age of a user
*
* @param userId the id of the user
* @return the age of the user
*/
public int calculateAge(String userId) {
Date birthday = getBirthday(userId);
if (birthday == null) {
return 0; // or throw an exception
}
Calendar birthdayCalendar = Calendar.getInstance();
birthdayCalendar.setTime(birthday);
Calendar today = Calendar.getInstance();
int age = today.get(Calendar.YEAR) - birthdayCalendar.get(Calendar.YEAR);
// Check if the birthday hasn't occurred yet this year
if (today.get(Calendar.DAY_OF_YEAR) < birthdayCalendar.get(Calendar.DAY_OF_YEAR)) {
age--;
}
return age;
}
/**
* Validates the profiles configuration
*
@ -122,33 +99,10 @@ public class BirthdayProfile extends Profile {
if (birthdays == null) {
birthdays = new HashMap<>();
}
List<String> toRemove = new ArrayList<>();
Guild discordGuild = guild.getDiscordGuild();
for (Map.Entry<String, Date> entry : birthdays.entrySet()) {
String userId = entry.getKey();
Date birthday = entry.getValue();
if (userId == null || birthday == null) { // this should never happen
continue;
}
// Check if the user is still in the guild, if not remove them
Member member = discordGuild.getMemberById(userId);
if (member == null) {
toRemove.add(userId);
}
}
for (String userId : toRemove) {
birthdays.remove(userId);
}
if (channelId == null) {
return false;
}
if (discordGuild.getTextChannelById(channelId) == null) {
if (guild.getDiscordGuild().getTextChannelById(channelId) == null) {
channelId = null;
return false;
}
@ -170,13 +124,10 @@ public class BirthdayProfile extends Profile {
int todayDay = today.get(Calendar.DAY_OF_MONTH);
int todayMonth = today.get(Calendar.MONTH); // Note: January is 0
Iterator<Map.Entry<String, Date>> iterator = birthdays.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Date> entry = iterator.next();
Date birthday = entry.getValue();
for (Map.Entry<String, UserBirthday> entry : birthdays.entrySet()) {
Date birthday = entry.getValue().getBirthday();
if (birthday == null) {
iterator.remove();
continue;
}
@ -223,7 +174,7 @@ public class BirthdayProfile extends Profile {
public String getBirthdayMessage(User user) {
return message
.replace("{user}", user.getAsMention())
.replace("{age}", String.valueOf(calculateAge(user.getId())));
.replace("{age}", String.valueOf(birthdays.get(user.getId()).calculateAge()));
}
@Override
@ -231,4 +182,27 @@ public class BirthdayProfile extends Profile {
birthdays.clear();
channelId = null;
}
@Override
public void load(Document document, Gson gson) {
birthdays = new HashMap<>();
for (String key : document.keySet()) {
UserBirthday userBirthday = new UserBirthday();
userBirthday.load((Document) document.get(key), gson);
birthdays.put(key, userBirthday);
}
channelId = document.getString("channelId");
message = (String) document.getOrDefault("message", DEFAULT_MESSAGE);
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (String key : birthdays.keySet()) {
document.put(key, birthdays.get(key).serialize(gson));
}
document.put("channelId", channelId);
document.put("message", message);
return document;
}
}

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

Some files were not shown because too many files have changed in this diff Show More