add stat channels feature
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m25s

This commit is contained in:
Lee 2024-07-07 06:50:25 +01:00
parent 6d98977198
commit 87a56700ec
9 changed files with 522 additions and 0 deletions

@ -0,0 +1,25 @@
package cc.fascinated.bat.features.statschannel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class StatsChannel {
/**
* The display of the stats channel
*/
private final String display;
/**
* The last value of the stats channel, used to compare
* the new value to the old value, so we don't have to
* update the channel every time
*/
private String lastValue;
}

@ -0,0 +1,67 @@
package cc.fascinated.bat.features.statschannel;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.statschannel.command.StatsChannelCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "Stats Channel Feature")
public class StatsChannelFeature extends Feature {
private final GuildService guildService;
@Autowired
public StatsChannelFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Stats Channels", FeatureProfile.FeatureState.DISABLED, true);
this.guildService = guildService;
super.registerCommand(commandService, context.getBean(StatsChannelCommand.class));
}
/**
* Scheduled task to update the stats channel
* This will run every hour
*/
@Scheduled(cron = "0 0 * * * *")
public void updateStatsChannel() {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
assert batGuild != null;
if (batGuild.getFeatureProfile().isFeatureDisabled(this)) {
continue;
}
StatsChannelProfile profile = batGuild.getStatsChannelProfile();
profile.refreshChannels(batGuild);
}
}
/**
* Get the placeholders that the user can use
*
* @param replacements What to replace the placeholders using
* @return The placeholders
*/
public static String getPlaceholders(Object... replacements) {
StringBuilder builder = new StringBuilder();
builder.append("**Available Placeholders:**\n");
for (StatsPlaceholders placeholder : StatsPlaceholders.values()) {
String placeholderString = placeholder.getPlaceholder();
builder.append("`").append(placeholderString).append("` - ")
.append(StatsPlaceholders.replaceAllPlaceholders(placeholderString, replacements)).append("\n");
}
return builder.toString();
}
}

