impl counter feature
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 2m13s

This commit is contained in:
Lee 2024-07-05 19:07:32 +01:00
parent f6a23e4888
commit 4e866895a0
13 changed files with 618 additions and 20 deletions

View File

@ -0,0 +1,20 @@
package cc.fascinated.bat.common;
/**
* @author Fascinated (fascinated7)
*/
public class NumberUtils {
/**
* Parses a string to an integer
*
* @param input the string to parse
* @return the parsed integer, or -1 if invalid
*/
public static int parseInt(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException exception) {
return -1;
}
}
}

View File

@ -28,11 +28,7 @@ public class MemberCountCommand extends BatCommand {
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
Guild discordGuild = guild.getDiscordGuild();
int totalMembers = 0, totalUsers = 0, totalBots = 0;
Map<OnlineStatus, Integer> memberCounts = new HashMap<>();
for (Member guildMember : discordGuild.getMembers()) {
OnlineStatus status = guildMember.getOnlineStatus();
memberCounts.put(status, memberCounts.getOrDefault(status, 0) + 1);
if (guildMember.getUser().isBot()) {
totalBots++;
} else {
@ -46,23 +42,10 @@ public class MemberCountCommand extends BatCommand {
Total Members: `%s`
Total Users: `%s`
Total Bots: `%s`
\s
**Member Presence**
%s Online: `%s`
%s Idle: `%s`
%s Do Not Disturb: `%s`
%s Offline: `%s`""".formatted(
""".formatted(
NumberFormatter.format(totalMembers),
NumberFormatter.format(totalUsers),
NumberFormatter.format(totalBots),
Emojis.ONLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.ONLINE, 0)),
Emojis.IDLE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.IDLE, 0)),
Emojis.DND_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.DO_NOT_DISTURB, 0)),
Emojis.OFFLINE_EMOJI,
NumberFormatter.format(memberCounts.getOrDefault(OnlineStatus.OFFLINE, 0))))
.build()).queue();
NumberFormatter.format(totalBots)
)).build()).queue();
}
}

View File

@ -0,0 +1,81 @@
package cc.fascinated.bat.features.counter;
import cc.fascinated.bat.common.ChannelUtils;
import cc.fascinated.bat.common.NumberUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class CounterChannel {
/**
* The channel id of this counter channel
*/
private final String channelId;
/**
* The current count of this counter channel
*/
private int currentCount;
/**
* Whether the counter should reset when a user
* inputs the wrong number or a non number
*/
private boolean breakable;
/**
* Increment the counter
*
* @param input the input to increment the counter with
* @return the return type of the increment
*/
public CounterResult incrementChannel(String input) {
int number = NumberUtils.parseInt(input);
// The number is invalid
if (number == -1 || number != this.nextNumber()) {
// The counter was broken
if (this.isBreakable()) {
this.reset();
return CounterResult.BROKEN;
}
return CounterResult.INVALID_NUMBER;
}
this.currentCount++;
return CounterResult.SUCCESS;
}
/**
* Gets the next number for this counter
*/
public int nextNumber() {
return currentCount + 1;
}
/**
* Resets the current count
* <p>
* This usually happens if the counter is broken
* </p>
*/
public void reset() {
currentCount = 0;
}
/**
* Gets the channel
*
* @return the channel
*/
public TextChannel getChannel() {
return ChannelUtils.getTextChannel(channelId);
}
}

View File

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

View File

