From 41472981207d84985d6a8fa89167c675d25faf86 Mon Sep 17 00:00:00 2001
From: Dragory <2606411+Dragory@users.noreply.github.com>
Date: Fri, 2 Apr 2021 17:44:21 +0300
Subject: [PATCH] Add counter documentation/examples. Tweak counter
 triggers/actions in automod.

Rename change_counter automod action to add_to_counter,
add set_counter action, rename counter trigger to counter_trigger.
---
 .../{changeCounter.ts => addToCounter.ts}     |  15 +-
 .../Automod/actions/availableActions.ts       |   9 +-
 .../src/plugins/Automod/actions/setCounter.ts |  22 ++
 .../Automod/triggers/availableTriggers.ts     |   6 +-
 .../{counter.ts => counterTrigger.ts}         |   0
 .../src/plugins/Counters/CountersPlugin.ts    |   8 +
 dashboard/src/components/docs/Counters.vue    | 295 ++++++++++++++++++
 dashboard/src/components/docs/DocsLayout.vue  |   4 +
 dashboard/src/routes.ts                       |   4 +
 9 files changed, 347 insertions(+), 16 deletions(-)
 rename backend/src/plugins/Automod/actions/{changeCounter.ts => addToCounter.ts} (62%)
 create mode 100644 backend/src/plugins/Automod/actions/setCounter.ts
 rename backend/src/plugins/Automod/triggers/{counter.ts => counterTrigger.ts} (100%)
 create mode 100644 dashboard/src/components/docs/Counters.vue

diff --git a/backend/src/plugins/Automod/actions/changeCounter.ts b/backend/src/plugins/Automod/actions/addToCounter.ts
similarity index 62%
rename from backend/src/plugins/Automod/actions/changeCounter.ts
rename to backend/src/plugins/Automod/actions/addToCounter.ts
index a3db8d31..fe293e4c 100644
--- a/backend/src/plugins/Automod/actions/changeCounter.ts
+++ b/backend/src/plugins/Automod/actions/addToCounter.ts
@@ -2,26 +2,21 @@ import * as t from "io-ts";
 import { automodAction } from "../helpers";
 import { CountersPlugin } from "../../Counters/CountersPlugin";
 
