Compare commits

..

33 Commits

Author SHA1 Message Date
f07e30d843 Add support for TMDB movie + series lookup 2024-07-03 18:41:58 -05: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
54 changed files with 2461 additions and 49 deletions

21
pom.xml

@ -108,6 +108,22 @@
<version>5.2.4</version>
</dependency>
<!-- Redis for caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Libraries -->
<dependency>
@ -143,6 +159,11 @@
<artifactId>spotify-web-api-java</artifactId>
<version>8.4.0</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

@ -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.

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

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

@ -18,6 +18,9 @@ public enum Category {
SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false),
UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false),
MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false),
MOVIES_TV(Emoji.fromFormatted("U+1F37F"), "Movies & TV", false),
SNIPE(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);

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

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

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

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

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

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

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

@ -2,18 +2,34 @@ 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.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateAvatarEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateNameEvent;
import java.util.List;
/**
* @author Fascinated (fascinated7)
@ -45,7 +61,7 @@ public interface EventListener {
* @param guild the guild the user left
* @param user the user that left the guild
*/
default void onGuildMemberLeave(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberRemoveEvent event) {
default void onGuildMemberLeave(@NonNull BatGuild guild, BatUser user, @NonNull GuildMemberRemoveEvent event) {
}
/**
@ -57,6 +73,25 @@ public interface EventListener {
default void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
}
/**
* Called when a user updates a message
*
* @param guild the guild that the message was updated in
* @param user the user that updated the message
*/
default void onGuildMessageEdit(@NonNull BatGuild guild, @NonNull BatUser user, DiscordMessage oldMessage,
@NonNull DiscordMessage newMessage, @NonNull MessageUpdateEvent event) {
}
/**
* Called when a user deletes a message
*
* @param guild the guild that the message was deleted in
* @param user the user that deleted the message
*/
default void onGuildMessageDelete(@NonNull BatGuild guild, BatUser user, DiscordMessage message, @NonNull MessageDeleteEvent event) {
}
/**
* Called when a user selects a string
*
@ -105,6 +140,98 @@ public interface EventListener {
default void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
}
/**
* Called when a user gets roles added to them
*
* @param guild the guild that the user added the role in
* @param user the user that added the role
* @param rolesAdded the roles that were added
*/
default void onGuildMemberRoleAdd(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleAddEvent event) {
}
/**
* Called when a user gets roles removed from them
*
* @param guild the guild that the user removed the role in
* @param user the user that removed the role
* @param rolesAdded the roles that were removed
*/
default void onGuildMemberRoleRemove(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull List<Role> rolesAdded, @NonNull GuildMemberRoleRemoveEvent event) {
}
/**
* Called when a channel is created
*
* @param guild the guild that the channel was created in
*/
default void onChannelCreate(@NonNull BatGuild guild, @NonNull ChannelCreateEvent event) {
}
/**
* Called when a channel is deleted
*
* @param guild the guild that the channel was deleted in
*/
default void onChannelDelete(@NonNull BatGuild guild, @NonNull ChannelDeleteEvent event) {
}
/**
* Called when a user is banned from a guild
*
* @param guild the guild that the user was banned from
* @param user the user that was banned
*/
default void onGuildMemberBan(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildBanEvent event) {
}
/**
* Called when a user is unbanned from a guild
*
* @param guild the guild that the user was unbanned from
* @param user the user that was unbanned
*/
default void onGuildMemberUnban(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildUnbanEvent event) {
}
/**
* Called when a user gets timed out in a guild (gets muted)
*
* @param guild the guild that the user timed out in
* @param user the user that timed out
*/
default void onGuildMemberTimeout(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberUpdateTimeOutEvent event) {
}
/**
* Called when a channels state is updated
*
* @param guild the guild that the channel was updated in
* @param event the event that was fired
*/
default void onGenericChannelUpdate(@NonNull BatGuild guild, @NonNull GenericChannelUpdateEvent<?> event) {
}
/**
* Called when a user updates their username
*
* @param user the user that updated their name
* @param oldName the old username
* @param newName the new username
*/
default void onUserUpdateName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateNameEvent event) {
}
/**
* Called when a user updates their avatar
*
* @param user the user that updated their avatar
* @param oldAvatarUrl the old avatar url
* @param newAvatarUrl the new avatar url
*/
default void onUserUpdateAvatar(@NonNull BatUser user, String oldAvatarUrl, String newAvatarUrl, @NonNull UserUpdateAvatarEvent event) {
}
/**
* Called when Spring is shutting down
*/

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

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

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

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

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

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

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

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