@ -0,0 +1,89 @@
package cc.fascinated.bat.features.counter;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class CounterProfile extends Serializable {
/**
* The counter channels in this guild
*/
private final List<CounterChannel> counters = new ArrayList<>();
/**
* Gets the counter channel by the channel id
*
* @param channelId the id of the channel
* @return the counter channel
*/
public CounterChannel getCounterChannel(String channelId) {
for (CounterChannel counter : counters) {
if (counter.getChannelId().equals(channelId)) {
return counter;
}
}
return null;
}
/**
* Removes the channel from the counter channels
*
* @param channel the channel to remove
*/
public void removeChannel(TextChannel channel) {
CounterChannel counterChannel = getCounterChannel(channel.getId());
if (counterChannel != null) {
counters.remove(counterChannel);
}
}
/**
* Sets up the channel as a counter channel
*
* @param channel the channel to set as a counter channel
* @param breakable whether the counter should reset when a user inputs the wrong number or a non number
*/
public void setupChannel(TextChannel channel, boolean breakable) {
counters.add(new CounterChannel(channel.getId(), 0, breakable));
}
@Override
public void load(Document document, Gson gson) {
for (Document counterDocument : document.getList("counters", Document.class, new ArrayList<>())) {
counters.add(new CounterChannel(
counterDocument.getString("channelId"),
counterDocument.getInteger("currentCount"),
counterDocument.getBoolean("breakable")
));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
List<Document> counterDocuments = new ArrayList<>();
for (CounterChannel counter : counters) {
Document counterDocument = new Document();
counterDocument.append("channelId", counter.getChannelId());
counterDocument.append("currentCount", counter.getCurrentCount());
counterDocument.append("breakable", counter.isBreakable());
counterDocuments.add(counterDocument);
}
document.append("counters", counterDocuments);
return document;
}
@Override
public void reset() {
counters.clear();
}
}

View File

@ -0,0 +1,10 @@
package cc.fascinated.bat.features.counter;
/**
* @author Fascinated (fascinated7)
*/
public enum CounterResult {
SUCCESS,
BROKEN,
INVALID_NUMBER,
}

View File

@ -0,0 +1,53 @@
package cc.fascinated.bat.features.counter;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.base.profile.FeatureProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class CountingListener implements EventListener {
private final CounterFeature counterFeature;
@Autowired
public CountingListener(@NonNull CounterFeature counterFeature) {
this.counterFeature = counterFeature;
}
@Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
FeatureProfile featureProfile = guild.getFeatureProfile();
if (featureProfile.isFeatureDisabled(counterFeature)) {
return;
}
CounterProfile counterProfile = guild.getCounterProfile();
if (counterProfile == null) {
return;
}
CounterChannel counterChannel = counterProfile.getCounterChannel(event.getChannel().getId());
if (counterChannel == null) {
return;
}
CounterResult result = counterChannel.incrementChannel(event.getMessage().getContentRaw());
if (result == null) {
return;
}
switch (result) {
case SUCCESS -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
case INVALID_NUMBER -> event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
case BROKEN -> {
event.getMessage().addReaction(Emoji.fromUnicode("")).queue();
event.getMessage().reply("%s, you have broken the counter!".formatted(event.getAuthor().getAsMention())).queue();
}
}
}
}

View File

@ -0,0 +1,26 @@
package cc.fascinated.bat.features.counter.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 = "counter", description = "View or configure the counter settings", requiredPermissions = Permission.MANAGE_CHANNEL)
public class CounterCommand extends BatCommand {
@Autowired
public CounterCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(SetupSubCommand.class),
context.getBean(SetSubCommand.class),
context.getBean(RemoveSubCommand.class),
context.getBean(SetBreakingSubCommand.class)
);
}
}

View File

@ -0,0 +1,77 @@
package cc.fascinated.bat.features.counter.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.features.counter.CounterChannel;
import cc.fascinated.bat.features.counter.CounterProfile;
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.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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.commands.build.OptionData;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("counter.remove:sub")
@CommandInfo(name = "remove", description = "Remove a counter channel")
public class RemoveSubCommand extends BatCommand {
public RemoveSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel to set the count in", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
if (channelOption == null) {
return;
}
GuildChannelUnion targetChannel = channelOption.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel must be a text channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
TextChannel textChannel = targetChannel.asTextChannel();
CounterProfile profile = guild.getCounterProfile();
if (profile == null) {
return;
}
// Check if the channel is a counter channel
CounterChannel counterChannel = profile.getCounterChannel(textChannel.getId());
if (counterChannel == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel %s is not a counter channel".formatted(
Emojis.CROSS_MARK_EMOJI,
textChannel.getAsMention()
))
.build()).queue();
return;
}
profile.removeChannel(textChannel);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("""
%s Successfully removed the counter channel %s
The count was `%s`
""".formatted(
Emojis.CHECK_MARK_EMOJI,
textChannel.getAsMention(),
NumberFormatter.formatCommas(counterChannel.getCurrentCount())
))
.build()).queue();
}
}

View File

