1
0
mirror of https://github.com/sciwhiz12/Janitor.git synced 2024-09-19 15:34:01 +00:00

Overhaul bot to be more extendible

This commit is contained in:
Arnold Alejo Nunag 2020-09-02 06:16:49 +08:00
parent 1f2668458a
commit da4abc6309
Signed by: sciwhiz12
GPG Key ID: 622CF446534317E1
19 changed files with 379 additions and 107 deletions

9
.gitignore vendored
View File

@ -1,14 +1,13 @@
# From https://github.com/github/gitignore/blob/master/Gradle.gitignore
.gradle
.idea/
.gradle/
build/
!src/**/build/
out/
run/
gradle-app.setting
!gradle-wrapper.jar
.gradletasknamecache
.idea/
out/
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties

View File

@ -32,9 +32,7 @@ dependencies {
implementation "net.sf.jopt-simple:jopt-simple:${jopt_version}"
implementation "com.google.guava:guava:${guava_version}"
implementation "com.google.code.gson:gson:${gson_version}"
implementation "org.apache.logging.log4j:log4j-api:${log4j_version}"
implementation "org.apache.logging.log4j:log4j-core:${log4j_version}"
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
implementation "ch.qos.logback:logback-classic:${logback_version}"
testImplementation "junit:junit:${junit_version}"
}

View File

@ -6,5 +6,5 @@ nightconfig_version=3.6.3
jopt_version=6.0-alpha-3
guava_version=29.0-jre
gson_version=2.8.6
logback_version=1.3.0-alpha5
junit_version=4.13
log4j_version=2.13.3

View File

@ -0,0 +1,35 @@
package sciwhiz12.janitor;
import com.google.common.base.Preconditions;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.Permission;
import sciwhiz12.janitor.config.BotConfig;
import sciwhiz12.janitor.config.BotOptions;
import static sciwhiz12.janitor.Logging.JANITOR;
public class BotStartup {
public static void main(String[] args) {
JANITOR.info("Starting...");
BotOptions options = new BotOptions(args);
BotConfig config = new BotConfig(options);
JANITOR.info("Building bot instance and connecting to Discord...");
JDABuilder builder;
JanitorBot bot;
try {
Preconditions.checkArgument(config.getToken().isPresent(),
"Token is not supplied through config or command line");
builder = JDABuilder.createDefault(config.getToken().get());
bot = new JanitorBot(builder, config);
bot.getJDA().awaitReady();
String inviteURL = bot.getJDA().getInviteUrl(Permission.ADMINISTRATOR);
JANITOR.info("Invite URL (gives ADMIN permission): " + inviteURL);
} catch (Exception ex) {
JANITOR.error("Error while building Discord connection", ex);
}
}
}

View File

@ -1,14 +0,0 @@
package sciwhiz12.janitor;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
public class CommandListener extends ListenerAdapter {
public static final CommandListener INSTANCE = new CommandListener();
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
JanitorBot.INSTANCE.getCommandRegistry().parseMessage(event);
}
}

View File

