Refactor settings architecture and remove singleton pattern
Reworked `Settings` class to replace the static singleton pattern with constructor injection of `SettingsProvider`, improving testability and modularity. Updated all affected classes, tests, and configurations to align with the new architecture.
This commit is contained in:
1 parent
1d737a2a4f
commit
64f4890931
21 files changed
+170
-120
No files matched your search
@@ -6,7 +6,6 @@ package me.bobert.netneutrality;
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import me.bobert.netneutrality.db.GSettings;
|
||||
import club.minnced.discord.jdave.interop.JDaveSessionFactory;
|
||||
import io.sentry.Sentry;
|
||||
import me.bobert.netneutrality.bootstrap.ApplicationContext;
|
||||
@@ -31,6 +30,7 @@ import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||
import me.bobert.netneutrality.utils.ChannelMessagePurger;
|
||||
import me.bobert.netneutrality.db.JpaUtils;
|
||||
import me.bobert.netneutrality.utils.Settings;
|
||||
import me.bobert.netneutrality.http.WebModule;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.audio.AudioModuleConfig;
|
||||
@@ -44,22 +44,19 @@ import org.apache.logging.log4j.core.config.Configurator;
|
||||
|
||||
public class App {
|
||||
private static final Logger logger = LogManager.getLogger(App.class);
|
||||
/**
|
||||
* Version string resolved from the early-bootstrap {@link Settings} singleton (properties must be
|
||||
* readable before the {@link ApplicationContext} exists).
|
||||
*/
|
||||
public static final String VERSION = Settings.getVersion();
|
||||
private static App instance;
|
||||
private final Settings settings;
|
||||
private JDA jda;
|
||||
private final ApplicationContext context = new ApplicationContext();
|
||||
|
||||
static void main() {
|
||||
System.out.println("Starting App v" + VERSION);
|
||||
Settings settings = new Settings();
|
||||
String version = settings.getMavenVersion();
|
||||
WebModule.setDefaultVersion(version);
|
||||
logger.info("Starting App v{}", version);
|
||||
|
||||
// Settings is intentionally a process-wide bootstrap singleton: Sentry/env/token must be
|
||||
// readable before the ApplicationContext is constructed.
|
||||
String env = Settings.getInstance().get("NETNEUTRALITY_BOT_ENV", "dev");
|
||||
String sentryDsn = Settings.getInstance().get("NETNEUTRALITY_SENTRY_DSN");
|
||||
String env = settings.get("NETNEUTRALITY_BOT_ENV", "dev");
|
||||
String sentryDsn = settings.get("NETNEUTRALITY_SENTRY_DSN");
|
||||
if (sentryDsn != null) {
|
||||
logger.info("Found Sentry DSN in config, setting up the SDK..");
|
||||
|
||||
@@ -68,7 +65,7 @@ public class App {
|
||||
options.setEnvironment(env);
|
||||
options.setTracesSampleRate(1.0);
|
||||
options.setDebug(false);
|
||||
options.setRelease(VERSION);
|
||||
options.setRelease(version);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,15 +75,16 @@ public class App {
|
||||
Configurator.setLevel("com.sedmelluq.discord", Level.DEBUG);
|
||||
}
|
||||
|
||||
instance = new App();
|
||||
instance = new App(settings);
|
||||
}
|
||||
|
||||
public App() {
|
||||
public App(Settings settings) {
|
||||
this.settings = settings;
|
||||
startup();
|
||||
}
|
||||
|
||||
private void startup() {
|
||||
String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN");
|
||||
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
||||
if (token == null) {
|
||||
throw new RuntimeException("No bot token provided, exiting.");
|
||||
}
|
||||
@@ -109,10 +107,8 @@ public class App {
|
||||
* context stops components in the exact reverse order.
|
||||
*/
|
||||
private void buildContext() {
|
||||
SettingsProvider settings = Settings.getInstance();
|
||||
|
||||
// Config first so SettingsProvider is resolvable for constructor injection.
|
||||
context.register(settings);
|
||||
context.register((SettingsProvider) settings);
|
||||
|
||||
// Allow ApplicationContext itself to be injected into components that need it.
|
||||
context.register(context);
|
||||
@@ -187,7 +183,7 @@ public class App {
|
||||
}
|
||||
|
||||
public void initJDA() {
|
||||
String token = Settings.getInstance().get("NETNEUTRALITY_BOT_TOKEN");
|
||||
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
||||
if (token == null) {
|
||||
SessionManager sessions = context.get(SessionManager.class);
|
||||
if (sessions != null) {
|
||||
@@ -216,9 +212,9 @@ public class App {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
System.out.println("[Net Neutrality] Shutting down...");
|
||||
logger.info("[Net Neutrality] Shutting down...");
|
||||
context.stop();
|
||||
System.out.println("[Net Neutrality] Shutdown complete");
|
||||
logger.info("[Net Neutrality] Shutdown complete");
|
||||
}
|
||||
|
||||
public JDA getJDA() {
|
||||
|
||||
@@ -33,7 +33,7 @@ public class MessageCommandContext extends CommandContext implements MessageCont
|
||||
private final SettingsProvider settings;
|
||||
|
||||
public MessageCommandContext(MessageReceivedEvent event, SettingsProvider settings) {
|
||||
super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor());
|
||||
super(event.getJDA(), event.isFromGuild() ? event.getGuild() : null, event.getChannel(), event.getMember(), event.getAuthor(), settings);
|
||||
this.settings = settings;
|
||||
this.event = event;
|
||||
this.message = event.getMessage();
|
||||
|
||||
@@ -9,6 +9,7 @@ package me.bobert.netneutrality.context.command;
|
||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.core.context.CommandContext;
|
||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||
import net.dv8tion.jda.api.events.Event;
|
||||
@@ -20,8 +21,8 @@ import java.util.List;
|
||||
public class MessageMenuContext extends CommandContext implements InteractionContext {
|
||||
private final MessageContextInteractionEvent event;
|
||||
|
||||
public MessageMenuContext(MessageContextInteractionEvent event) {
|
||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser());
|
||||
public MessageMenuContext(MessageContextInteractionEvent event, SettingsProvider settings) {
|
||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.core.command.options.SlashCommandOption;
|
||||
import me.bobert.netneutrality.core.context.CommandContext;
|
||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||
@@ -19,8 +20,8 @@ import java.util.stream.Collectors;
|
||||
public class SlashCommandContext extends CommandContext implements InteractionContext {
|
||||
private final SlashCommandInteractionEvent event;
|
||||
|
||||
public SlashCommandContext(SlashCommandInteractionEvent event) {
|
||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser());
|
||||
public SlashCommandContext(SlashCommandInteractionEvent event, SettingsProvider settings) {
|
||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package me.bobert.netneutrality.context.command;
|
||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.core.context.CommandContext;
|
||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import net.dv8tion.jda.api.entities.User;
|
||||
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
@@ -21,8 +22,8 @@ import java.util.List;
|
||||
public class UserMenuContext extends CommandContext implements InteractionContext {
|
||||
private final UserContextInteractionEvent event;
|
||||
|
||||
public UserMenuContext(UserContextInteractionEvent event) {
|
||||
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser());
|
||||
public UserMenuContext(UserContextInteractionEvent event, SettingsProvider settings) {
|
||||
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser(), settings);
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ package me.bobert.netneutrality.core.context;
|
||||
|
||||
import me.bobert.netneutrality.core.command.Command;
|
||||
import me.bobert.netneutrality.core.command.options.CommandOption;
|
||||
import me.bobert.netneutrality.utils.Settings;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
@@ -33,10 +33,12 @@ public abstract class CommandContext extends AbstractContext {
|
||||
private SubcommandData subcommand;
|
||||
private final Map<OptionData, String> invalidOptions = new HashMap<>();
|
||||
private final List<CommandOption> options;
|
||||
private final SettingsProvider settings;
|
||||
|
||||
public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
|
||||
public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user, SettingsProvider settings) {
|
||||
super(jda, guild, channel, member, user);
|
||||
this.options = new ArrayList<>();
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public Command getCommand() {
|
||||
@@ -131,7 +133,7 @@ public abstract class CommandContext extends AbstractContext {
|
||||
}
|
||||
|
||||
public boolean isMessageFromCreator() {
|
||||
String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER");
|
||||
String creatorId = settings.get("NETNEUTRALITY_BOT_OWNER");
|
||||
|
||||
return creatorId.equals(getDiscordUser().getId());
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ package me.bobert.netneutrality.core.thread;
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -18,6 +21,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public class ThreadManager implements ExecutorProvider {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ThreadManager.class);
|
||||
private final Map<String, ExecutorService> executors = new ConcurrentHashMap<>();
|
||||
|
||||
public ThreadManager() {
|
||||
@@ -56,7 +60,7 @@ public class ThreadManager implements ExecutorProvider {
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
System.out.println("[Executor] Shutting down...");
|
||||
logger.info("[Executor] Shutting down...");
|
||||
|
||||
try {
|
||||
for (ExecutorService ex : executors.values()) {
|
||||
@@ -64,17 +68,17 @@ public class ThreadManager implements ExecutorProvider {
|
||||
ex.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
System.err.println("[Executor] Running tasks interrupted");
|
||||
logger.info("[Executor] Running tasks interrupted");
|
||||
} finally {
|
||||
executors.forEach((name, ex) -> {
|
||||
if (!ex.isTerminated()) {
|
||||
System.err.println("[Executor: " + name + "] Forcing shutdown of running tasks..");
|
||||
logger.warn("[Executor: {}] Forcing shutdown of running tasks..", name);
|
||||
ex.shutdownNow();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
System.out.println("[Executor] Shutdown finished!");
|
||||
logger.info("[Executor] Shutdown finished!");
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,9 @@ public class EventsHandler implements EventListener {
|
||||
return new MessageTaskContext(event);
|
||||
});
|
||||
|
||||
addListener(UserContextInteractionEvent.class, UserMenuContext::new);
|
||||
addListener(MessageContextInteractionEvent.class, MessageMenuContext::new);
|
||||
addListener(SlashCommandInteractionEvent.class, SlashCommandContext::new);
|
||||
addListener(UserContextInteractionEvent.class, event -> new UserMenuContext(event, settings));
|
||||
addListener(MessageContextInteractionEvent.class, event -> new MessageMenuContext(event, settings));
|
||||
addListener(SlashCommandInteractionEvent.class, event -> new SlashCommandContext(event, settings));
|
||||
addListener(ModalInteractionEvent.class, ModalTaskContext::new);
|
||||
addListener(ButtonInteractionEvent.class, ButtonTaskContext::new);
|
||||
addListener(MessageReactionAddEvent.class, EmojiTaskContext::new);
|
||||
|
||||
@@ -9,7 +9,6 @@ package me.bobert.netneutrality.http;
|
||||
import com.google.gson.Gson;
|
||||
import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
import me.bobert.netneutrality.utils.Settings;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -35,7 +34,12 @@ public class WebModule {
|
||||
}
|
||||
|
||||
private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)";
|
||||
private static volatile String defaultVersion = "0.0.0";
|
||||
private static final Logger log = LoggerFactory.getLogger(WebModule.class);
|
||||
|
||||
public static void setDefaultVersion(String version) {
|
||||
defaultVersion = version;
|
||||
}
|
||||
private static final Map<String, CachedResponse> cache = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(100, 0.75f, true) {
|
||||
@Override
|
||||
@@ -170,6 +174,7 @@ public class WebModule {
|
||||
public static class Builder {
|
||||
private Request request;
|
||||
private final String url;
|
||||
private String version;
|
||||
private Method method = Method.GET;
|
||||
private long timeout = 5;
|
||||
private TimeUnit timeUnit = TimeUnit.MINUTES;
|
||||
@@ -187,6 +192,7 @@ public class WebModule {
|
||||
*/
|
||||
public Builder(String url) {
|
||||
this.url = url;
|
||||
this.version = defaultVersion;
|
||||
}
|
||||
|
||||
public Builder ping() {
|
||||
@@ -286,7 +292,7 @@ public class WebModule {
|
||||
|
||||
private Request createRequest() {
|
||||
Request.Builder builder = new Request.Builder().url(url);
|
||||
builder.addHeader("User-Agent", String.format(USER_AGENT, Settings.getVersion()));
|
||||
builder.addHeader("User-Agent", String.format(USER_AGENT, version));
|
||||
headers.forEach(builder::addHeader);
|
||||
|
||||
if (method == Method.PING) {
|
||||
|
||||
@@ -9,13 +9,16 @@ import me.bobert.netneutrality.core.context.TaskContext;
|
||||
import me.bobert.netneutrality.core.tasks.impl.AbstractBuiltInTask;
|
||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||
import me.bobert.netneutrality.core.tasks.trait.WithButton;
|
||||
import me.bobert.netneutrality.utils.Settings;
|
||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
|
||||
public class ReportBug extends AbstractBuiltInTask implements WithButton {
|
||||
|
||||
public ReportBug(TaskManager taskManager) {
|
||||
private final SettingsProvider settings;
|
||||
|
||||
public ReportBug(TaskManager taskManager, SettingsProvider settings) {
|
||||
super(taskManager);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +34,6 @@ public class ReportBug extends AbstractBuiltInTask implements WithButton {
|
||||
@Override
|
||||
public void executeTask(TaskContext context) {
|
||||
context.replyEphemeral("To report a bug with the bot, you can either use the /report-bug command " +
|
||||
"or join the discord here: " + Settings.getDiscordInvite());
|
||||
"or join the discord here: " + settings.get("NETNEUTRALITY_BOT_INVITE", "https://discord.gg/PyCpd7JGMp"));
|
||||
}
|
||||
}
|
||||
@@ -20,19 +20,14 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Process-wide configuration loaded from {@code bot.properties} and environment variables.
|
||||
*
|
||||
* <p>{@link #getInstance()} is retained intentionally as the early-bootstrap access point: Sentry,
|
||||
* env and the bot token must be readable before the {@code ApplicationContext} exists. Once the
|
||||
* context is built, prefer injecting {@link SettingsProvider}.
|
||||
*/
|
||||
public class Settings implements SettingsProvider {
|
||||
|
||||
private static Settings INSTANCE;
|
||||
private final File propFile = new File("bot.properties");
|
||||
private final Properties prop = new Properties();
|
||||
public final String DATETIMEFORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
private Settings() {
|
||||
public Settings() {
|
||||
checkAndLoadPropertiesFile();
|
||||
loadEnvironmentVariables();
|
||||
}
|
||||
@@ -53,36 +48,6 @@ public class Settings implements SettingsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Early-bootstrap accessor used before the application context exists.
|
||||
*
|
||||
* @return the process-wide settings instance
|
||||
*/
|
||||
public static Settings getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new Settings();
|
||||
}
|
||||
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap helper for the public Discord invite URL.
|
||||
*
|
||||
* @return the invite URL
|
||||
*/
|
||||
public static String getDiscordInvite() {
|
||||
return Settings.getInstance().get("NETNEUTRALITY_BOT_INVITE", "https://discord.gg/PyCpd7JGMp");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap helper for the bot version string.
|
||||
*
|
||||
* @return the version
|
||||
*/
|
||||
public static String getVersion() {
|
||||
return Settings.getInstance().getMavenVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(String key) {
|
||||
|
||||
Reference in new issue
Block a user