Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add setting to keep config synced with a custom URL #488

Merged
merged 4 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/main/java/dinkplugin/DinkPluginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,20 @@ default boolean includeLocation() {
return true;
}

@ConfigItem(
keyName = SettingsManager.DYNAMIC_IMPORT_CONFIG_KEY,
name = "Dynamic Config URL",
description = "Synchronizes your Dink configuration with the specified URL.<br/>" +
"Whenever Dink starts, it imports the config offered by the URL.<br/>" +
"The config applies to all webhooks, so ensure you trust this URL.<br/>" +
"Only one URL is supported",
position = 1017,
section = advancedSection
)
default String dynamicConfigUrl() {
return "";
}

@ConfigItem(
keyName = "discordWebhook", // do not rename; would break old configs
name = "Primary Webhook URLs",
Expand Down
73 changes: 70 additions & 3 deletions src/main/java/dinkplugin/SettingsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.events.ConfigChanged;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
Expand All @@ -30,6 +37,7 @@
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.swing.SwingUtilities;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -53,6 +61,8 @@
@RequiredArgsConstructor(onConstructor_ = { @Inject })
public class SettingsManager {
public static final String CONFIG_GROUP = "dinkplugin";
public static final String DYNAMIC_IMPORT_CONFIG_KEY = "dynamicConfigUrl";

private static final Set<Integer> PROBLEMATIC_VARBITS;

/**
Expand Down Expand Up @@ -90,6 +100,7 @@ public class SettingsManager {
private final DinkPlugin plugin;
private final DinkPluginConfig config;
private final ConfigManager configManager;
private final OkHttpClient httpClient;

/**
* Check whether a username complies with the configured RSN filter list.
Expand Down Expand Up @@ -128,6 +139,7 @@ public void init() {
.addAll(keysBySection.getOrDefault(DinkPluginConfig.webhookSection.toLowerCase().replace(" ", ""), Collections.emptySet()))
.add("metadataWebhook") // MetaNotifier's configuration is in the Advanced section
.build();
importDynamicConfig(config.dynamicConfigUrl());
}

void onCommand(CommandExecuted event) {
Expand Down Expand Up @@ -186,6 +198,11 @@ void onConfigChanged(ConfigChanged event) {
return;
}

if (DYNAMIC_IMPORT_CONFIG_KEY.equals(key)) {
importDynamicConfig(value);
return;
}

if (value != null && value.isEmpty() && ("embedFooterText".equals(key) || "embedFooterIcon".equals(key) || "deathIgnoredRegions".equals(key) || ChatNotifier.PATTERNS_CONFIG_KEY.equals(key))) {
SwingUtilities.invokeLater(() -> {
if (StringUtils.isEmpty(configManager.getConfiguration(CONFIG_GROUP, key))) {
Expand Down Expand Up @@ -335,6 +352,50 @@ private void setFilteredNames(String configValue) {
log.debug("Updated RSN Filter List to: {}", filteredNames);
}

private void importDynamicConfig(String url) {
if (url == null || url.isBlank()) return;

HttpUrl httpUrl = HttpUrl.parse(url);
if (httpUrl == null) {
plugin.addChatWarning("The specified Dynamic Config URL is invalid");
return;
}

Request request = new Request.Builder().url(httpUrl).get().build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
ResponseBody body = response.body();
if (body == null) {
plugin.addChatWarning("The specified Dynamic Config URL did not provide any settings to import");
return;
}

Map<String, Object> map;
try {
map = gson.fromJson(body.charStream(), new TypeToken<Map<String, Object>>() {}.getType());
} catch (Exception e) {
log.warn("Could not deserialize dynamic config", e);
plugin.addChatWarning("Failed to parse settings from the Dynamic Config URL");
return;
} finally {
body.close();
}

// prevent never-ending requests if service always yields a different config URL
map.remove(DYNAMIC_IMPORT_CONFIG_KEY);

handleImport(map, true);
}

@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.warn("Could not reach dynamic config url", e);
plugin.addChatWarning("Failed to read the specified Dynamic Config URL");
}
});
}

/**
* Exports the full Dink config to a JSON map, excluding empty lists,
* which is copied to the user's clipboard in string form
Expand Down Expand Up @@ -392,15 +453,15 @@ private void importConfig() {
return null;
}
})
.thenAcceptAsync(this::handleImport)
.thenAcceptAsync(m -> handleImport(m, false))
.exceptionally(e -> {
plugin.addChatWarning("Failed to read clipboard");
return null;
});
}

@Synchronized
private void handleImport(Map<String, Object> map) {
private void handleImport(Map<String, Object> map, boolean quiet) {
if (map == null) return;

AtomicInteger numUpdated = new AtomicInteger();
Expand Down Expand Up @@ -462,11 +523,17 @@ private void handleImport(Map<String, Object> map) {
}
});

int count = numUpdated.get();
if (quiet && count <= 0) {
log.debug("Updated 0 config settings from map of size {}", map.size());
return;
}

plugin.addChatSuccess(
String.format(
"Updated %d config settings (from %d total specified in import). " +
"Please close and open the plugin settings panel for these changes to be visually reflected.",
numUpdated.get(),
count,
map.size()
)
);
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/dinkplugin/notifiers/BaseNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import dinkplugin.message.NotificationBody;
import dinkplugin.util.WorldUtils;
import net.runelite.api.Client;
import net.runelite.api.Player;
import net.runelite.api.WorldType;
import org.apache.commons.lang3.StringUtils;

Expand Down Expand Up @@ -34,7 +35,8 @@ public boolean isEnabled() {
if (WorldUtils.isIgnoredWorld(world)) {
return false;
}
return settingsManager.isNamePermitted(client.getLocalPlayer().getName());
Player player = client.getLocalPlayer();
return player != null && settingsManager.isNamePermitted(player.getName());
}

protected abstract String getWebhookUrl();
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/dinkplugin/notifiers/MockedNotifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ abstract class MockedNotifierTest extends MockedTestBase {
protected ConfigManager configManager = Mockito.mock(ConfigManager.class);

@Bind
protected SettingsManager settingsManager = Mockito.spy(new SettingsManager(gson, client, clientThread, plugin, config, configManager));
protected SettingsManager settingsManager = Mockito.spy(new SettingsManager(gson, client, clientThread, plugin, config, configManager, httpClient));

@Bind
protected DiscordMessageHandler messageHandler = Mockito.spy(new DiscordMessageHandler(gson, client, drawManager, httpClient, config, executor, clientThread, discordService));
Expand Down
Loading