-export const ChangeCounterAction = automodAction({
+export const AddToCounterAction = automodAction({
   configType: t.type({
-    name: t.string,
-    change: t.string,
+    counter: t.string,
+    amount: t.number,
   }),
 
   defaultConfig: {},
 
   async apply({ pluginData, contexts, actionConfig, matchResult }) {
-    const change = parseInt(actionConfig.change, 10);
-    if (Number.isNaN(change)) {
-      throw new Error("Invalid change number");
-    }
-
     const countersPlugin = pluginData.getPlugin(CountersPlugin);
     countersPlugin.changeCounterValue(
-      actionConfig.name,
+      actionConfig.counter,
       contexts[0].message?.channel_id || null,
       contexts[0].user?.id || null,
-      change,
+      actionConfig.amount,
     );
   },
 });
diff --git a/backend/src/plugins/Automod/actions/availableActions.ts b/backend/src/plugins/Automod/actions/availableActions.ts
index 21ebe945..7857db6f 100644
--- a/backend/src/plugins/Automod/actions/availableActions.ts
+++ b/backend/src/plugins/Automod/actions/availableActions.ts
@@ -12,7 +12,8 @@ import { AddRolesAction } from "./addRoles";
 import { RemoveRolesAction } from "./removeRoles";
 import { SetAntiraidLevelAction } from "./setAntiraidLevel";
 import { ReplyAction } from "./reply";
-import { ChangeCounterAction } from "./changeCounter";
+import { AddToCounterAction } from "./addToCounter";
+import { SetCounterAction } from "./setCounter";
 
 export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
   clean: CleanAction,
@@ -27,7 +28,8 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
   remove_roles: RemoveRolesAction,
   set_antiraid_level: SetAntiraidLevelAction,
   reply: ReplyAction,
-  change_counter: ChangeCounterAction,
+  add_to_counter: AddToCounterAction,
+  set_counter: SetCounterAction,
 };
 
 export const AvailableActions = t.type({
@@ -43,5 +45,6 @@ export const AvailableActions = t.type({
   remove_roles: RemoveRolesAction.configType,
   set_antiraid_level: SetAntiraidLevelAction.configType,
   reply: ReplyAction.configType,
-  change_counter: ChangeCounterAction.configType,
+  add_to_counter: AddToCounterAction.configType,
+  set_counter: SetCounterAction.configType,
 });
diff --git a/backend/src/plugins/Automod/actions/setCounter.ts b/backend/src/plugins/Automod/actions/setCounter.ts
new file mode 100644
index 00000000..0dbbaa37
--- /dev/null
+++ b/backend/src/plugins/Automod/actions/setCounter.ts
@@ -0,0 +1,22 @@
+import * as t from "io-ts";
+import { automodAction } from "../helpers";
+import { CountersPlugin } from "../../Counters/CountersPlugin";
+
+export const SetCounterAction = automodAction({
+  configType: t.type({
+    counter: t.string,
+    value: t.number,
+  }),
+
+  defaultConfig: {},
+
+  async apply({ pluginData, contexts, actionConfig, matchResult }) {
+    const countersPlugin = pluginData.getPlugin(CountersPlugin);
+    countersPlugin.setCounterValue(
+      actionConfig.counter,
+      contexts[0].message?.channel_id || null,
+      contexts[0].user?.id || null,
+      actionConfig.value,
+    );
+  },
+});
diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts
index 175df6d7..18c671b6 100644
--- a/backend/src/plugins/Automod/triggers/availableTriggers.ts
+++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts
@@ -17,7 +17,7 @@ import { MemberJoinTrigger } from "./memberJoin";
 import { RoleAddedTrigger } from "./roleAdded";
 import { RoleRemovedTrigger } from "./roleRemoved";
 import { StickerSpamTrigger } from "./stickerSpam";
-import { CounterTrigger } from "./counter";
+import { CounterTrigger } from "./counterTrigger";
 import { NoteTrigger } from "./note";
 import { WarnTrigger } from "./warn";
 import { MuteTrigger } from "./mute";
@@ -46,7 +46,7 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
   member_join_spam: MemberJoinSpamTrigger,
   sticker_spam: StickerSpamTrigger,
 
-  counter: CounterTrigger,
+  counter_trigger: CounterTrigger,
 
   note: NoteTrigger,
   warn: WarnTrigger,
@@ -77,7 +77,7 @@ export const AvailableTriggers = t.type({
   member_join_spam: MemberJoinSpamTrigger.configType,
   sticker_spam: StickerSpamTrigger.configType,
 
-  counter: CounterTrigger.configType,
+  counter_trigger: CounterTrigger.configType,
 
   note: NoteTrigger.configType,
   warn: WarnTrigger.configType,
diff --git a/backend/src/plugins/Automod/triggers/counter.ts b/backend/src/plugins/Automod/triggers/counterTrigger.ts
similarity index 100%
rename from backend/src/plugins/Automod/triggers/counter.ts
rename to backend/src/plugins/Automod/triggers/counterTrigger.ts
diff --git a/backend/src/plugins/Counters/CountersPlugin.ts b/backend/src/plugins/Counters/CountersPlugin.ts
index b6789e81..68a5bcea 100644
--- a/backend/src/plugins/Counters/CountersPlugin.ts
+++ b/backend/src/plugins/Counters/CountersPlugin.ts
@@ -102,6 +102,14 @@ const configPreprocessor: ConfigPreprocessorFn<CountersPluginType> = options =>
  * After being triggered, a trigger is "reset" if the counter value no longer matches the trigger (e.g. drops to 100 or below in the above example). After this, that trigger can be triggered again.
  */
 export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counters", {
+  showInDocs: true,
+  info: {
+    prettyName: "Counters",
+    description:
+      "Keep track of per-user, per-channel, or global numbers and trigger specific actions based on this number",
+    configurationGuide: "See <a href='/docs/setup-guides/counters'>Counters setup guide</a>",
+  },
+
   configSchema: ConfigSchema,
   defaultOptions,
   configPreprocessor,
diff --git a/dashboard/src/components/docs/Counters.vue b/dashboard/src/components/docs/Counters.vue
new file mode 100644
index 00000000..c8af5159
--- /dev/null
+++ b/dashboard/src/components/docs/Counters.vue
@@ -0,0 +1,295 @@
+<template>
+  <div>
+    <h1>Counters</h1>
+    <p>
+      Counters are an advanced feature in Zeppelin that allows you keep track of per-user, per-channel, or global numbers and trigger specific actions based on this number.
+      Common use cases are infraction points, XP systems, activity roles, and so on.
+    </p>
+    <p>
+      This guide will be expanded in the future. For now, it contains examples of common counter use cases.
+      Also see the <router-link to="/docs/plugins/counters">documentation for the Counters plugin.</router-link>
+    </p>
+
+    <h2>Examples</h2>
+
+    <h3>Infraction points</h3>
+    <p>
+      In this example, warns, mutes, and kicks all accumulate "infraction points" for a user.
+      When the user reaches too many points, they are automatically banned.
+    </p>
+
+    <Expandable class="wide">
+      <template v-slot:title>Click to view example</template>
+      <template v-slot:content>
+        <CodeBlock>
+          plugins:
+            counters:
+              config:
+                counters:
+
+                  infraction_points:
+                    per_user: true
+                    triggers:
+                      # When a user accumulates 50 or more (>=50) infraction points, this trigger will activate.
+                      # The numbers here are arbitrary - you could choose to use 5 or 500 instead, depending on the granularity you want.
+                      autoban:
+                        condition: ">=50"
+                    # Remove 1 infraction point each day
+                    decay:
+                      amount: 1
+                      every: 24h
+
+            automod:
+              config:
+                rules:
+
+                  add_infraction_points_on_warn:
+                    triggers:
+                      - warn: {}
+                    actions:
+                      add_to_counter:
+                        counter: "infraction_points"
+                        amount: 10
+
+                  add_infraction_points_on_mute:
+                    triggers:
+                      - mute: {}
+                    actions:
+                      add_to_counter:
+                        counter: "infraction_points"
+                        amount: 20
+
+                  add_infraction_points_on_kick:
+                    triggers:
+                      - kick: {}
+                    actions:
+                      add_to_counter:
+                        counter: "infraction_points"
+                        amount: 40
+
+                  autoban_on_too_many_infraction_points:
+                    triggers:
+                      # The counter trigger we specified further above, "autoban", is used to trigger an automod rule here
+                      - counter_trigger:
+                          counter: "infraction_points"
+                          trigger: "autoban"
+                    actions:
+                      ban:
+                        reason: "Too many infraction points"
+        </CodeBlock>
+      </template>
+    </Expandable>
+
+    <h3>Escalating automod punishments</h3>
+    <p>
+      This example allows users to trigger the `some_infraction` automod rule 3 times. On the 4th time, they are automatically muted.
+    </p>
+
+    <Expandable class="wide">
+      <template v-slot:title>Click to view example</template>
+      <template v-slot:content>
+        <CodeBlock code-lang="yaml">
+          plugins:
+            counters:
+              config:
+                counters:
+
+                  automod_infractions:
+                    per_user: true
+                    triggers:
+                      # When a user accumulates 100 or more (>=100) automod infraction points, this trigger will activate
+                      # The numbers here are arbitrary - you could choose to use 10 or 1000 instead.
+                      too_many_infractions:
+                        condition: ">=100"
+                    # Remove 100 automod infraction points per hour
+                    decay:
+                      amount: 100
+                      every: 1h
+
+            automod:
+              config:
+                rules:
+
+                  # An example automod rule that adds automod infraction points
+                  some_infraction:
+                    triggers:
+                      - match_words:
+                          words: ['poopoo head']
+
+                    actions:
+                      clean: true
+                      reply: 'Do not insult other users'
+                      add_to_counter:
+                        counter: "automod_infractions"
+                        amount: 25 # This infraction adds 25 automod infraction points
+
+                  # An example rule that is triggered when the user accumulates too many automod infraction points
+                  automute_on_too_many_infractions:
+                    triggers:
+                      - counter_trigger:
+                          counter: "automod_infractions"
+                          trigger: "too_many_infractions"
+
+                    actions:
+                      mute:
+                        reason: "You have been muted for tripping too many automod filters"
+                        remove_roles_on_mute: true
+                        restore_roles_on_mute: true
+        </CodeBlock>
+      </template>
+    </Expandable>
+
+    <h3>Simple XP system</h3>
+    <p>
+      This example creates an XP system where every message sent grants you 1 XP, max once per minute.
+      At 100, 250, 500, and 1000 XP the system grants the user a new role.
+    </p>
+
+    <Expandable class="wide">
+      <template v-slot:title>Click to view example</template>
+      <template v-slot:content>
+        <CodeBlock>
+          plugins:
+            counters:
+              config:
+                counters:
+                  xp:
+                    per_user: true
+                    triggers:
+                      role_1:
+                        condition: ">=100"
+                      role_2:
+                        condition: ">=250"
+                      role_3:
+                        condition: ">=500"
+                      role_4:
+                        condition: ">=1000"
+
+            automod:
+              config:
+                rules:
+
+                  accumulate_xp:
+                    triggers:
+                      - any_message: {}
+
+                    actions:
+                      log: false # Don't spam logs with XP changes
+                      add_to_counter:
+                        counter: "xp"
+                        amount: 1 # Each message adds 1 XP
+
+                    cooldown: 1m # Only count 1 message per minute
+
+                  add_xp_role_1:
+                    triggers:
+                      - counter_trigger:
+                          counter: "xp"
+                          trigger: "role_1"
+
+                    actions:
+                      add_roles: ["123456789123456789"] # Role ID for xp role 1
+
+                  add_xp_role_2:
+                    triggers:
+                      - counter_trigger:
+                          counter: "xp"
+                          trigger: "role_2"
+
+                    actions:
+                      add_roles: ["123456789123456789"] # Role ID for xp role 2
+
+                  add_xp_role_3:
+                    triggers:
+                      - counter_trigger:
+                          counter: "xp"
+                          trigger: "role_3"
+
+                    actions:
+                      add_roles: ["123456789123456789"] # Role ID for xp role 3
+
+                  add_xp_role_4:
+                    triggers:
+                      - counter_trigger:
+                          counter: "xp"
+                          trigger: "role_4"
+
+                    actions:
+                      add_roles: ["123456789123456789"] # Role ID for xp role 4
+        </CodeBlock>
+      </template>
+    </Expandable>
+
+    <h3>Activity role ("regular role")</h3>
+    <p>
+      This example is similar to the XP system, but the number decays and the role granted by the system can be removed if the user's activity goes down.
+    </p>
+
+    <Expandable class="wide">
+      <template v-slot:title>Click to view example</template>
+      <template v-slot:content>
+        <CodeBlock code-lang="yaml">
+          plugins:
+            counters:
+              config:
+                counters:
+                  activity:
+                    per_user: true
+                    triggers:
+                      grant_role:
+                        condition: ">=100"
+                        # We set a separate threshold for when the role should be removed. This is so the decay doesn't remove the activity role immediately.
+                        # If this value isn't set, reverse_condition defaults to the opposite of the condition, i.e. "<100" in this case.
+                        reverse_condition: "<50"
+                    decay:
+                      amount: 1
+                      every: 1h
+
+            automod:
+              config:
+                rules:
+
+                  accumulate_activity:
+                    triggers:
+                      - any_message: {}
+
+                    actions:
+                      log: false # Don't spam logs with activity changes
+                      add_to_counter:
+                        counter: "activity"
+                        amount: 1 # Each message adds 1 to the counter
+
+                    cooldown: 1m # Only count 1 message per minute
+
+                  grant_activity_role:
+                    triggers:
+                      - counter_trigger:
+                          counter: "activity"
+                          trigger: "grant_role"
+
+                    actions:
+                      add_roles: ["123456789123456789"] # Role ID for activity role
+
+                  remove_activity_role:
+                    triggers:
+                      - counter_trigger:
+                          counter: "activity"
+                          trigger: "grant_role"
+                          reverse: true # This indicates we want to use the *reverse* of the specified trigger, see reverse_condition in counters above
+
+                    actions:
+                      remove_roles: ["123456789123456789"] # Role ID for activity role
+        </CodeBlock>
+      </template>
+    </Expandable>
+  </div>
+</template>
+
+<script>
+import CodeBlock from "./CodeBlock";
+import Expandable from "../Expandable";
+
+export default {
+  components: { CodeBlock, Expandable },
+};
+</script>
diff --git a/dashboard/src/components/docs/DocsLayout.vue b/dashboard/src/components/docs/DocsLayout.vue
index b508ef46..e8657d8a 100644
--- a/dashboard/src/components/docs/DocsLayout.vue
+++ b/dashboard/src/components/docs/DocsLayout.vue
@@ -115,6 +115,10 @@
           to: '/docs/setup-guides/moderation',
           label: 'Moderation',
         },
+        {
+          to: '/docs/setup-guides/counters',
+          label: 'Counters',
+        },
       ],
     },
   ];
diff --git a/dashboard/src/routes.ts b/dashboard/src/routes.ts
index c38d1f7e..4ef8e996 100644
--- a/dashboard/src/routes.ts
+++ b/dashboard/src/routes.ts
@@ -53,6 +53,10 @@ export const router = new VueRouter({
           path: "setup-guides/moderation",
           component: () => import("./components/docs/WorkInProgress.vue"),
         },
+        {
+          path: "setup-guides/counters",
+          component: () => import("./components/docs/Counters.vue"),
+        },
         {
           path: "plugins/:pluginName/:tab?",
           component: () => import("./components/docs/Plugin.vue"),