Bot Command Implementation

# Changes:
- Added /ban <player> <reason> [duration] command. Bans a user on the server for the specified reason and duration (in minutes). If no duration is specified, the default is 5 minutes.

- Added /kick <player> <reason> command. Kicks a player on the server for the specified reason.

- Added /whisper <player> <message> command. Send a private message to a player on the server.
This commit is contained in:
Paul Reilly 2023-09-04 22:57:36 -05:00
parent 4681fc9596
commit 85cc1f7ae0
13 changed files with 435 additions and 142 deletions

View File

@ -34,6 +34,13 @@ import org.bukkit.Bukkit;
public class Aggregate
{
private static final FNS4J logger = FNS4J.getLogger("Veritas");
private static final String FAILED_PACKET = """
Failed to process inbound chat packet.
An offending element was found transmitted through the stream.
The element has been dropped, and ignored.
Offending element: %s
Caused by: %s
Stack Trace: %s""";
private final BotClient bot;
private final Veritas plugin;
private final BukkitNative bukkitNativeListener;
@ -62,8 +69,17 @@ public class Aggregate
this.bukkitNativeListener = new BukkitNative(plugin);
this.serverListener = new ServerListener(plugin);
Bukkit.getServer().getPluginManager().registerEvents(this.getBukkitNativeListener(), plugin);
this.getServerListener().minecraftChatBound().subscribe();
Bukkit.getServer()
.getPluginManager()
.registerEvents(this.getBukkitNativeListener(), plugin);
this.getServerListener()
.minecraftChatBound()
.onErrorContinue((th, v) -> Aggregate.getLogger()
.error(FAILED_PACKET.formatted(
v.getClass().getName(),
th.getCause(),
th.getMessage())))
.subscribe();
this.bot = bot1;
}
@ -87,6 +103,11 @@ public class Aggregate
return bot;
}
public BotConfig getBotConfig()
{
return bot.getConfig();
}
public Veritas getPlugin()
{
return plugin;

View File

@ -58,13 +58,13 @@ public class ServerListener
.on(MessageCreateEvent.class)
.filter(m -> m.getMessage()
.getChannelId()
.equals(bot.getChatChannelId()))
.equals(bot.getConfig().getChatChannelId()))
.filter(m -> m.getMember().orElse(null) != null)
.filter(m -> !m.getMessage()
.getAuthor()
.orElseThrow(IllegalAccessError::new)
.getId()
.equals(plugin.getAggregate().getBot().getClient().getSelfId()))
.equals(bot.getClient().getSelfId()))
.doOnError(Aggregate.getLogger()::error)
.doOnNext(this::doMessageBodyDetails)
.then();
@ -81,7 +81,7 @@ public class ServerListener
.hoverEvent(HoverEvent.showText(
Component.text("Click to join our Discord server!")))
.clickEvent(ClickEvent.openUrl(
plugin.getAggregate().getBot().getInviteLink())))
plugin.getAggregate().getBotConfig().getInviteLink())))
.append(Component.text("] ", NamedTextColor.DARK_GRAY));
TextComponent user = Component.empty();

View File

