Repository: pajlads/DinkPlugin
Pull request #310 (2023-09-08 18:48:44)

feat(loot): add item allowlist and denylist
Allowlist: useful for bingo events or untradeable drops below gp threshold
Denylist: useful to ignore common drops that exceed gp threshold

![image](https://github.com/pajlads/DinkPlugin/assets/8106344/d2d2008c-8269-4017-8369-e2c352504feb)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 837e90d..ad22a22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Add item name allowlist and denylist for loot notifications. (#310)
 - Minor: Add allowlist mode for Filtered RSNs under the Advanced category. Use this setting with caution. (#306)
 - Minor: Send kill count notifications for Penance Queen kills. (#304)
 - Minor: Include popup notification widget in collection log screenshots. (#309)
diff --git a/README.md b/README.md
index aa52c05..87ec70a 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@ For example: `::dinkexport pet` or `::dinkexport collectionlog`.
 With the output of the above command (`::dinkexport`) copied to your clipboard, you can merge these settings with your own via the `::dinkimport` chat command.
 
 This import can replace all of your notifier settings.
-However, webhook URL lists and filtered RSNs will be combined, rather than outright replaced.
+However, webhook URL lists, filtered RSNs, and filtered item names for the loot notifier would be combined, rather than outright replaced.
 If you would like all settings overwritten rather than merged during import, simply press the `Reset` button at the bottom of the plugin settings panel to clear out all settings (including URLs) before running `::dinkimport`.
 
 After an import, if the dink plugin settings panel was open, simply close and open it for the updated configuration to be reflected in the user interface.
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index 37565f3..e4caeef 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -86,6 +86,7 @@ public class DinkPlugin extends Plugin {
     protected void startUp() {
         log.info("Started up Dink");
         settingsManager.init();
+        lootNotifier.init();
         levelNotifier.initLevels();
     }
 
@@ -123,7 +124,12 @@ public class DinkPlugin extends Plugin {
 
     @Subscribe
     public void onConfigChanged(ConfigChanged event) {
+        if (!SettingsManager.CONFIG_GROUP.equals(event.getGroup())) {
+            return;
+        }
+
         settingsManager.onConfigChanged(event);
+        lootNotifier.onConfigChanged(event.getKey(), event.getNewValue());
     }
 
     @Subscribe
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index 80ce835..f4f30ac 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -12,7 +12,7 @@ import net.runelite.client.config.ConfigSection;
 import net.runelite.client.config.Range;
 import net.runelite.client.config.Units;
 
-@ConfigGroup("dinkplugin")
+@ConfigGroup(SettingsManager.CONFIG_GROUP)
 public interface DinkPluginConfig extends Config {
 
     @ConfigSection(
@@ -764,6 +764,30 @@ public interface DinkPluginConfig extends Config {
         return true;
     }
 
+    @ConfigItem(
+        keyName = "lootItemAllowlist",
+        name = "Item Allowlist",
+        description = "Always fire notifications for these items, despite value settings.<br/>" +
+            "Place one item name per line (case-insensitive)",
+        position = 37,
+        section = lootSection
+    )
+    default String lootItemAllowlist() {
+        return "";
+    }
+
+    @ConfigItem(
+        keyName = "lootItemDenylist",
+        name = "Item Denylist",
+        description = "Never fire notifications for these items, despite value settings.<br/>" +
+            "Place one item name per line (case-insensitive)",
+        position = 38,
+        section = lootSection
+    )
+    default String lootItemDenylist() {
+        return "";
+    }
+
     @ConfigItem(
         keyName = "lootNotifMessage",
         name = "Notification Message",
@@ -771,7 +795,7 @@ public interface DinkPluginConfig extends Config {
             "Use %USERNAME% to insert your username<br/>" +
             "Use %LOOT% to insert the loot<br/>" +
             "Use %SOURCE% to show the source of the loot",
-        position = 37,
+        position = 39,
         section = lootSection
     )
     default String lootNotifyMessage() {
diff --git a/src/main/java/dinkplugin/SettingsManager.java b/src/main/java/dinkplugin/SettingsManager.java
index e187d76..c52172a 100644
--- a/src/main/java/dinkplugin/SettingsManager.java
+++ b/src/main/java/dinkplugin/SettingsManager.java
@@ -49,7 +49,7 @@ import static dinkplugin.util.ConfigUtil.*;
 @Singleton
 @RequiredArgsConstructor(onConstructor_ = { @Inject })
 public class SettingsManager {
-    private static final String CONFIG_GROUP = "dinkplugin";
+    public static final String CONFIG_GROUP = "dinkplugin";
     private static final Set<Integer> PROBLEMATIC_VARBITS;
 
     /**
@@ -153,10 +153,6 @@ public class SettingsManager {
     }
 
     void onConfigChanged(ConfigChanged event) {
-        if (!CONFIG_GROUP.equals(event.getGroup())) {
-            return;
-        }
-
         String key = event.getKey();
         String value = event.getNewValue();
 
@@ -377,7 +373,7 @@ public class SettingsManager {
             Object prevValue = configManager.getConfiguration(CONFIG_GROUP, key, valueType);
             Object newValue;
 
-            if (webhookConfigKeys.contains(key) || "ignoredNames".equals(key)) {
+            if (webhookConfigKeys.contains(key) || "ignoredNames".equals(key) || "lootItemAllowlist".equals(key) || "lootItemDenylist".equals(key)) {
                 // special case: multi-line configs that should be merged (rather than replaced)
                 assert prevValue == null || prevValue instanceof String;
                 Collection<String> lines = readDelimited((String) prevValue).collect(Collectors.toCollection(LinkedHashSet::new));
diff --git a/src/main/java/dinkplugin/notifiers/LootNotifier.java b/src/main/java/dinkplugin/notifiers/LootNotifier.java
index bed5d56..8f8ee75 100644
--- a/src/main/java/dinkplugin/notifiers/LootNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/LootNotifier.java
@@ -12,6 +12,7 @@ import dinkplugin.message.templating.Template;
 import dinkplugin.message.templating.impl.JoiningReplacement;
 import dinkplugin.notifiers.data.LootNotificationData;
 import dinkplugin.notifiers.data.SerializedItemStack;
+import dinkplugin.util.ConfigUtil;
 import dinkplugin.util.ItemUtils;
 import dinkplugin.util.SerializedLoot;
 import dinkplugin.util.Utils;
@@ -42,7 +43,9 @@ import javax.inject.Singleton;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 @Slf4j
@@ -63,6 +66,9 @@ public class LootNotifier extends BaseNotifier {
     @Inject
     private ConfigManager configManager;
 
+    private final Set<String> itemLowerNameAllowlist = new HashSet<>();
+    private final Set<String> itemLowerNameDenylist = new HashSet<>();
+
     private final Cache<String, Integer> killCounts = CacheBuilder.newBuilder()
         .expireAfterAccess(10, TimeUnit.MINUTES)
         .build();
@@ -81,6 +87,42 @@ public class LootNotifier extends BaseNotifier {
         killCounts.invalidateAll();
     }
 
+    public void init() {
+        synchronized (itemLowerNameAllowlist) {
+            itemLowerNameAllowlist.clear();
+            ConfigUtil.readDelimited(config.lootItemAllowlist())
+                .map(String::toLowerCase)
+                .forEach(itemLowerNameAllowlist::add);
+        }
+
+        synchronized (itemLowerNameDenylist) {
+            itemLowerNameDenylist.clear();
+            ConfigUtil.readDelimited(config.lootItemDenylist())
+                .map(String::toLowerCase)
+                .forEach(itemLowerNameDenylist::add);
+        }
+    }
+
+    public void onConfigChanged(String key, String value) {
+        Collection<String> itemNames;
+        if ("lootItemAllowlist".equals(key)) {
+            itemNames = itemLowerNameAllowlist;
+        } else if ("lootItemDenylist".equals(key)) {
+            itemNames = itemLowerNameDenylist;
+        } else {
+            return;
+        }
+
+        synchronized (itemNames) {
+            itemNames.clear();
+            ConfigUtil.readDelimited(value)
+                .map(String::toLowerCase)
+                .forEach(itemNames::add);
+        }
+
+        log.debug("{} is now {}", key, itemNames);
+    }
+
     public void onNpcLootReceived(NpcLootReceived event) {
         if (!isEnabled()) {
             // increment kill count
@@ -172,7 +214,9 @@ public class LootNotifier extends BaseNotifier {
         for (ItemStack item : reduced) {
             SerializedItemStack stack = ItemUtils.stackFromItem(itemManager, item.getId(), item.getQuantity());
             long totalPrice = stack.getTotalPrice();
-            if (totalPrice >= minValue) {
+            String lowerName = stack.getName().toLowerCase();
+            boolean worthy = totalPrice >= minValue || itemLowerNameAllowlist.contains(lowerName);
+            if (worthy && !itemLowerNameDenylist.contains(lowerName)) {
                 sendMessage = true;
                 lootMessage.component(ItemUtils.templateStack(stack, true));
                 if (icons) embeds.add(Embed.ofImage(ItemUtils.getItemImageUrl(item.getId())));
diff --git a/src/main/java/dinkplugin/util/ItemUtils.java b/src/main/java/dinkplugin/util/ItemUtils.java
index 74385da..8406510 100644
--- a/src/main/java/dinkplugin/util/ItemUtils.java
+++ b/src/main/java/dinkplugin/util/ItemUtils.java
@@ -118,7 +118,7 @@ public class ItemUtils {
     public SerializedItemStack stackFromItem(ItemManager itemManager, int id, int quantity) {
         ItemComposition composition = itemManager.getItemComposition(id);
         int price = getPrice(itemManager, id, composition);
-        return new SerializedItemStack(id, quantity, price, composition.getName());
+        return new SerializedItemStack(id, quantity, price, String.valueOf(composition.getMembersName()));
     }
 
     public String formatStack(SerializedItemStack item, boolean includePrice) {


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
index 37ef9e6..c475ec9 100644
--- a/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/LootNotifierTest.java
@@ -106,6 +106,45 @@ class LootNotifierTest extends MockedNotifierTest {
         );
     }
 
+    @Test
+    void testNotifyAllowlist() {
+        // prepare mocks
+        Player player = mock(Player.class);
+        when(player.getName()).thenReturn(LOOTED_NAME);
+
+        // fire event
+        notifier.onConfigChanged("lootItemAllowlist", "salmon\nraw trout\ntuna\npike");
+        PlayerLootReceived event = new PlayerLootReceived(player, Collections.singletonList(new ItemStack(ItemID.TUNA, 1, null)));
+        plugin.onPlayerLootReceived(event);
+
+        // verify notification message
+        verify(messageHandler).createMessage(
+            PRIMARY_WEBHOOK_URL,
+            false,
+            NotificationBody.builder()
+                .text(
+                    Template.builder()
+                        .template(String.format("%s has looted: 1 x {{tuna}} (%d) from %s for %d gp", PLAYER_NAME, TUNA_PRICE, LOOTED_NAME, TUNA_PRICE))
+                        .replacement("{{tuna}}", Replacements.ofWiki("Tuna"))
+                        .build()
+                )
+                .extra(new LootNotificationData(Collections.singletonList(new SerializedItemStack(ItemID.TUNA, 1, TUNA_PRICE, "Tuna")), LOOTED_NAME, LootRecordType.PLAYER, null))
+                .type(NotificationType.LOOT)
+                .build()
+        );
+    }
+
+    @Test
+    void testIgnoreDenylist() {
+        // fire event
+        notifier.onConfigChanged("lootItemDenylist", "Ruby");
+        LootReceived event = new LootReceived(LOOTED_NAME, 99, LootRecordType.PICKPOCKET, Collections.singletonList(new ItemStack(ItemID.RUBY, 1, null)), 1);
+        plugin.onLootReceived(event);
+
+        // ensure no notification
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     @DisplayName("Ensure LootReceived event for The Whisperer fires a notification - https://github.com/pajlads/DinkPlugin/pull/286")
     void testNotifyWhisperer() {