@ -0,0 +1,119 @@
package cc.fascinated.bat.features.statschannel;
import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.DiscordService;
import com.google.gson.Gson;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
public class StatsChannelProfile extends Serializable {
/**
* The channels to show stats on.
*/
private final Map<String, StatsChannel> channels = new HashMap<>();
/**
* Gets the channels to show stats on.
*
* @return the channels
*/
public Map<VoiceChannel, StatsChannel> getChannels() {
Map<VoiceChannel, StatsChannel> textChannels = new HashMap<>();
for (Map.Entry<String, StatsChannel> entry : channels.entrySet()) {
VoiceChannel textChannel = DiscordService.JDA.getVoiceChannelById(entry.getKey());
if (textChannel != null) {
textChannels.put(textChannel, entry.getValue());
}
}
return textChannels;
}
/**
* Gets a channel
*
* @param id the id of the channel
* @return the channel
*/
public VoiceChannel getChannel(String id) {
if (!channels.containsKey(id)) {
return null;
}
return DiscordService.JDA.getVoiceChannelById(id);
}
/**
* Adds a new stats channel
*
* @param channel the channel
* @param text the text to display
*/
public void addChannel(VoiceChannel channel, String text) {
channels.put(channel.getId(), new StatsChannel(text, ""));
}
/**
* Removes a channel
*
* @param channel the channel
*/
public void removeChannel(VoiceChannel channel) {
channels.remove(channel.getId());
}
/**
* Updates all the stats channels for the guild
*/
public void refreshChannels(BatGuild guild) {
for (Map.Entry<String, StatsChannel> entry : this.channels.entrySet()) {
VoiceChannel channel = DiscordService.JDA.getVoiceChannelById(entry.getKey());
if (channel == null) {
continue;
}
StatsChannel statsChannel = entry.getValue();
String display = StatsPlaceholders.replaceAllPlaceholders(statsChannel.getDisplay(), guild);
if (statsChannel.getLastValue().equals(display)) { // Don't update if the value is the same
continue;
}
channel.getManager().setName(display).queue();
}
}
@Override
public void load(Document document, Gson gson) {
Document channelsDocument = (Document) document.getOrDefault("channels", new Document());
for (Map.Entry<String, Object> entry : channelsDocument.entrySet()) {
Document channelDocument = (Document) entry.getValue();
channels.put(entry.getKey(), new StatsChannel(
channelDocument.getString("display"),
channelDocument.getString("lastValue")
));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
Document channelsDocument = new Document();
for (Map.Entry<String, StatsChannel> entry : channels.entrySet()) {
StatsChannel statsChannel = entry.getValue();
Document channelDocument = new Document();
channelDocument.put("display", statsChannel.getDisplay());
channelDocument.put("lastValue", statsChannel.getLastValue());
channelsDocument.put(entry.getKey(), channelDocument);
}
document.put("channels", channelsDocument);
return document;
}
@Override
public void reset() {
channels.clear();
}
}

@ -0,0 +1,81 @@
package cc.fascinated.bat.features.statschannel;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.model.BatGuild;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public enum StatsPlaceholders {
MEMBER_COUNT("{member_count}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return NumberFormatter.simpleFormat(((BatGuild) object).getDiscordGuild().getMembers().size());
}
},
BOT_COUNT("{bot_count}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return NumberFormatter.simpleFormat(((BatGuild) object).getDiscordGuild().getMembers().stream()
.filter(member -> member.getUser().isBot()).count());
}
},
CHANNEL_COUNT("{channel_count}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return NumberFormatter.simpleFormat(((BatGuild) object).getDiscordGuild().getChannels().size());
}
},
ROLE_COUNT("{role_count}", BatGuild.class) {
@Override
public String replacePlaceholder(Object object) {
return NumberFormatter.simpleFormat(((BatGuild) object).getDiscordGuild().getRoles().size());
}
};
/**
* The placeholder string that will get replaced
*/
private final String placeholder;
/**
* The class that the placeholder is associated with
*/
private final Class<?> clazz;
/**
* Replaces the placeholder with the string based on the overridden method
*
* @param object The object to replace the placeholder with
* @return The string with the placeholder replaced
*/
public String replacePlaceholder(Object object) {
if (clazz != null && !clazz.isInstance(object)) {
throw new IllegalArgumentException("Object is not an instance of " + clazz.getName());
}
return null;
}
/**
* Replaces all placeholders in a message with the objects provided
*
* @param message The message to replace the placeholders in
* @param objects The objects to replace the placeholders with
* @return The message with the placeholders replaced
*/
public static String replaceAllPlaceholders(String message, Object... objects) {
for (StatsPlaceholders placeholder : values()) {
for (Object object : objects) {
if (placeholder.getClazz() != null && !placeholder.getClazz().isInstance(object)) {
continue;
}
message = message.replace(placeholder.getPlaceholder(), placeholder.replacePlaceholder(object));
}
}
return message;
}
}