@ -23,16 +23,20 @@
package fns.veritas.client;
import discord4j.common.util.Snowflake;
import discord4j.core.DiscordClientBuilder;
import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.entity.Guild;
import discord4j.core.object.entity.Message;
import discord4j.core.object.entity.PartialMember;
import discord4j.core.object.entity.Role;
import discord4j.core.object.entity.User;
import discord4j.core.object.entity.channel.Channel;
import discord4j.core.object.entity.channel.TextChannel;
import discord4j.core.spec.MessageCreateSpec;
import fns.veritas.cmd.base.BotCommandHandler;
import java.util.List;
import java.util.Objects;
import reactor.core.publisher.Mono;
public class BotClient
@ -58,37 +62,11 @@ public class BotClient
client.on(ChatInputInteractionEvent.class, handler::handle);
}
public String getBotId()
{
return client.getSelfId().asString();
}
public Mono<Guild> getServerGuildId()
{
return client.getGuildById(config.getId());
}
public GatewayDiscordClient getClient()
{
return client;
}
public Snowflake getChatChannelId()
{
return config.getChatChannelId();
}
public Snowflake getLogChannelId()
{
return config.getLogChannelId();
}
public String getInviteLink()
{
return config.getInviteLink();
}
public void messageChatChannel(String message, boolean system)
{
String channelID = config.getChatChannelId().asString();
@ -119,7 +97,6 @@ public class BotClient
if (message.contains("@"))
{
// \u200B is Zero Width Space, invisible on Discord
newMessage = message.replace("@", "@\u200B");
}
@ -144,6 +121,33 @@ public class BotClient
return deformat(newMessage);
}
public Mono<Boolean> isAdmin(final User user)
{
return getGuild().flatMap(guild -> guild.getMemberById(user.getId()))
.flatMapMany(PartialMember::getRoles)
.filter(role -> getConfig().getAdminRoleId().asLong() == role.getId().asLong())
.filter(Objects::nonNull)
.next()
.hasElement();
}
public Mono<Channel> getLogsChannel() {
return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getLogChannelId()));
}
public Mono<Channel> getChatChannel() {
return getGuild().flatMap(guild -> guild.getChannelById(getConfig().getChatChannelId()));
}
public Mono<Guild> getGuild() {
return getClient().getGuildById(getConfig().getGuildId());
}
public BotConfig getConfig()
{
return config;
}
public String deformat(String input)
{
return input.replaceAll("([_\\\\`*>|])", "\\\\$1");

View File

@ -39,9 +39,17 @@ import org.jetbrains.annotations.NonNls;
public class BotConfig
{
@NonNls
public static final String GUILD_ID = "guild_id";
private static final String GUILD_ID = "bot_settings.guild_id";
@NonNls
private static final String BOT_TOKEN = "bot_token";
private static final String BOT_TOKEN = "bot_settings.bot_token";
@NonNls
private static final String MC_CHANNEL_ID = "bot_settings.mc_channel_id";
@NonNls
private static final String LOG_CHANNEL_ID = "bot_settings.log_channel_id";
@NonNls
private static final String INVITE_LINK = "bot_settings.invite_link";
private final GenericConfig config;
public BotConfig(final Veritas plugin) throws IOException
@ -54,29 +62,29 @@ public class BotConfig
return config.getString(BOT_TOKEN);
}
public String getPrefix()
public Snowflake getGuildId()
{
return config.getString("bot_prefix");
}
public Snowflake getId()
{
return Snowflake.of(config.getString(GUILD_ID));
return Snowflake.of(config.getLong(GUILD_ID));
}
public Snowflake getChatChannelId()
{
return Snowflake.of(config.getString("channel_id"));
return Snowflake.of(config.getLong(MC_CHANNEL_ID));
}
public Snowflake getLogChannelId()
{
return Snowflake.of(config.getString("log_channel_id"));
return Snowflake.of(config.getLong(LOG_CHANNEL_ID));
}
public Snowflake getAdminRoleId()
{
return Snowflake.of(config.getLong("admin_settings.admin_role_id"));
}
public String getInviteLink()
{
return config.getString("invite_link");
return config.getString(INVITE_LINK);
}
private Function<File, FileConfiguration> f0(final Veritas plugin)
@ -94,11 +102,10 @@ public class BotConfig
catch (IOException | InvalidConfigurationException ex)
{
fc.addDefault(BOT_TOKEN, "token");
fc.addDefault("bot_prefix", "!");
fc.addDefault(GUILD_ID, GUILD_ID);
fc.addDefault("channel_id", "nil");
fc.addDefault("log_channel_id", "nil");
fc.addDefault("invite_link", "https://discord.gg/invite");
fc.addDefault(GUILD_ID, 0);
fc.addDefault(MC_CHANNEL_ID, 0);
fc.addDefault(LOG_CHANNEL_ID, 0);
fc.addDefault(INVITE_LINK, "https://discord.gg/invite");
fc.options().copyDefaults(true);

View File

@ -0,0 +1,98 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.veritas.cmd;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandInteractionOptionValue;
import discord4j.core.object.entity.User;
import fns.patchwork.base.Shortcuts;
import fns.veritas.Veritas;
import fns.veritas.cmd.base.BotCommand;
import java.time.Duration;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import reactor.core.publisher.Mono;
public class BanCommand implements BotCommand
{
@Override
public String getName()
{
return "ban";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
final String playerName = event.getOption("player")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final String reason = event.getOption("reason")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final Duration duration = event.getOption("duration")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asLong)
.map(Duration::ofMinutes)
.orElse(Duration.ofMinutes(5));
final User user = event.getInteraction().getUser();
return Shortcuts.provideModule(Veritas.class)
.getAggregate()
.getBot()
.isAdmin(user)
.doOnSuccess(b ->
{
if (Boolean.FALSE.equals(b))
return;
final Player player = Bukkit.getPlayer(playerName);
if (player == null)
{
event.reply()
.withEphemeral(true)
.withContent("Player not found")
.block();
return;
}
player.ban(reason, duration, user.getUsername());
event.reply()
.withContent("Kicked " + playerName)
.withEphemeral(true)
.block();
event.getInteractionResponse()
.createFollowupMessage(user.getUsername() + ": Kicked " + playerName)
.then();
})
.then();
}
}

View File

@ -0,0 +1,90 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.veritas.cmd;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandInteractionOptionValue;
import discord4j.core.object.entity.User;
import fns.patchwork.base.Shortcuts;
import fns.patchwork.kyori.MiniMessageWrapper;
import fns.veritas.Veritas;
import fns.veritas.cmd.base.BotCommand;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import reactor.core.publisher.Mono;
public class KickCommand implements BotCommand
{
@Override
public String getName()
{
return "kick";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
final String playerName = event.getOption("player")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final String reason = event.getOption("reason")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final User user = event.getInteraction().getUser();
return Shortcuts.provideModule(Veritas.class)
.getAggregate()
.getBot()
.isAdmin(user)
.doOnSuccess(b ->
{
if (Boolean.FALSE.equals(b))
return;
final Player player = Bukkit.getPlayer(playerName);
if (player == null)
{
event.reply()
.withEphemeral(true)
.withContent("Player not found")
.block();
return;
}
player.kick(MiniMessageWrapper.deserialize(true, reason));
event.reply()
.withContent("Kicked " + playerName)
.withEphemeral(true)
.block();
event.getInteractionResponse()
.createFollowupMessage(user.getUsername() + ": Kicked " + playerName)
.then();
})
.then();
}
}

View File

@ -0,0 +1,75 @@
/*
* This file is part of FreedomNetworkSuite - https://github.com/SimplexDevelopment/FreedomNetworkSuite
* Copyright (C) 2023 Simplex Development and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.veritas.cmd;
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
import discord4j.core.object.command.ApplicationCommandInteractionOption;
import discord4j.core.object.command.ApplicationCommandInteractionOptionValue;
import fns.patchwork.kyori.MiniMessageWrapper;
import fns.veritas.cmd.base.BotCommand;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import reactor.core.publisher.Mono;
public class WhisperCommand implements BotCommand
{
@Override
public String getName()
{
return "whisper";
}
@Override
public Mono<Void> handle(ChatInputInteractionEvent event)
{
final String player = event.getOption("player")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final String message = event.getOption("message")
.flatMap(ApplicationCommandInteractionOption::getValue)
.map(ApplicationCommandInteractionOptionValue::asString)
.orElseThrow();
final Component c = MiniMessageWrapper.deserialize(true,
"<gray>[<yellow>Whisper<gray>] <white>"
+ event.getInteraction().getUser().getUsername()
+ "<gray>: "
+ message);
final Player actual = Bukkit.getPlayer(player);
if (actual == null) {
return event.reply("Player not found!")
.withEphemeral(true)
.then();
}
actual.sendMessage(c);
return event.reply("Sent!")
.withEphemeral(true)
.then();
}
}

View File

@ -1,72 +0,0 @@
/*
* This file is part of Freedom-Network-Suite - https://github.com/AtlasMediaGroup/Freedom-Network-Suite
* Copyright (C) 2023 Total Freedom Server Network and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package fns.veritas.messaging;
import discord4j.core.object.component.LayoutComponent;
import discord4j.core.spec.MessageCreateFields;
import discord4j.core.spec.MessageCreateSpec;
import discord4j.rest.util.AllowedMentions;
public class SimpleMessageWrapper
{
private final MessageCreateSpec.Builder spec;
public SimpleMessageWrapper()
{
this.spec = MessageCreateSpec.builder();
}
public void setContent(final String content)
{
this.spec.content(content);
}
public void setEmbeds(final EmbedWrapper embed)
{
this.spec.addAllEmbeds(embed.getEmbeds());
}
public void setAttachments(final MessageCreateFields.File... files)
{
this.spec.addFiles(files);
}
public void setSpoilerAttachments(final MessageCreateFields.FileSpoiler... files)
{
this.spec.addFileSpoilers(files);
}
public void setAllowedMentions(final AllowedMentions allowedMentions)
{
this.spec.allowedMentions(allowedMentions);
}
public void setLayoutComponents(final LayoutComponent... components)
{
for (final LayoutComponent component : components)
{
this.spec.addComponent(component);
}
}
}

View File

@ -0,0 +1,24 @@
{
"name": "ban",
"description": "Bans a user from the server.",
"options": [
{
"name": "player",
"description": "The player to ban.",
"type": 3,
"required": true
},
{
"name": "reason",
"description": "The reason for the ban.",
"type": 3,
"required": true
},
{
"name": "duration",
"description": "The duration of the ban, in minutes. Default is 5 minutes.",
"type": 4,
"required": false
}
]
}

View File

@ -0,0 +1,18 @@
{
"name": "kick",
"description": "Kicks a user from the server",
"options": [
{
"name": "player",
"type": 3,
"description": "The player to kick",
"required": true
},
{
"name": "reason",
"type": 3,
"description": "The reason for kicking the player",
"required": true
}
]
}

View File

@ -0,0 +1,18 @@
{
"name": "whisper",
"description": "Whisper to a user.",
"options": [
{
"name": "player",
"type": 3,
"description": "The in-game user to whisper to.",
"required": true
},
{
"name": "message",
"type": 3,
"description": "The message to send.",
"required": true
}
]
}

View File

@ -0,0 +1,10 @@
[bot_settings]
bot_token = "xyz-123-REPLACE-ME"
invite_link = "https://discord.gg/invite"
guild_id = 0
mc_channel_id = 0
log_channel_id = 0
[admin_settings]
# This role will be able to use the /kick and /ban commands.
admin_role_id = 0