@ -1,68 +1,52 @@
package sciwhiz12.janitor;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.commands.OKCommand;
import sciwhiz12.janitor.commands.PingCommand;
import sciwhiz12.janitor.commands.ShutdownCommand;
import sciwhiz12.janitor.config.BotConfig;
import sciwhiz12.janitor.listeners.StatusListener;
import javax.security.auth.login.LoginException;
public class JanitorBot {
public static JanitorBot INSTANCE;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.Logging.STATUS;
public class JanitorBot {
private final JDA jda;
private final BotConfig config;
private final CommandRegistry cmdRegistry;
public JanitorBot(JDA jda, String prefix) {
this.jda = jda;
this.cmdRegistry = new CommandRegistry(this, prefix);
public JanitorBot(JDABuilder jdaBuilder, BotConfig config) throws LoginException {
this.config = config;
this.cmdRegistry = new CommandRegistry(this, config.getCommandPrefix());
jdaBuilder
.setActivity(Activity.playing("the Readying game..."))
.setStatus(OnlineStatus.DO_NOT_DISTURB)
.setAutoReconnect(true)
.addEventListeners(
cmdRegistry,
new StatusListener(this)
);
this.jda = jdaBuilder.build();
JANITOR.info(STATUS, "Bot is built");
}
public JDA getDiscord() {
public JDA getJDA() {
return this.jda;
}
public BotConfig getConfig() {
return this.config;
}
public CommandRegistry getCommandRegistry() {
return this.cmdRegistry;
}
public static void main(String[] args) throws LoginException {
System.out.println("Starting...");
OptionParser parser = new OptionParser();
ArgumentAcceptingOptionSpec<String> token = parser
.accepts("token", "The Discord token for the bot user").withRequiredArg().required();
ArgumentAcceptingOptionSpec<String> prefix = parser
.accepts("prefix", "The prefix for commands").withRequiredArg().defaultsTo("!");
ArgumentAcceptingOptionSpec<Long> owner = parser.accepts("owner",
"The snowflake ID of the bot owner; Used for shutdowns and other bot management commands")
.withRequiredArg().ofType(Long.class);
OptionSet options = parser.parse(args);
System.out.println("Configuring and connecting...");
JDABuilder builder = JDABuilder.createDefault(token.value(options));
builder.addEventListeners(CommandListener.INSTANCE);
builder.enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS);
JDA jda = builder.build();
INSTANCE = new JanitorBot(jda, prefix.value(options));
String inviteURL = jda.getInviteUrl(Permission.ADMINISTRATOR);
INSTANCE.getCommandRegistry().addCommand("ping", new PingCommand());
INSTANCE.getCommandRegistry().addCommand("ok", new OKCommand());
if (options.has(owner)) {
INSTANCE.getCommandRegistry().addCommand("shutdown", new ShutdownCommand(owner.value(options)));
}
System.out.println("Ready! Invite URL: " + inviteURL);
public void shutdown() {
JANITOR.info(STATUS, "Shutting down!");
getJDA().shutdown();
}
}

View File

@ -0,0 +1,14 @@
package sciwhiz12.janitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
public class Logging {
public static final Marker STATUS = MarkerFactory.getMarker("STATUS");
public static final Marker COMMANDS = MarkerFactory.getMarker("COMMANDS");
public static final Logger JANITOR = LoggerFactory.getLogger("janitor");
public static final Logger CONFIG = LoggerFactory.getLogger("janitor.config");
}

View File

@ -0,0 +1,13 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public abstract class BaseCommand {
protected final CommandRegistry registry;
public BaseCommand(CommandRegistry registry) {
this.registry = registry;
}
public abstract void onCommand(MessageReceivedEvent event);
}

View File

@ -1,44 +1,58 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.jetbrains.annotations.NotNull;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.listeners.BaseListener;
import sciwhiz12.janitor.utils.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CommandRegistry {
private final JanitorBot bot;
private final String prefix;
import static sciwhiz12.janitor.Logging.COMMANDS;
import static sciwhiz12.janitor.Logging.JANITOR;
public class CommandRegistry extends BaseListener {
private final Pattern pattern;
private final Map<String, ICommand> registry = new HashMap<>();
private final Map<String, BaseCommand> registry = new HashMap<>();
public CommandRegistry(JanitorBot bot, String prefix) {
this.bot = bot;
this.prefix = prefix;
super(bot);
this.pattern = Pattern.compile("^" + prefix + "([A-Za-z0-9]+).*$");
addCommand("ping", new PingCommand(this));
addCommand("ok", new OKCommand(this));
if (bot.getConfig().getOwnerID().isPresent()) {
addCommand("shutdown", new ShutdownCommand(this, bot.getConfig().getOwnerID().get()));
}
}
public void addCommand(String cmd, ICommand instance) {
public JanitorBot getBot() {
return this.bot;
}
public void addCommand(String cmd, BaseCommand instance) {
registry.put(cmd, instance);
}
public void parseMessage(MessageReceivedEvent event) {
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
try {
String msg = event.getMessage().getContentStripped();
String msg = event.getMessage().getContentDisplay();
Matcher matcher = pattern.matcher(msg);
if (!matcher.matches()) { return; }
String cmd = matcher.group(1);
if (registry.containsKey(cmd)) {
System.out.printf("Received command: %s ; full message: %s%n", cmd, msg);
registry.get(cmd).onCommand(bot, event);
if (matcher.matches()) {
String cmd = matcher.group(1);
if (registry.containsKey(cmd)) {
JANITOR.debug(COMMANDS, "Received command: {}; author: {}, full message: {}", cmd,
Util.toString(event.getAuthor()), msg);
registry.get(cmd).onCommand(event);
}
}
}
catch (Exception e) {
System.err
.printf("Error while parsing message: %s ; %s%n", event.getMessage().getContentStripped(),
e);
} catch (Exception e) {
JANITOR.error(COMMANDS, "Error while parsing message: {}",
event.getMessage().getContentStripped(), e);
}
}
}

View File

@ -1,8 +0,0 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.JanitorBot;
public interface ICommand {
void onCommand(JanitorBot bot, MessageReceivedEvent event);
}

View File

@ -1,11 +1,14 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.JanitorBot;
public class OKCommand implements ICommand {
public class OKCommand extends BaseCommand {
public OKCommand(CommandRegistry registry) {
super(registry);
}
@Override
public void onCommand(JanitorBot bot, MessageReceivedEvent event) {
public void onCommand(MessageReceivedEvent event) {
event.getMessage().addReaction("\uD83D\uDC4C").queue();
}
}

View File

@ -1,11 +1,14 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.JanitorBot;
public class PingCommand implements ICommand {
public class PingCommand extends BaseCommand {
public PingCommand(CommandRegistry registry) {
super(registry);
}
@Override
public void onCommand(JanitorBot bot, MessageReceivedEvent event) {
public void onCommand(MessageReceivedEvent event) {
event.getMessage().getChannel().sendMessage("Pong!").queue();
}
}

View File

@ -1,20 +1,41 @@
package sciwhiz12.janitor.commands;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.utils.Util;
public class ShutdownCommand implements ICommand {
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.Logging.STATUS;
public class ShutdownCommand extends BaseCommand {
private final long ownerID;
public ShutdownCommand(long ownerID) {
public ShutdownCommand(CommandRegistry registry, long ownerID) {
super(registry);
System.out.println(ownerID);
this.ownerID = ownerID;
}
@Override
public void onCommand(JanitorBot bot, MessageReceivedEvent event) {
public void onCommand(MessageReceivedEvent event) {
if (event.getAuthor().getIdLong() == ownerID) {
event.getMessage().getChannel().sendMessage("Shutting down. Goodbye!").queue();
event.getJDA().shutdown();
event.getMessage().getChannel()
.sendMessage("Shutting down, in accordance with the owner's command. Goodbye all!")
.queue();
registry.getBot().getConfig().getOwnerID()
.map(id -> event.getJDA().getUserById(id))
.ifPresent(owner -> owner.openPrivateChannel().submit()
.thenCompose(channel -> channel.sendMessage(
"Shutting down, in accordance with your orders. Goodbye!")
.submit())
.whenComplete(Util.handle(
msg -> JANITOR
.debug(STATUS, "Sent shutdown message to owner: {}",
Util.toString(owner)),
err -> JANITOR
.error(STATUS, "Error while sending shutdown message to owner", err)
))
.thenAccept(v -> registry.getBot().shutdown())
.join());
}
}
}

View File

@ -0,0 +1,69 @@
package sciwhiz12.janitor.config;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileWatcher;
import com.electronwill.nightconfig.toml.TomlFormat;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import static sciwhiz12.janitor.Logging.CONFIG;
import static sciwhiz12.janitor.Logging.JANITOR;
public class BotConfig {
public static final Path DEFAULT_CONFIG_PATH = Path.of("config.toml");
public static final String CLIENT_TOKEN = "discord.client_token";
public static final String OWNER_ID = "discord.owner_id";
public static final String COMMAND_PREFIX = "commands.prefix";
private final BotOptions options;
private final Path configPath;
private final CommentedFileConfig config;
public BotConfig(BotOptions options) {
this.options = options;
this.configPath = options.getConfigPath().orElse(DEFAULT_CONFIG_PATH);
this.config = CommentedFileConfig.builder(configPath, TomlFormat.instance())
.defaultResource("default-config.toml")
.preserveInsertionOrder()
.build();
try {
JANITOR.info("Building config from {}", configPath);
config.load();
// TODO: config spec
FileWatcher.defaultInstance().addWatch(configPath, this::onFileChange);
} catch (IOException ex) {
JANITOR.error("Error while building config from file {}", configPath, ex);
}
}
public CommentedFileConfig getRawConfig() {
return config;
}
public Optional<String> getToken() {
return options.getToken().or(() -> config.getOptional(CLIENT_TOKEN));
}
public String getCommandPrefix() {
return options.getCommandPrefix().orElseGet(() -> config.get(COMMAND_PREFIX));
}
public Optional<Long> getOwnerID() {
return options.getOwnerID().or(() -> config.getOptional(OWNER_ID));
}
public void close() {
config.close();
}
void onFileChange() {
try {
CONFIG.info("Reloading config due to file change {}", configPath);
config.load();
} catch (Exception ex) {
CONFIG.error("Error while reloading config from {}", configPath, ex);
}
}
}

View File

@ -0,0 +1,54 @@
package sciwhiz12.janitor.config;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.util.PathConverter;
import java.nio.file.Path;
import java.util.Optional;
import static joptsimple.util.PathProperties.*;
public class BotOptions {
private final OptionSet options;
private final ArgumentAcceptingOptionSpec<Path> configPath;
private final ArgumentAcceptingOptionSpec<String> token;
private final ArgumentAcceptingOptionSpec<String> prefix;
private final ArgumentAcceptingOptionSpec<Long> owner;
public BotOptions(String[] arguments) {
OptionParser parser = new OptionParser();
this.configPath = parser
.accepts("config", "The path to the config file; defaults to 'config.toml'")
.withRequiredArg()
.withValuesConvertedBy(new PathConverter(FILE_EXISTING, READABLE, WRITABLE));
this.token = parser
.accepts("token", "The Discord token for the bot user")
.withRequiredArg();
this.prefix = parser
.accepts("prefix", "The prefix for commands")
.withRequiredArg();
this.owner = parser.accepts("owner",
"The user ID of the bot owner")
.withRequiredArg()
.ofType(Long.class);
this.options = parser.parse(arguments);
}
public Optional<Path> getConfigPath() {
return configPath.valueOptional(options);
}
public Optional<String> getToken() {
return token.valueOptional(options);
}
public Optional<String> getCommandPrefix() {
return prefix.valueOptional(options);
}
public Optional<Long> getOwnerID() {
return owner.valueOptional(options);
}
}

View File

@ -0,0 +1,12 @@
package sciwhiz12.janitor.listeners;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import sciwhiz12.janitor.JanitorBot;
public abstract class BaseListener extends ListenerAdapter {
protected final JanitorBot bot;
public BaseListener(JanitorBot bot) {
this.bot = bot;
}
}

View File

@ -0,0 +1,35 @@
package sciwhiz12.janitor.listeners;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.events.ReadyEvent;
import org.jetbrains.annotations.NotNull;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.utils.Util;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.Logging.STATUS;
public class StatusListener extends BaseListener {
public StatusListener(JanitorBot bot) {
super(bot);
}
@Override
public void onReady(@NotNull ReadyEvent event) {
event.getJDA().getPresence()
.setPresence(OnlineStatus.ONLINE, Activity.playing("n' sweeping n' testing!"));
JANITOR.info("Ready!");
bot.getConfig().getOwnerID()
.map(ownerId -> bot.getJDA().retrieveUserById(ownerId))
.ifPresent(retrieveUser ->
retrieveUser.submit()
.thenCompose(user -> user.openPrivateChannel().submit())
.thenCompose(channel -> channel.sendMessage("Started up and ready!").submit())
.whenComplete(Util.handle(
msg -> JANITOR.debug(STATUS, "Sent ready message to owner!"),
error -> JANITOR.error(STATUS, "Error while sending ready message to owner", error))
)
);
}
}

View File

@ -0,0 +1,30 @@
package sciwhiz12.janitor.utils;
import net.dv8tion.jda.api.entities.User;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class Util {
public static <T> T make(Supplier<T> creator, Consumer<T> configurator) {
T obj = creator.get();
configurator.accept(obj);
return obj;
}
public static String toString(final User user) {
return String.format("<%s#%s:%s>", user.getName(), user.getDiscriminator(), user.getId());
}
public static <Success, Error> BiConsumer<Success, Error> handle(final Consumer<Success> success,
final Consumer<Error> exceptionally) {
return (suc, ex) -> {
if (ex == null) {
success.accept(suc);
} else {
exceptionally.accept(ex);
}
};
}
}

View File

@ -0,0 +1,10 @@
[discord]
# The client secret/token for the bot user
client_token = "NONE"
# The id of the bot owner; used for sending status messages and allowing bot admin commands
owner_id = 0
[commands]
# The prefix for commands
prefix = "!"