@ -0,0 +1,78 @@
package cc.fascinated.bat.features.statschannel.command;
import cc.fascinated.bat.Emojis;
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.features.statschannel.StatsChannelProfile;
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.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;
import java.util.Collections;
/**
* @author Fascinated (fascinated7)
*/
@Component("stats-channel.add:sub")
@CommandInfo(
name = "add",
description = "Add a stats channel"
)
public class AddSubCommand extends BatCommand {
public AddSubCommand() {
super.addOptions(new OptionData(OptionType.STRING, "display", "The text to display in the stats channel", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping displayOption = event.getOption("display");
assert displayOption != null;
String display = displayOption.getAsString();
if (display.length() > 24) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel name can only be a maximum of 32 characters".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
StatsChannelProfile profile = guild.getStatsChannelProfile();
if (profile.getChannels().size() >= 5) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s You can only have a maximum of 5 stats channels".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
guild.getDiscordGuild().createVoiceChannel("stats channel").queue(voiceChannel -> {
// Disallow everyone from connecting
voiceChannel.getManager().putPermissionOverride(
guild.getDiscordGuild().getPublicRole(),
null,
Collections.singleton(Permission.VOICE_CONNECT)
).queue();
profile.addChannel(voiceChannel, display);
profile.refreshChannels(guild); // For the stat channels to be updated
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(new EmbedDescriptionBuilder("%s The stats chanel %s has been created".formatted(
Emojis.CHECK_MARK_EMOJI,
voiceChannel.getAsMention()
))
.appendLine("Display: `%s`".formatted(display), true)
.build())
.build()).queue();
});
}
}

@ -0,0 +1,49 @@
package cc.fascinated.bat.features.statschannel.command;
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.features.statschannel.StatsChannel;
import cc.fascinated.bat.features.statschannel.StatsChannelFeature;
import cc.fascinated.bat.features.statschannel.StatsChannelProfile;
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.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Component("stats-channel.current:sub")
@CommandInfo(
name = "current",
description = "Shows the current stats channels"
)
public class CurrentSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
StatsChannelProfile profile = guild.getStatsChannelProfile();
EmbedDescriptionBuilder builder = new EmbedDescriptionBuilder("Stats Channels");
if (profile.getChannels().isEmpty()) {
builder.appendLine("You have no stats channels set up", true);
} else {
for (Map.Entry<VoiceChannel, StatsChannel> entry : profile.getChannels().entrySet()) {
builder.appendLine("%s: `%s`".formatted(entry.getKey().getAsMention(), entry.getValue().getDisplay()), true);
}
}
builder.emptyLine();
builder.appendLine(StatsChannelFeature.getPlaceholders(guild), false);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(builder.build())
.build())
.queue();
}
}

@ -0,0 +1,64 @@
package cc.fascinated.bat.features.statschannel.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.statschannel.StatsChannelProfile;
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.VoiceChannel;
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("stats-channel.remove:sub")
@CommandInfo(
name = "remove",
description = "Remove a stats channel"
)
public class RemoveSubCommand extends BatCommand {
public RemoveSubCommand() {
super.addOptions(new OptionData(OptionType.CHANNEL, "channel", "The stats channel to remove", true));
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
assert channelOption != null;
GuildChannelUnion channelUnion = channelOption.getAsChannel();
if (channelUnion.getType() != ChannelType.VOICE) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel must be a voice channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
StatsChannelProfile profile = guild.getStatsChannelProfile();
VoiceChannel statsChannel = profile.getChannel(channelUnion.getId());
if (statsChannel == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("%s The channel is not a stats channel".formatted(Emojis.CROSS_MARK_EMOJI))
.build()).queue();
return;
}
statsChannel.delete().queue(then -> {
profile.removeChannel(statsChannel);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("%s The stats channel has been removed".formatted(Emojis.CHECK_MARK_EMOJI))
.build()).queue();
});
}
}

@ -0,0 +1,29 @@
package cc.fascinated.bat.features.statschannel.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 = "stats-channel",
description = "Change your stats channel settings",
requiredPermissions = Permission.MANAGE_SERVER
)
public class StatsChannelCommand extends BatCommand {
@Autowired
public StatsChannelCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(AddSubCommand.class),
context.getBean(RemoveSubCommand.class),
context.getBean(CurrentSubCommand.class)
);
}
}

@ -11,6 +11,7 @@ import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.features.statschannel.StatsChannelProfile;
import cc.fascinated.bat.features.welcomer.WelcomerProfile;
import cc.fascinated.bat.premium.PremiumProfile;
import cc.fascinated.bat.service.DiscordService;
@ -180,6 +181,15 @@ public class BatGuild extends ProfileHolder {
return getProfile(MinecraftProfile.class);
}
/**
* Gets the stats channel profile
*
* @return the stats channel profile
*/
public StatsChannelProfile getStatsChannelProfile() {
return getProfile(StatsChannelProfile.class);
}
/**
* Saves the user
*/