@ -0,0 +1,80 @@
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");
/**
* 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;
}
}

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

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

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

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

@ -0,0 +1,52 @@
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 lombok.NonNull;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class ChannelListener implements EventListener {
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) {
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) {
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());
}
}

@ -0,0 +1,209 @@
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.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 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;
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("Account Age: <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.getDiscordUser().isBot()) return;
logFeature.sendLog(guild, LogType.MEMBER_LEAVE, EmbedUtils.errorEmbed()
.setDescription(new EmbedDescriptionBuilder("Member Left")
.appendLine("Member: %s".formatted(user.getDiscordUser().getAsMention()), true)
.appendLine("Username: %s".formatted(user.getDiscordUser().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;
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;
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
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;
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
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;
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
if (batGuild == null) continue;
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: [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;
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;
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;
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;
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;
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());
}
}

@ -0,0 +1,58 @@
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 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
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.getDiscordUser().isBot() || message.getAuthor().isBot()) return;
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;
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());
}
}

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

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

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

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

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

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

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

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

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

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

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

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

@ -5,6 +5,7 @@ import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.DiscordService;
@ -109,10 +110,20 @@ public class BatGuild extends ProfileHolder {
return getProfile(BirthdayProfile.class);
}
/**
* Gets the log profile
*
* @return the log profile
*/
public LogProfile getLogProfile() {
return getProfile(LogProfile.class);
}
/**
* Saves the user
*/
public void save() {
org.bson.Document document = new org.bson.Document();
document.put("_id", id);
document.put("createdAt", createdAt);
@ -124,7 +135,7 @@ public class BatGuild extends ProfileHolder {
MongoService.INSTANCE.getGuildsCollection().replaceOne(
new org.bson.Document("_id", id),
this.getDocument(),
document,
new ReplaceOptions().upsert(true)
);
}

@ -103,6 +103,7 @@ public class BatUser extends ProfileHolder {
* Saves the user
*/
public void save() {
org.bson.Document document = new org.bson.Document();
document.put("_id", id);
document.put("createdAt", createdAt);
@ -114,7 +115,7 @@ public class BatUser extends ProfileHolder {
MongoService.INSTANCE.getUsersCollection().replaceOne(
new org.bson.Document("_id", id),
this.getDocument(),
document,
new ReplaceOptions().upsert(true)
);
}

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

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

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

@ -75,7 +75,7 @@ public class CommandService extends ListenerAdapter {
JDA jda = DiscordService.JDA;
long before = System.currentTimeMillis();
Guild adminGuild = jda.getGuildById(Consts.ADMIN_GUILD);
Guild adminGuild = jda.getGuildById(Config.INSTANCE.getAdminGuild());
if (!Config.isProduction()) {
if (adminGuild == null) {
log.error("Unable to find the admin guild to register commands");

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

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

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

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

@ -2,11 +2,13 @@ package cc.fascinated.bat.service;
import cc.fascinated.bat.common.TimerUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import com.mongodb.client.model.Filters;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
@ -83,6 +85,11 @@ public class UserService implements EventListener {
log.info("Saved all users.");
}
@Override
public void onGuildMemberJoin(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull GuildMemberJoinEvent event) {
user.setGlobalName(event.getUser().getName()); // Ensure the user's name is up-to-date
}
@Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
log.info("User \"{}\" changed their name from \"{}\" to \"{}\"", user.getName(), oldName, newName);

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

30
terms-of-service.txt Normal file

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