Repository: pajlads/DinkPlugin (java)

Issue:
Setting to skip low tier clue notifications
Similar to the "Min Slayer Points" threshold, it might be useful to filter out low level clues.
Maybe something like "The minimum level Clue Casket tier to warrant a notification": ["Beginner", "Easy", "Medium", "Hard", "Elite", "Master"] where "Beginner" is the default (i.e. notify for all tier caskets)

There's a chance this setting is too extravagant, since minimum loot value threshold might be enough for people.

Not a request from me, but it came up briefly at @Supinic's stream, so I thought I'd get people's opinions here.

Pull request description:
feat: add min clue tier setting
Closes #120


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a482fa5..750b1e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Add setting to skip notifications for low tier clue scrolls. (#134)
 - Minor: Add embed field on a max level up indicating total 99+ skills. (#115)
 - Minor: Include all lost items in extra death notification data. (#132)
 - Minor: Reduce ticks to initialize diary notifier and improve logging for edge cases. (#129)
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index a873a56..5b7aa9c 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -1,6 +1,7 @@
 package dinkplugin;
 
 import dinkplugin.domain.AchievementDiary;
+import dinkplugin.domain.ClueTier;
 import dinkplugin.domain.CombatAchievementTier;
 import dinkplugin.domain.PlayerLookupService;
 import net.runelite.client.config.Config;
@@ -720,7 +721,7 @@ public interface DinkPluginConfig extends Config {
     @ConfigItem(
         keyName = "clueShowItems",
         name = "Show Item Icons",
-        description = "Show item icons gained from the clue",
+        description = "Show item icons gained from the clue as embeds",
         position = 72,
         section = clueSection
     )
@@ -728,11 +729,22 @@ public interface DinkPluginConfig extends Config {
         return false;
     }
 
+    @ConfigItem(
+        keyName = "clueMinTier",
+        name = "Min Tier",
+        description = "The minimum tier of the clue scroll for a notification to be sent",
+        position = 73,
+        section = clueSection
+    )
+    default ClueTier clueMinTier() {
+        return ClueTier.BEGINNER;
+    }
+
     @ConfigItem(
         keyName = "clueMinValue",
         name = "Min Value",
         description = "The minimum value of the combined items for a notification to be sent",
-        position = 73,
+        position = 74,
         section = clueSection
     )
     default int clueMinValue() {
@@ -743,7 +755,7 @@ public interface DinkPluginConfig extends Config {
         keyName = "clueImageMinValue",
         name = "Screenshot Min Value",
         description = "The minimum combined value of the items to send a screenshot. Must have 'Send Image' enabled",
-        position = 74,
+        position = 75,
         section = clueSection
     )
     default int clueImageMinValue() {
@@ -754,7 +766,7 @@ public interface DinkPluginConfig extends Config {
         keyName = "clueNotifMessage",
         name = "Notification Message",
         description = "The message to be sent through the webhook. Use %USERNAME% to insert your username, %CLUE% to insert the clue type, %LOOT% to show the loot obtained and %COUNT% to insert how many of those clue types you have completed",
-        position = 75,
+        position = 76,
         section = clueSection
     )
     default String clueNotifyMessage() {
diff --git a/src/main/java/dinkplugin/domain/ClueTier.java b/src/main/java/dinkplugin/domain/ClueTier.java
new file mode 100644
index 0000000..b57aa88
--- /dev/null
+++ b/src/main/java/dinkplugin/domain/ClueTier.java
@@ -0,0 +1,35 @@
+package dinkplugin.domain;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public enum ClueTier {
+    BEGINNER,
+    EASY,
+    MEDIUM,
+    HARD,
+    ELITE,
+    MASTER;
+
+    private static final Map<String, ClueTier> MAPPINGS;
+    private final String displayName = this.name().charAt(0) + this.name().substring(1).toLowerCase();
+
+    @Override
+    public String toString() {
+        return this.displayName;
+    }
+
+    @Nullable
+    public static ClueTier parse(@NotNull String tier) {
+        return MAPPINGS.get(tier.toUpperCase());
+    }
+
+    static {
+        MAPPINGS = Arrays.stream(values()).collect(Collectors.toMap(Enum::name, Function.identity()));
+    }
+}
diff --git a/src/main/java/dinkplugin/notifiers/ClueNotifier.java b/src/main/java/dinkplugin/notifiers/ClueNotifier.java
index 39d9544..da43ec4 100644
--- a/src/main/java/dinkplugin/notifiers/ClueNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/ClueNotifier.java
@@ -1,5 +1,6 @@
 package dinkplugin.notifiers;
 
+import dinkplugin.domain.ClueTier;
 import dinkplugin.message.Embed;
 import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
@@ -7,6 +8,7 @@ import dinkplugin.util.ItemUtils;
 import dinkplugin.util.Utils;
 import dinkplugin.notifiers.data.ClueNotificationData;
 import dinkplugin.notifiers.data.SerializedItemStack;
+import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.events.WidgetLoaded;
 import net.runelite.api.widgets.Widget;
 import net.runelite.api.widgets.WidgetID;
@@ -26,6 +28,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+@Slf4j
 @Singleton
 public class ClueNotifier extends BaseNotifier {
     private static final Pattern CLUE_SCROLL_REGEX = Pattern.compile("You have completed (?<scrollCount>\\d+) (?<scrollType>\\w+) Treasure Trails\\.");
@@ -50,9 +53,12 @@ public class ClueNotifier extends BaseNotifier {
         if (isEnabled()) {
             Matcher clueMatcher = CLUE_SCROLL_REGEX.matcher(chatMessage);
             if (clueMatcher.find()) {
-                // game message always occurs before widget load; save this data
-                this.clueCount = clueMatcher.group("scrollCount");
-                this.clueType = clueMatcher.group("scrollType");
+                String tier = clueMatcher.group("scrollType");
+                if (checkClueTier(tier)) {
+                    // game message always occurs before widget load; save this data
+                    this.clueCount = clueMatcher.group("scrollCount");
+                    this.clueType = tier;
+                }
             }
         }
     }
@@ -130,6 +136,15 @@ public class ClueNotifier extends BaseNotifier {
         return String.format("%s x %s (%s)", item.getQuantity(), item.getName(), QuantityFormatter.quantityToStackSize(item.getTotalPrice()));
     }
 
+    private boolean checkClueTier(String clueType) {
+        ClueTier tier = ClueTier.parse(clueType);
+        if (tier == null) {
+            log.warn("Failed to parse clue tier: {}", clueType);
+            return true; // permissive approach
+        }
+        return tier.ordinal() >= config.clueMinTier().ordinal();
+    }
+
     public void reset() {
         this.clueCount = "";
         this.clueType = "";


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/ClueNotifierTest.java b/src/test/java/dinkplugin/notifiers/ClueNotifierTest.java
index b52bd70..bd5ce48 100644
--- a/src/test/java/dinkplugin/notifiers/ClueNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/ClueNotifierTest.java
@@ -1,6 +1,7 @@
 package dinkplugin.notifiers;
 
 import com.google.inject.testing.fieldbinder.Bind;
+import dinkplugin.domain.ClueTier;
 import dinkplugin.message.NotificationBody;
 import dinkplugin.message.NotificationType;
 import dinkplugin.notifiers.data.ClueNotificationData;
@@ -41,6 +42,7 @@ class ClueNotifierTest extends MockedNotifierTest {
         when(config.notifyClue()).thenReturn(true);
         when(config.clueSendImage()).thenReturn(false);
         when(config.clueShowItems()).thenReturn(false);
+        when(config.clueMinTier()).thenReturn(ClueTier.MEDIUM);
         when(config.clueMinValue()).thenReturn(500);
         when(config.clueNotifyMessage()).thenReturn("%USERNAME% has completed a %CLUE% clue, for a total of %COUNT%. They obtained: %LOOT%");
 
@@ -82,6 +84,31 @@ class ClueNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testIgnoreTier() {
+        // fire chat event
+        notifier.onChatMessage("You have completed 1312 beginner Treasure Trails.");
+
+        // mock widgets
+        Widget widget = mock(Widget.class);
+        when(client.getWidget(WidgetInfo.CLUE_SCROLL_REWARD_ITEM_CONTAINER)).thenReturn(widget);
+
+        Widget child = mock(Widget.class);
+        when(child.getItemQuantity()).thenReturn(1);
+        when(child.getItemId()).thenReturn(ItemID.RUBY);
+
+        Widget[] children = { child };
+        when(widget.getChildren()).thenReturn(children);
+
+        // fire widget event
+        WidgetLoaded event = new WidgetLoaded();
+        event.setGroupId(WidgetID.CLUE_SCROLL_REWARD_GROUP_ID);
+        plugin.onWidgetLoaded(event);
+
+        // ensure no notification was fired
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testIgnoreLoot() {
         // fire chat event