@ -0,0 +1,80 @@
package cc.fascinated.bat.features.counter.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.counter.CounterChannel;
import cc.fascinated.bat.features.counter.CounterProfile;
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.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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.commands.build.OptionData;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("counter.setbreaking:sub")
@CommandInfo(name = "setbreaking", description = "Updates if the counter should break")
public class SetBreakingSubCommand extends BatCommand {
public SetBreakingSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel to set the count in", true));
super.addOptions(new OptionData(OptionType.BOOLEAN, "breakable", "Should the channel break on a invalid or wrong number", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
OptionMapping breakableOption = event.getOption("breakable");
if (channelOption == null || breakableOption == null) {
return;
}
GuildChannelUnion targetChannel = channelOption.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel must be a text channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
TextChannel textChannel = targetChannel.asTextChannel();
boolean breakable = breakableOption.getAsBoolean();
CounterProfile profile = guild.getCounterProfile();
if (profile == null) {
return;
}
// Check if the channel is a counter channel
CounterChannel counterChannel = profile.getCounterChannel(textChannel.getId());
if (counterChannel == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel %s is not a counter channel".formatted(
Emojis.CROSS_MARK_EMOJI,
textChannel.getAsMention()
))
.build()).queue();
return;
}
counterChannel.setBreakable(breakable);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s The counter in %s will %s break on an invalid or wrong number".formatted(
Emojis.CHECK_MARK_EMOJI,
textChannel.getAsMention(),
breakable ? "now" : "no longer"
))
.build()).queue();
counterChannel.getChannel().sendMessage("The counter will %s break on an invalid or wrong number".formatted(
breakable ? "now" : "no longer"
)).queue();
}
}

View File

@ -0,0 +1,80 @@
package cc.fascinated.bat.features.counter.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.counter.CounterChannel;
import cc.fascinated.bat.features.counter.CounterProfile;
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.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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.commands.build.OptionData;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("counter.set:sub")
@CommandInfo(name = "set", description = "Set the current count in a channel")
public class SetSubCommand extends BatCommand {
public SetSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel to set the count in", true));
super.addOptions(new OptionData(OptionType.INTEGER, "count", "The count to set", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
OptionMapping countOption = event.getOption("count");
if (channelOption == null || countOption == null) {
return;
}
GuildChannelUnion targetChannel = channelOption.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel must be a text channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
TextChannel textChannel = targetChannel.asTextChannel();
int count = countOption.getAsInt();
CounterProfile profile = guild.getCounterProfile();
if (profile == null) {
return;
}
// Check if the channel is a counter channel
CounterChannel counterChannel = profile.getCounterChannel(textChannel.getId());
if (counterChannel == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel %s is not a counter channel".formatted(
Emojis.CROSS_MARK_EMOJI,
textChannel.getAsMention()
))
.build()).queue();
return;
}
counterChannel.setCurrentCount(count);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s Successfully set the count in %s to %d".formatted(
Emojis.CHECK_MARK_EMOJI,
textChannel.getAsMention(),
count
))
.build()).queue();
counterChannel.getChannel().sendMessage("The count has been set to `%s`".formatted(
count
)).queue();
}
}

View File

@ -0,0 +1,67 @@
package cc.fascinated.bat.features.counter.command;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.counter.CounterProfile;
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.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
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.commands.build.OptionData;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("counter.setup:sub")
@CommandInfo(name = "setup", description = "Setup a counter for a channel")
public class SetupSubCommand extends BatCommand {
public SetupSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The channel to setup the counter in", true));
super.addOptions(new OptionData(OptionType.BOOLEAN, "breakable", "Should the channel break on a invalid or wrong number", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
OptionMapping breakableOption = event.getOption("breakable");
if (channelOption == null || breakableOption == null) {
return;
}
GuildChannelUnion targetChannel = channelOption.getAsChannel();
if (targetChannel.getType() != ChannelType.TEXT) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel must be a text channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
TextChannel textChannel = targetChannel.asTextChannel();
CounterProfile profile = guild.getCounterProfile();
if (profile.getCounterChannel(textChannel.getId()) != null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s This channel is already a counter channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
boolean breakable = breakableOption.getAsBoolean();
profile.setupChannel(textChannel, breakable);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s Successfully setup the counter in %s".formatted(
Emojis.CHECK_MARK_EMOJI,
textChannel.getAsMention()
))
.build()).queue();
}
}

View File

@ -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.counter.CounterProfile;
import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile;
@ -150,6 +151,15 @@ public class BatGuild extends ProfileHolder {
return getProfile(WelcomerProfile.class);
}
/**
* Gets the counter profile
*
* @return the counter profile
*/
public CounterProfile getCounterProfile() {
return getProfile(CounterProfile.class);
}
/**
* Saves the user
*/