Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7cebc013b | ||
|
|
f4103206db | ||
|
|
c9b1bfed40 | ||
|
|
7f764b4d99 | ||
|
|
fb7ddbba70 | ||
|
|
85b1d13718 | ||
|
|
7f2191a11a | ||
|
|
9b79bdbdd5 | ||
|
|
6b9852cc14 | ||
|
|
fbf627c971 | ||
|
|
b2077132cf | ||
|
|
f622c3ee03 | ||
|
|
ab83f3ed0c | ||
|
|
a021b503a0 | ||
|
|
d28714aacc | ||
|
|
7632a66250 | ||
|
|
bb6936d657 | ||
|
|
d4062b679a | ||
|
|
313ee0a9a3 | ||
|
|
7afc384d17 | ||
|
|
fea1f240dd | ||
|
|
1dba0e857f | ||
|
|
0966aa689f | ||
|
|
138e237fbc | ||
|
|
6b38ec1669 | ||
|
|
ae8e11feb4 | ||
|
|
5cd415e300 | ||
|
|
7cdaa4bf25 | ||
|
|
280ddf583b | ||
|
|
4969cafc97 | ||
|
|
5f6e63542b | ||
|
|
bca9c96468 | ||
|
|
7569c06a36 | ||
|
|
88bafbc1ac | ||
|
|
a5acd6ec83 | ||
|
|
d93c8bdef2 | ||
|
|
8a32bd6485 | ||
|
|
425cbc4826 | ||
|
|
3a2d3f5047 | ||
|
|
ae20b85400 | ||
|
|
e993c5d376 | ||
|
|
4f9d1c1ca1 | ||
|
|
372bae0e03 | ||
|
|
6f35ec3705 | ||
|
|
a542d80c1d | ||
|
|
9dcf256aa1 | ||
|
|
da206f41ad | ||
|
|
550beb9baf | ||
|
|
7f9adcef36 | ||
|
|
f24eb52697 | ||
|
|
60dbc42148 | ||
|
|
8d9fb29848 | ||
|
|
f7a7e817f9 | ||
|
|
e09cab6872 | ||
|
|
f1797f29fd | ||
|
|
4eae07f831 | ||
|
|
63696b746e |
@@ -109,7 +109,7 @@ CM comes equipped with a dashboard designed for use by both moderators and bot o
|
||||
* View **real-time logs** of what the bot is doing on your subreddit
|
||||
* **Run bot on any permalink**
|
||||
|
||||

|
||||

|
||||
|
||||
### Bot Setup/Authentication
|
||||
|
||||
@@ -117,11 +117,11 @@ A bot oauth helper allows operators to define oauth credentials/permissions and
|
||||
|
||||
Operator view/invite link generation:
|
||||
|
||||

|
||||

|
||||
|
||||
Moderator view/invite and authorization:
|
||||
|
||||

|
||||

|
||||
|
||||
### Configuration Editor
|
||||
|
||||
@@ -134,7 +134,7 @@ A built-in editor using [monaco-editor](https://microsoft.github.io/monaco-edito
|
||||
* Authenticated view loads subreddit configurations by simple link found on the subreddit dashboard
|
||||
* Switch schemas to edit either subreddit or operator configurations
|
||||
|
||||

|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* [Getting Started](#getting-started)
|
||||
* [How It Works](#how-it-works)
|
||||
* [Concepts](#concepts)
|
||||
* [Run](#runs)
|
||||
* [Check](#checks)
|
||||
* [Rule](#rule)
|
||||
* [Examples](#available-rules)
|
||||
@@ -36,31 +37,61 @@ Review **at least** the **How It Works** and **Concepts** below, then:
|
||||
|
||||
Where possible Context Mod (CM) uses the same terminology as, and emulates the behavior, of **automoderator** so if you are familiar with that much of this may seem familiar to you.
|
||||
|
||||
### Diagram
|
||||
|
||||
Expand the section below for a simplified flow diagram of how CM processes an incoming Activity. Then refer the text description of the diagram below as well as [Concepts](#Concepts) for descriptions of individual components.
|
||||
|
||||
<details>
|
||||
<summary>Diagram</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
CM's lifecycle looks like this:
|
||||
|
||||
#### 1) A new event in your subreddit is received by CM
|
||||
|
||||
The events CM watches for are configured by you. These can be new modqueue/unmoderated items, submissions, or comments.
|
||||
|
||||
#### 2) CM sequentially processes each Check in your configuration
|
||||
#### 2) CM sequentially processes each Run in your configuration
|
||||
|
||||
A [**Run**](#Runs) is made up of a set of [**Checks**](#Checks)
|
||||
|
||||
#### 3) CM sequentially processes each Check in the current Run
|
||||
|
||||
A **Check** is a set of:
|
||||
|
||||
* One or more **Rules** that define what conditions should **trigger** this Check
|
||||
* One or more **Actions** that define what the bot should do once the Check is **triggered**
|
||||
* One or more [**Rules**](#Rule) that define what conditions should **trigger** this Check
|
||||
* One or more [**Actions**](#Action) that define what the bot should do once the Check is **triggered**
|
||||
|
||||
#### 3) Each Check is processed, *in order*, until a Check is triggered
|
||||
#### 4) Each Check is processed, *in order*, until a Check is **triggered**
|
||||
|
||||
Once a Check is **triggered** no more Checks will be processed. This means all subsequent Checks in your configuration (in the order you listed them) are basically skipped.
|
||||
In CM's default configuration, once a Check is **triggered** no more Checks will be processed. This means all subsequent Checks in this Run (in the order you listed them) are skipped.
|
||||
|
||||
#### 4) All Actions from that Check are executed
|
||||
#### 5) All Actions from the triggered Check are executed
|
||||
|
||||
After all Actions are executed CM returns to waiting for the next Event.
|
||||
After all **Actions** from the triggered **Check** are executed CM begins processing the next **Run**
|
||||
|
||||
#### 6) Rinse and Repeat from #3
|
||||
|
||||
Until all Runs have been processed.
|
||||
|
||||
## Concepts
|
||||
|
||||
Core, high-level concepts regarding how CM works.
|
||||
|
||||
### Runs
|
||||
|
||||
A **Run** is made up of a set of **Checks** that represent a group of related behaviors the bot should check for or perform -- that are independent of any other behaviors the Bot should perform.
|
||||
|
||||
An example of Runs:
|
||||
|
||||
* A group of Checks that look for missing flairs on a user or a new submission and flair accordingly
|
||||
* A group of Checks that detect spam or self-promotion and then remove those activities
|
||||
|
||||
Both group of Checks are independent of each other (don't have any patterns or actions in common). Learn more about using [Runs and **Flow Control** to control how CM behaves.](/docs/examples/advancedConcepts/flowControl.md)
|
||||
|
||||
### Checks
|
||||
|
||||
A **Check** is the main logical unit of behavior for the bot. It is equivalent to "if X then Y". A Check is comprised of:
|
||||
@@ -68,7 +99,7 @@ A **Check** is the main logical unit of behavior for the bot. It is equivalent t
|
||||
* One or more **Rules** that are tested against an **Activity**
|
||||
* One of more **Actions** that are performed when the **Rules** are satisfied
|
||||
|
||||
The bot's configuration can be made up of one or more **Checks** that are processed **in the order they are listed in the configuration.**
|
||||
A Run can be made up of one or more **Checks** that are processed **in the order they are listed in the Run.**
|
||||
|
||||
Once a Check is **triggered** (its Rules are satisfied and Actions performed) all subsequent Checks are skipped.
|
||||
|
||||
@@ -87,7 +118,7 @@ A **Rule** is some set of **criteria** (conditions) that are tested against an A
|
||||
|
||||
There are generally three main properties for a Rule:
|
||||
|
||||
* **Critiera** -- The conditions/values you want to test for.
|
||||
* **Criteria** -- The conditions/values you want to test for.
|
||||
* **Activities Window** -- If applicable, the range of activities that the **criteria** will be tested against.
|
||||
* **Rule-specific options** -- Any number of options that modify how the **criteria** are tested.
|
||||
|
||||
@@ -149,6 +180,7 @@ An **Action** is some action the bot can take against the checked Activity (comm
|
||||
|
||||
* Remove (Comment/Submission)
|
||||
* Flair (Submission)
|
||||
* User Flair (Submission/Comment)
|
||||
* Ban (User)
|
||||
* Approve (Comment/Submission)
|
||||
* Comment (Reply to Comment/Submission)
|
||||
@@ -160,12 +192,12 @@ For detailed explanation and options of what individual Actions can do [see the
|
||||
|
||||
### Filters
|
||||
|
||||
**Checks, Rules, and Actions** all have two additional (optional) criteria "tests". These tests behave differently than rule/check triggers in that:
|
||||
**Runs, Checks, Rules, and Actions** all have two additional (optional) criteria "tests". These tests behave differently than rule/check triggers in that:
|
||||
|
||||
* When they **pass** the thing being tested continues to process as usual
|
||||
* When they **fail** the thing being tested **is skipped, not failed.**
|
||||
|
||||
For **Checks** and **Actions** skipping means that the thing is not processed. The Action is not run, the Check is not triggered.
|
||||
For **Runs**, **Checks**, and **Actions** skipping means that the thing is not processed. The Action is not run, the Check is not triggered.
|
||||
|
||||
In the context of **Rules** (in a Check) skipping means the Rule does not get run BUT it does not fail. The Check will continue processing as if the Rule did not exist. However, if ALL Rules in a Check are skipped then the Check does "fail" (is not triggered).
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ This directory contains example of valid, ready-to-go configurations for Context
|
||||
* [Author and post flairs](/docs/examples/onlyfansFlair)
|
||||
* [Toolbox User Notes](/docs/examples/userNotes)
|
||||
* [Advanced Concepts](/docs/examples/advancedConcepts)
|
||||
* [Flow Control](/docs/examples/advancedConcepts/flowControl.md)
|
||||
* [Rule Sets](/docs/examples/advancedConcepts/ruleSets.json5)
|
||||
* [Name Rules](/docs/examples/advancedConcepts/ruleNameReuse.json5)
|
||||
* [Check Ordering](/docs/examples/advancedConcepts)
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
See **Rule Name Reuse Examples [YAML](/docs/examples/advancedConcepts/ruleNameReuse.yaml) | [JSON](/docs/examples/advancedConcepts/ruleNameReuse.json5)**
|
||||
|
||||
### Check Order
|
||||
### Check Order and Flow Control
|
||||
|
||||
Checks are run in the order they appear in your configuration, therefore you should place your highest requirement/severe action checks at the top and lowest requirement/moderate actions at the bottom.
|
||||
|
||||
This is so that if an Activity warrants a more serious reaction that Check is triggered first rather than having a lower requirement check with less severe actions triggered and causing all subsequent Checks to be skipped.
|
||||
This is so that if an Activity warrants a more serious reaction that Check is triggered first rather than having a lower requirement check with less severe actions triggered and causing all subsequent Checks to be skipped.
|
||||
|
||||
This behavior can also be controlled modified using [Flow Control](/docs/examples/advancedConcepts/flowControl.md)
|
||||
|
||||
* Attribution >50% AND Repeat Activity 8x AND Recent Activity in 2 subs => remove submission + ban
|
||||
* Attribution >20% AND Repeat Activity 4x AND Recent Activity in 5 subs => remove submission + flair user restricted
|
||||
|
||||
228
docs/examples/advancedConcepts/flowControl.md
Normal file
@@ -0,0 +1,228 @@
|
||||
Context Mod's behavior after a **Check** has been processed can be configured by a user. This allows a subreddit to control exactly what Runs/Checks will be processed based on the outcome (triggered or not) of a Check.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Post-Check Properties](#post-check-properties)
|
||||
* [State](#state)
|
||||
* [Behavior](#behavior)
|
||||
+ [Next](#next)
|
||||
+ [Next Run](#next-run)
|
||||
+ [Stop](#stop)
|
||||
+ [Goto](#goto)
|
||||
- [Goto Syntax](#goto-syntax)
|
||||
- [Default Behaviors](#default-behaviors)
|
||||
* [Defining Default Behaviors](#defining-default-behaviors)
|
||||
- [Examples](#examples)
|
||||
|
||||
# Post-Check Properties
|
||||
|
||||
## State
|
||||
|
||||
When a Check is finished processing it can be in one of two states:
|
||||
|
||||
* **Triggered** -- The **Rules** defined in the Check were **triggered** which caused the **Actions** for the Check to be run
|
||||
* **Failure** -- The **Rules** defined in the check were **not triggered**, based on the conditions that were set (either from the [Check condition](/docs/README.md#Checks) or [Rule Sets](/docs/examples/advancedConcepts/README.md#Rule-Sets)), and no **Actions** were run
|
||||
|
||||
The behavior CM follows is based on which state it is in. The behavior can be specified **by one or both** of these **state properties** on the Check configuration:
|
||||
|
||||
* `postTrigger` -- Specifies what behavior to take when the check is **triggered**
|
||||
* `postFail` -- Specifies what behavior to take when the check is **not triggered**
|
||||
|
||||
## Behavior
|
||||
|
||||
There are **four** behaviors CM can take. Both/either **state properties** can be defined with **any behavior.**
|
||||
|
||||
### Next
|
||||
|
||||
The **Next** behavior tells CM to continue to whatever comes *after the Check that was just processed.* This could be another Check or, if this is the last Check in a Run, the next Run.
|
||||
|
||||
NOTE: `next` is the **default behavior** for the `postFail` state
|
||||
|
||||
Example
|
||||
|
||||
```yaml
|
||||
- name: MyCheck
|
||||
# ...
|
||||
postFail: next # if Check is not triggered then CM will start processing AnotherCheck
|
||||
|
||||
- name: AnotherCheck
|
||||
# ...
|
||||
```
|
||||
|
||||
### Next Run
|
||||
|
||||
The **Next Run** behavior tells CM to **skip all remaining Checks in the current Run and start processing the next Run in order.**
|
||||
|
||||
NOTE: `nextRun` is the **default behavior** for the `postTrigger` state
|
||||
|
||||
Example
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
- name: MyFirstRun
|
||||
checks:
|
||||
- name: MyCheck
|
||||
# ...
|
||||
postTrigger: nextRun # if Check is triggered then CM will SKIP mySecondCheck and instead start processing MySecondRun
|
||||
- name: MySecondCheck
|
||||
# ...
|
||||
|
||||
- name: MySecondRun
|
||||
checks:
|
||||
- name: FooCheck
|
||||
# ...
|
||||
```
|
||||
|
||||
### Stop
|
||||
|
||||
The **Stop** behavior tells CM to **stop processing the Activity entirely.** This means all remaining Checks and Runs will not be processed.
|
||||
|
||||
Example
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
- name: MyFirstRun
|
||||
checks:
|
||||
- name: MyCheck
|
||||
# ...
|
||||
postTrigger: stop # if Check is triggered CM will NOT process MySecondCheck OR MySecondRun. The activity is "done" being processed at this point
|
||||
- name: MySecondCheck
|
||||
# ...
|
||||
|
||||
- name: MySecondRun
|
||||
checks:
|
||||
- name: FooCheck
|
||||
# ...
|
||||
```
|
||||
|
||||
### Goto
|
||||
|
||||
The **Goto** behavior is an **advanced** behavior that allows you to specify that CM should "jump to" a specific place in your configuration, regardless of order/location, and continue processing the Activity from there. It can be used to do things like:
|
||||
|
||||
* create a loop/iteration to have CM re-process the Activity on an earlier executed part of your configuration because a later part modified the Activity (flaired, etc...)
|
||||
* use a Check as a simplified *switch statement*
|
||||
|
||||
**Goto should be use with care.** If you do not fully understand how this mechanism works you should avoid using it as **most** behaviors can be accomplished using the other behaviors.
|
||||
|
||||
As an additional protection **goto depth is limited to 1 by default** which means if a `goto` would be executed more than once during an Activity's lifecycle CM will automatically stop processing that Activity. The `maxGotoDepth` can be raised by the [**Bot Operator**](/docs/gettingStartedOperator.md) per subreddit.
|
||||
|
||||
#### Goto Syntax
|
||||
|
||||
Location to "jump to" can be specified as:
|
||||
|
||||
* **Run** -- `goto:myRunName`
|
||||
* **Check inside a different Run** -- `goto:myRunName.aCheckInsideTheRun`
|
||||
* **Check inside the current Run** -- `goto:.myCheck`
|
||||
|
||||
Example
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
- name: MyFirstRun
|
||||
checks:
|
||||
- name: FirstCheck
|
||||
# ...
|
||||
- name: MyCheck
|
||||
# ...
|
||||
postTrigger: 'goto:MyThirdRun' # jump to the run MyThirdRun
|
||||
postFail: 'goto:MySecondRun.BuzzCheck' # jump to the Check BuzzCheck inside the Run MySecondRun
|
||||
|
||||
- name: MySecondRun
|
||||
checks:
|
||||
- name: FooCheck
|
||||
# ...
|
||||
- name: BuzzCheck
|
||||
# ...
|
||||
|
||||
- name: MyThirdRun
|
||||
checks:
|
||||
- name: BarCheck
|
||||
# ...
|
||||
```
|
||||
|
||||
# Default Behaviors
|
||||
|
||||
It is **not required** to define post-Check behavior. CM uses sane defaults to mimic the behavior of automoderator as well as what is "intuitive" when reading a configuration -- that logic flows from top-to-bottom in the order it was defined. For each Check like this:
|
||||
|
||||
```yaml
|
||||
- name: MyCheck
|
||||
kind: comment
|
||||
rules:
|
||||
# ...
|
||||
actions:
|
||||
# ...
|
||||
```
|
||||
|
||||
`postTrigger` and `postFail` have default behaviors (mentioned in the sections above) that make the Check end up working like this:
|
||||
|
||||
```yaml
|
||||
- name: MyCheck
|
||||
kind: comment
|
||||
rules:
|
||||
# ...
|
||||
actions:
|
||||
# ...
|
||||
postTrigger: nextRun # check is triggered and actions were performed, skip remaining checks and go to the next Run
|
||||
postFail: next # check is not triggered and no actions performed, continue to the next check in this Run
|
||||
```
|
||||
|
||||
**So if you are fine with all Checks running in order until one triggered there is no need to define post-Check behaviors at all.**
|
||||
|
||||
## Defining Default Behaviors
|
||||
|
||||
Defining `postTrigger` and/or `postFail` on a **Run** will set the default behavior for any **Checks** in the Run that **do not have an explicit behavior set.**
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
- name: MyFirstRun
|
||||
postTrigger: stop # all Checks without postTrigger defined will have 'stop' as their behavior
|
||||
checks:
|
||||
- name: FooCheck # postTrigger is 'stop' since it is not defined
|
||||
# ...
|
||||
- name: BarCheck
|
||||
# ...
|
||||
postTrigger: next # overrides default behavior
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
One **Run** with **default behavior** (no post-Check behavior explicitly defined)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph spam ["(Run) Spam"]
|
||||
b1["(Check) self-promotion"] -- "postFail: next" --> b2
|
||||
b2["(Check) repeat spam"] -- "postFail: next" --> b3
|
||||
b3["(Check) Good user"]
|
||||
end
|
||||
b1 -- "postTrigger: nextRun" --> finish
|
||||
b2 -- "postTrigger: nextRun" --> finish
|
||||
b3 -- "postFail: next" --> finish
|
||||
b3 -- "postTrigger: nextRun" --> finish
|
||||
finish[Processing Finished]
|
||||
```
|
||||
|
||||
Two **Runs** with **default behavior** (no post-Check behavior explicitly defined)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph flair ["(Run) Flairing"]
|
||||
a1["(Check) Flair Submission based on history"]-- "postFail: next" -->a2
|
||||
a2["(Check) Flair Submission based on user profile"] -- "postFail: next" --> a3
|
||||
a3["(Check) Flair Submission based on self text"]
|
||||
end
|
||||
a1 -- "postTrigger: nextRun" --> b1
|
||||
a2 -- "postTrigger: nextRun" --> b1
|
||||
a3 -- "postFail: next" --> b1
|
||||
a3 -- "postTrigger: nextRun" --> b1
|
||||
subgraph spam ["(Run) Spam"]
|
||||
b1["(Check) self-promotion"] -- "postFail: next" -->b2
|
||||
b2["(Check) repeat spam"] -- "postFail: next" -->b3
|
||||
b3["(Check) Good user"]
|
||||
end
|
||||
b1 -- "postTrigger: nextRun" --> finish
|
||||
b2 -- "postTrigger: nextRun" --> finish
|
||||
b3 -- "postFail: next" --> finish
|
||||
b3 -- "postTrigger: nextRun" --> finish
|
||||
finish[Processing Finished]
|
||||
```
|
||||
96
docs/examples/advancedConcepts/flowControl.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
runs:
|
||||
- name: flairAndCategory
|
||||
|
||||
# Runs inherit the same filters as checks/rules/actions
|
||||
# If these filters fail the Run is skipped and CM processes the next run in order
|
||||
# authorIs:
|
||||
# itemIs:
|
||||
|
||||
# Set the default behavior for check trigger/fail
|
||||
# postTrigger:
|
||||
# postFail:
|
||||
|
||||
# Defaults can also be set for check authorIs/itemIs
|
||||
# same as at operator/subreddit level - any defined here will override "higher" defaults
|
||||
# filterCriteriaDefaults:
|
||||
|
||||
checks:
|
||||
- name: goodUserFlair
|
||||
description: flair user if they have decent history in sub
|
||||
kind: submission
|
||||
authorIs:
|
||||
exclude:
|
||||
- flairText: 'Good User'
|
||||
rules:
|
||||
- kind: recentActivity
|
||||
thresholds:
|
||||
- threshold: '> 5'
|
||||
karma: '> 10'
|
||||
subreddits:
|
||||
- mySubreddit
|
||||
actions:
|
||||
- kind: userflair
|
||||
text: 'Good User'
|
||||
# post-behavior after a check has run. Either the check is TRIGGERED or FAIL
|
||||
# there are 4 possible behaviors for each post-behavior type:
|
||||
#
|
||||
# 'next' => Continue to next check in order
|
||||
# 'nextRun' => Exit the current Run (skip all remaining Checks) and go to the next Run in order
|
||||
# 'stop' => Exit the current Run and finish activity processing immediately (skip all remaining Runs)
|
||||
# 'goto:run[.check]' => Specify a run[.check] to jump to. This can be anywhere in your config. CM will continue to process in order from the specified point.
|
||||
#
|
||||
# GOTO syntax --
|
||||
# 'goto:normalFilters' => go to run "normalFilters"
|
||||
# 'goto:normalFilters.myCheck' => go to run "normalFilters" and start at check "myCheck"
|
||||
# 'goto:.goodUserFlair' => go to check 'goodUserFlair' IN THE SAME RUN currently processing
|
||||
#
|
||||
|
||||
# this means if the check triggers then continue to 'good submission flair'
|
||||
postTrigger: next # default is 'nextRun'
|
||||
# postFail: # default is 'next'
|
||||
|
||||
- name: good submission flair
|
||||
description: flair submission if from good user
|
||||
kind: submission
|
||||
authorIs:
|
||||
include:
|
||||
- flairText: 'Good User'
|
||||
actions:
|
||||
- kind: flair
|
||||
text: 'Trusted Source'
|
||||
- kind: approve
|
||||
# this means if the check is triggered then stop processing the activity entirely
|
||||
postTrigger: stop
|
||||
|
||||
- name: Determine Suspect
|
||||
checks:
|
||||
- name: is suspect
|
||||
kind: submission
|
||||
rules:
|
||||
- kind: recentActivity
|
||||
thresholds:
|
||||
- subreddits:
|
||||
- over_18: true
|
||||
actions:
|
||||
# do some actions
|
||||
|
||||
# if check is triggered then go to run 'suspectFilters'
|
||||
postTrigger: 'goto:suspectFilters'
|
||||
# if check is not triggered then go to run 'normalFilters'
|
||||
postFail: 'goto:normalFilters'
|
||||
|
||||
- name: suspectFilters
|
||||
postTrigger: stop
|
||||
authorIs:
|
||||
exclude:
|
||||
- flairText: 'Good User'
|
||||
checks:
|
||||
# some checks for users that are suspicious
|
||||
|
||||
|
||||
- name: normalFilters
|
||||
authorIs:
|
||||
exclude:
|
||||
- flairText: 'Good User'
|
||||
checks:
|
||||
# some checks for general activities
|
||||
@@ -1,75 +1,79 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Auto Remove SP Karma",
|
||||
"description": "Remove submission because author has self-promo >10% and posted in karma subs recently",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
// named rules can be referenced at any point in the configuration (where they occur does not matter)
|
||||
// and can be used in any Check
|
||||
// Note: rules do not transfer between subreddit configurations
|
||||
"freekarmasub",
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
"name": "Auto Remove SP Karma",
|
||||
"description": "Remove submission because author has self-promo >10% and posted in karma subs recently",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
// named rules can be referenced at any point in the configuration (where they occur does not matter)
|
||||
// and can be used in any Check
|
||||
// Note: rules do not transfer between subreddit configurations
|
||||
"freekarmasub",
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
"kind": "comment",
|
||||
"content": "Your submission was removed because you are over reddit's threshold for self-promotion and recently posted this content in a karma sub"
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission was removed because you are over reddit's threshold for self-promotion and recently posted this content in a karma sub"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Free Karma On Submission Alert",
|
||||
"description": "Check if author has posted this submission in 'freekarma' subreddits",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// rules can be re-used throughout a configuration by referencing them by name
|
||||
//
|
||||
// The rule name itself can only contain spaces, hyphens and underscores
|
||||
// The value used to reference it will have all of these removed, and lower-cased
|
||||
//
|
||||
// so to reference this rule use the value 'freekarmasub'
|
||||
"name": "Free_Karma-SUB",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"useSubmissionAsReference":true,
|
||||
"thresholds": [
|
||||
"name": "Free Karma On Submission Alert",
|
||||
"description": "Check if author has posted this submission in 'freekarma' subreddits",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
// rules can be re-used throughout a configuration by referencing them by name
|
||||
//
|
||||
// The rule name itself can only contain spaces, hyphens and underscores
|
||||
// The value used to reference it will have all of these removed, and lower-cased
|
||||
//
|
||||
// so to reference this rule use the value 'freekarmasub'
|
||||
"name": "Free_Karma-SUB",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"useSubmissionAsReference":true,
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Submission posted {{rules.freekarmasub.totalCount}} times in karma {{rules.freekarmasub.subCount}} subs over {{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}"
|
||||
}
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Submission posted {{rules.freekarmasub.totalCount}} times in karma {{rules.freekarmasub.subCount}} subs over {{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
checks:
|
||||
- name: Auto Remove SP Karma
|
||||
description: >-
|
||||
Remove submission because author has self-promo >10% and posted in karma
|
||||
subs recently
|
||||
kind: submission
|
||||
rules:
|
||||
# named rules can be referenced at any point in the configuration (where they occur does not matter)
|
||||
# and can be used in any Check
|
||||
# Note: rules do not transfer between subreddit configurations
|
||||
- freekarmasub
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: remove
|
||||
- kind: comment
|
||||
content: >-
|
||||
Your submission was removed because you are over reddit's threshold
|
||||
for self-promotion and recently posted this content in a karma sub
|
||||
- name: Free Karma On Submission Alert
|
||||
description: Check if author has posted this submission in 'freekarma' subreddits
|
||||
kind: submission
|
||||
rules:
|
||||
# rules can be re-used throughout a configuration by referencing them by name
|
||||
#
|
||||
# The rule name itself can only contain spaces, hyphens and underscores
|
||||
# The value used to reference it will have all of these removed, and lower-cased
|
||||
#
|
||||
# so to reference this rule use the value 'freekarmasub'
|
||||
- name: Free_Karma-SUB
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
useSubmissionAsReference: true
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Submission posted {{rules.freekarmasub.totalCount}} times in karma
|
||||
{{rules.freekarmasub.subCount}} subs over
|
||||
{{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Auto Remove SP Karma
|
||||
description: >-
|
||||
Remove submission because author has self-promo >10% and posted in karma
|
||||
subs recently
|
||||
kind: submission
|
||||
rules:
|
||||
# named rules can be referenced at any point in the configuration (where they occur does not matter)
|
||||
# and can be used in any Check
|
||||
# Note: rules do not transfer between subreddit configurations
|
||||
- freekarmasub
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: remove
|
||||
- kind: comment
|
||||
content: >-
|
||||
Your submission was removed because you are over reddit's threshold
|
||||
for self-promotion and recently posted this content in a karma sub
|
||||
- name: Free Karma On Submission Alert
|
||||
description: Check if author has posted this submission in 'freekarma' subreddits
|
||||
kind: submission
|
||||
rules:
|
||||
# rules can be re-used throughout a configuration by referencing them by name
|
||||
#
|
||||
# The rule name itself can only contain spaces, hyphens and underscores
|
||||
# The value used to reference it will have all of these removed, and lower-cased
|
||||
#
|
||||
# so to reference this rule use the value 'freekarmasub'
|
||||
- name: Free_Karma-SUB
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
useSubmissionAsReference: true
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Submission posted {{rules.freekarmasub.totalCount}} times in karma
|
||||
{{rules.freekarmasub.subCount}} subs over
|
||||
{{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}
|
||||
|
||||
@@ -1,84 +1,88 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Self Promo All or low comment",
|
||||
"description": "SP >10% of all activities or >10% of submissions with low comment engagement",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
// this attribution rule is looking at all activities
|
||||
//
|
||||
// we want want this one rule to trigger the check because >10% of all activity (submission AND comments) is a good requirement
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
// this is a **Rule Set**
|
||||
//
|
||||
// it is made up of "nested" rules with a pass condition (AND/OR)
|
||||
// if the nested rules pass the condition then the Rule Set triggers the Check
|
||||
//
|
||||
// AND = all nested rules must be triggered to make the Rule Set trigger
|
||||
// AND = any of the nested Rules will be the Rule Set trigger
|
||||
"condition": "AND",
|
||||
// in this check we use an Attribution >10% on ONLY submissions, which is a lower requirement then the above attribution rule
|
||||
// and combine it with a History rule looking for low comment engagement
|
||||
// to make a "higher" requirement Rule Set our of two low requirement Rules
|
||||
"name": "Self Promo All or low comment",
|
||||
"description": "SP >10% of all activities or >10% of submissions with low comment engagement",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"name": "attr20sub",
|
||||
// this attribution rule is looking at all activities
|
||||
//
|
||||
// we want want this one rule to trigger the check because >10% of all activity (submission AND comments) is a good requirement
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
"lookAt": "media"
|
||||
},
|
||||
{
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
// this is a **Rule Set**
|
||||
//
|
||||
// it is made up of "nested" rules with a pass condition (AND/OR)
|
||||
// if the nested rules pass the condition then the Rule Set triggers the Check
|
||||
//
|
||||
// AND = all nested rules must be triggered to make the Rule Set trigger
|
||||
// AND = any of the nested Rules will be the Rule Set trigger
|
||||
"condition": "AND",
|
||||
// in this check we use an Attribution >10% on ONLY submissions, which is a lower requirement then the above attribution rule
|
||||
// and combine it with a History rule looking for low comment engagement
|
||||
// to make a "higher" requirement Rule Set our of two low requirement Rules
|
||||
"rules": [
|
||||
{
|
||||
"window": "90 days",
|
||||
"comment": "< 50%"
|
||||
"name": "attr20sub",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
"lookAt": "media"
|
||||
},
|
||||
{
|
||||
"window": "90 days",
|
||||
"comment": "> 40% OP"
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
{
|
||||
"window": "90 days",
|
||||
"comment": "< 50%"
|
||||
},
|
||||
{
|
||||
"window": "90 days",
|
||||
"comment": "> 40% OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
},
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission was removed because you are over reddit's threshold for self-promotion or exhibit low comment engagement"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
},
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission was removed because you are over reddit's threshold for self-promotion or exhibit low comment engagement"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
checks:
|
||||
- name: Self Promo All or low comment
|
||||
description: >-
|
||||
SP >10% of all activities or >10% of submissions with low comment
|
||||
engagement
|
||||
kind: submission
|
||||
rules:
|
||||
# this attribution rule is looking at all activities
|
||||
#
|
||||
# we want want this one rule to trigger the check because >10% of all activity (submission AND comments) is a good requirement
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
# this is a RULE SET
|
||||
#
|
||||
# it is made up of "nested" rules with a pass condition (AND/OR)
|
||||
# if the nested rules pass the condition then the Rule Set triggers the Check
|
||||
#
|
||||
# AND = all nested rules must be triggered to make the Rule Set trigger
|
||||
# AND = any of the nested Rules will be the Rule Set trigger
|
||||
- condition: AND
|
||||
# in this check we use an Attribution >10% on ONLY submissions, which is a lower requirement then the above attribution rule
|
||||
# and combine it with a History rule looking for low comment engagement
|
||||
# to make a "higher" requirement Rule Set our of two low requirement Rules
|
||||
runs:
|
||||
- checks:
|
||||
- name: Self Promo All or low comment
|
||||
description: >-
|
||||
SP >10% of all activities or >10% of submissions with low comment
|
||||
engagement
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr20sub
|
||||
# this attribution rule is looking at all activities
|
||||
#
|
||||
# we want want this one rule to trigger the check because >10% of all activity (submission AND comments) is a good requirement
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 100
|
||||
lookAt: media
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window: 90 days
|
||||
comment: < 50%
|
||||
- window: 90 days
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: remove
|
||||
- kind: comment
|
||||
content: >-
|
||||
Your submission was removed because you are over reddit's threshold
|
||||
for self-promotion or exhibit low comment engagement
|
||||
# this is a RULE SET
|
||||
#
|
||||
# it is made up of "nested" rules with a pass condition (AND/OR)
|
||||
# if the nested rules pass the condition then the Rule Set triggers the Check
|
||||
#
|
||||
# AND = all nested rules must be triggered to make the Rule Set trigger
|
||||
# AND = any of the nested Rules will be the Rule Set trigger
|
||||
- condition: AND
|
||||
# in this check we use an Attribution >10% on ONLY submissions, which is a lower requirement then the above attribution rule
|
||||
# and combine it with a History rule looking for low comment engagement
|
||||
# to make a "higher" requirement Rule Set our of two low requirement Rules
|
||||
rules:
|
||||
- name: attr20sub
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 100
|
||||
lookAt: media
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window: 90 days
|
||||
comment: < 50%
|
||||
- window: 90 days
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: remove
|
||||
- kind: comment
|
||||
content: >-
|
||||
Your submission was removed because you are over reddit's threshold
|
||||
for self-promotion or exhibit low comment engagement
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of entire history",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
// criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
"criteria": [
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of entire history",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// threshold can be a percent or an absolute number
|
||||
"threshold": "> 10%",
|
||||
// The default is "all" -- calculate percentage of entire history (submissions & comments)
|
||||
// "thresholdOn": "all",
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
// criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
"criteria": [
|
||||
{
|
||||
// threshold can be a percent or an absolute number
|
||||
"threshold": "> 10%",
|
||||
// The default is "all" -- calculate percentage of entire history (submissions & comments)
|
||||
// "thresholdOn": "all",
|
||||
|
||||
// look at last 90 days of Author's activities (comments and submissions)
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
// look at Author's last 100 activities (comments and submissions)
|
||||
"window": 100
|
||||
// look at last 90 days of Author's activities (comments and submissions)
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
// look at Author's last 100 activities (comments and submissions)
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr10all.largestPercent}}% of {{rules.attr10all.activityTotal}} items over {{rules.attr10all.window}}"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr10all.largestPercent}}% of {{rules.attr10all.activityTotal}} items over {{rules.attr10all.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
checks:
|
||||
- name: Self Promo Activities
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of entire
|
||||
history
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
# criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
criteria:
|
||||
- threshold: '> 10%' # threshold can be a percent or an absolute number
|
||||
# The default is "all" -- calculate percentage of entire history (submissions & comments)
|
||||
#thresholdOn: all
|
||||
#
|
||||
# look at last 90 days of Author's activities (comments and submissions)
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
# look at Author's last 100 activities (comments and submissions)
|
||||
window: 100
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.attr10all.largestPercent}}% of
|
||||
{{rules.attr10all.activityTotal}} items over
|
||||
{{rules.attr10all.window}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Self Promo Activities
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of entire
|
||||
history
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
# criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
criteria:
|
||||
- threshold: '> 10%' # threshold can be a percent or an absolute number
|
||||
# The default is "all" -- calculate percentage of entire history (submissions & comments)
|
||||
#thresholdOn: all
|
||||
#
|
||||
# look at last 90 days of Author's activities (comments and submissions)
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
# look at Author's last 100 activities (comments and submissions)
|
||||
window: 100
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.attr10all.largestPercent}}% of
|
||||
{{rules.attr10all.activityTotal}} items over
|
||||
{{rules.attr10all.window}}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
checks:
|
||||
- name: Self Promo Submissions
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of their
|
||||
submissions
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10sub
|
||||
kind: attribution
|
||||
# criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
criteria:
|
||||
- threshold: '> 10%' # threshold can be a percent or an absolute number
|
||||
thresholdOn: submissions # calculate percentage of submissions, rather than entire history (submissions & comments)
|
||||
window: 90 days # look at last 90 days of Author's activities (comments and submissions)
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 100 # look at Author's last 100 activities (comments and submissions)
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.attr10sub.largestPercent}}% of
|
||||
{{rules.attr10sub.activityTotal}} items over
|
||||
{{rules.attr10sub.window}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Self Promo Submissions
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of their
|
||||
submissions
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10sub
|
||||
kind: attribution
|
||||
# criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
criteria:
|
||||
- threshold: '> 10%' # threshold can be a percent or an absolute number
|
||||
thresholdOn: submissions # calculate percentage of submissions, rather than entire history (submissions & comments)
|
||||
window: 90 days # look at last 90 days of Author's activities (comments and submissions)
|
||||
- threshold: '> 10%'
|
||||
thresholdOn: submissions
|
||||
window: 100 # look at Author's last 100 activities (comments and submissions)
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.attr10sub.largestPercent}}% of
|
||||
{{rules.attr10sub.activityTotal}} items over
|
||||
{{rules.attr10sub.window}}
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Self Promo Submissions",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of their submissions",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr10sub",
|
||||
"kind": "attribution",
|
||||
// criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
"criteria": [
|
||||
"name": "Self Promo Submissions",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of their submissions",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// threshold can be a percent or an absolute number
|
||||
"threshold": "> 10%",
|
||||
// calculate percentage of submissions, rather than entire history (submissions & comments)
|
||||
"thresholdOn": "submissions",
|
||||
"name": "attr10sub",
|
||||
"kind": "attribution",
|
||||
// criteria defaults to OR -- so either of these criteria will trigger the rule
|
||||
"criteria": [
|
||||
{
|
||||
// threshold can be a percent or an absolute number
|
||||
"threshold": "> 10%",
|
||||
// calculate percentage of submissions, rather than entire history (submissions & comments)
|
||||
"thresholdOn": "submissions",
|
||||
|
||||
// look at last 90 days of Author's activities (comments and submissions)
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
// look at Author's last 100 activities (comments and submissions)
|
||||
"window": 100
|
||||
// look at last 90 days of Author's activities (comments and submissions)
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"thresholdOn": "submissions",
|
||||
// look at Author's last 100 activities (comments and submissions)
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr10sub.largestPercent}}% of {{rules.attr10sub.activityTotal}} items over {{rules.attr10sub.window}}"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr10sub.largestPercent}}% of {{rules.attr10sub.activityTotal}} items over {{rules.attr10sub.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,67 +1,71 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Karma/Meme Sub Activity",
|
||||
"description": "Report on karma sub activity or meme sub activity if user isn't a memelord",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
"name": "Karma/Meme Sub Activity",
|
||||
"description": "Report on karma sub activity or meme sub activity if user isn't a memelord",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
},
|
||||
{
|
||||
"name": "noobmemer",
|
||||
"kind": "recentActivity",
|
||||
// authors filter will be checked before a rule is run. If anything passes then the Rule is skipped -- it is not failed or triggered.
|
||||
// if *all* Rules for a Check are skipped due to authors filter then the Check will fail
|
||||
"authorIs": {
|
||||
// each property (include/exclude) can contain multiple AuthorCriteria
|
||||
// if any AuthorCriteria passes its test the Rule is skipped
|
||||
//
|
||||
// for an AuthorCriteria to pass all properties present on it must pass
|
||||
//
|
||||
// if "include" is present it will always run and exclude will be skipped
|
||||
// "include:" []
|
||||
"exclude": [
|
||||
// for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
{
|
||||
"flairText": ["Supreme Memer"],
|
||||
"names": ["user1","user2"]
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
},
|
||||
{
|
||||
"name": "noobmemer",
|
||||
"kind": "recentActivity",
|
||||
// authors filter will be checked before a rule is run. If anything passes then the Rule is skipped -- it is not failed or triggered.
|
||||
// if *all* Rules for a Check are skipped due to authors filter then the Check will fail
|
||||
"authorIs": {
|
||||
// each property (include/exclude) can contain multiple AuthorCriteria
|
||||
// if any AuthorCriteria passes its test the Rule is skipped
|
||||
//
|
||||
// for an AuthorCriteria to pass all properties present on it must pass
|
||||
//
|
||||
// if "include" is present it will always run and exclude will be skipped
|
||||
// "include:" []
|
||||
"exclude": [
|
||||
// for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
{
|
||||
"flairText": ["Supreme Memer"],
|
||||
"names": ["user1","user2"]
|
||||
},
|
||||
{
|
||||
// for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
"flairText": ["Decent Memer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
"flairText": ["Decent Memer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"dankmemes",
|
||||
]
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"dankmemes",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted in free karma sub, or in /r/dankmemes and does not have meme flair in this subreddit"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted in free karma sub, or in /r/dankmemes and does not have meme flair in this subreddit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
checks:
|
||||
- name: Karma/Meme Sub Activity
|
||||
description: Report on karma sub activity or meme sub activity if user isn't a memelord
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
window: 7 days
|
||||
- name: noobmemer
|
||||
kind: recentActivity
|
||||
# authors filter will be checked before a rule is run. If anything passes then the Rule is skipped -- it is not failed or triggered.
|
||||
# if *all* Rules for a Check are skipped due to authors filter then the Check will fail
|
||||
authorIs:
|
||||
# each property (include/exclude) can contain multiple AuthorCriteria
|
||||
# if any AuthorCriteria passes its test the Rule is skipped
|
||||
#
|
||||
# for an AuthorCriteria to pass all properties present on it must pass
|
||||
#
|
||||
# if include is present it will always run and exclude will be skipped
|
||||
#-include:
|
||||
exclude:
|
||||
# for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
- flairText:
|
||||
- Supreme Memer
|
||||
names:
|
||||
- user1
|
||||
- user2
|
||||
# for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
- flairText:
|
||||
- Decent Memer
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- dankmemes
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has posted in free karma sub, or in /r/dankmemes and does not
|
||||
have meme flair in this subreddit
|
||||
runs:
|
||||
- checks:
|
||||
- name: Karma/Meme Sub Activity
|
||||
description: Report on karma sub activity or meme sub activity if user isn't a memelord
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
window: 7 days
|
||||
- name: noobmemer
|
||||
kind: recentActivity
|
||||
# authors filter will be checked before a rule is run. If anything passes then the Rule is skipped -- it is not failed or triggered.
|
||||
# if *all* Rules for a Check are skipped due to authors filter then the Check will fail
|
||||
authorIs:
|
||||
# each property (include/exclude) can contain multiple AuthorCriteria
|
||||
# if any AuthorCriteria passes its test the Rule is skipped
|
||||
#
|
||||
# for an AuthorCriteria to pass all properties present on it must pass
|
||||
#
|
||||
# if include is present it will always run and exclude will be skipped
|
||||
#-include:
|
||||
exclude:
|
||||
# for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
- flairText:
|
||||
- Supreme Memer
|
||||
names:
|
||||
- user1
|
||||
- user2
|
||||
# for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
- flairText:
|
||||
- Decent Memer
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- dankmemes
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has posted in free karma sub, or in /r/dankmemes and does not
|
||||
have meme flair in this subreddit
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Flair New User Sub",
|
||||
"description": "Flair submission as sketchy if user does not have vet flair",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "newflair",
|
||||
"kind": "author",
|
||||
// rule will trigger if Author does not have "vet" flair text
|
||||
"exclude": [
|
||||
"name": "Flair New User Sub",
|
||||
"description": "Flair submission as sketchy if user does not have vet flair",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
"name": "newflair",
|
||||
"kind": "author",
|
||||
// rule will trigger if Author does not have "vet" flair text
|
||||
"exclude": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "flair",
|
||||
"text": "New User",
|
||||
"css": "orange"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "flair",
|
||||
"text": "New User",
|
||||
"css": "orange"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
checks:
|
||||
- name: Flair New User Sub
|
||||
description: Flair submission as sketchy if user does not have vet flair
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: newflair
|
||||
kind: author
|
||||
# rule will trigger if Author does not have "vet" flair text
|
||||
exclude:
|
||||
- flairText:
|
||||
- vet
|
||||
actions:
|
||||
- kind: flair
|
||||
text: New User
|
||||
css: orange
|
||||
runs:
|
||||
- checks:
|
||||
- name: Flair New User Sub
|
||||
description: Flair submission as sketchy if user does not have vet flair
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: newflair
|
||||
kind: author
|
||||
# rule will trigger if Author does not have "vet" flair text
|
||||
exclude:
|
||||
- flairText:
|
||||
- vet
|
||||
actions:
|
||||
- kind: flair
|
||||
text: New User
|
||||
css: orange
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Flair Vetted User Submission",
|
||||
"description": "Flair submission as Approved if user has vet flair",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "newflair",
|
||||
"kind": "author",
|
||||
// rule will trigger if Author has "vet" flair text
|
||||
"include": [
|
||||
"name": "Flair Vetted User Submission",
|
||||
"description": "Flair submission as Approved if user has vet flair",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
"name": "newflair",
|
||||
"kind": "author",
|
||||
// rule will trigger if Author has "vet" flair text
|
||||
"include": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "flair",
|
||||
"text": "Vetted",
|
||||
"css": "green"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "flair",
|
||||
"text": "Vetted",
|
||||
"css": "green"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
checks:
|
||||
- name: Flair Vetted User Submission
|
||||
description: Flair submission as Approved if user has vet flair
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: newflair
|
||||
kind: author
|
||||
# rule will trigger if Author has "vet" flair text
|
||||
include:
|
||||
- flairText:
|
||||
- vet
|
||||
actions:
|
||||
- kind: flair
|
||||
text: Vetted
|
||||
css: green
|
||||
runs:
|
||||
- checks:
|
||||
- name: Flair Vetted User Submission
|
||||
description: Flair submission as Approved if user has vet flair
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: newflair
|
||||
kind: author
|
||||
# rule will trigger if Author has "vet" flair text
|
||||
include:
|
||||
- flairText:
|
||||
- vet
|
||||
actions:
|
||||
- kind: flair
|
||||
text: Vetted
|
||||
css: green
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "non-vetted karma/meme activity",
|
||||
"description": "Report if Author has SP and has recent karma/meme sub activity and isn't vetted",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
// The Author Rule is best used in conjunction with other Rules --
|
||||
// instead of having to write an AuthorFilter for every Rule where you want to skip it based on Author criteria
|
||||
// you can write one Author Rule and make it fail on the required criteria
|
||||
// so that the check fails and Actions don't run
|
||||
"name": "nonvet",
|
||||
"kind": "author",
|
||||
"exclude": [
|
||||
"name": "non-vetted karma/meme activity",
|
||||
"description": "Report if Author has SP and has recent karma/meme sub activity and isn't vetted",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "attr10",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
// The Author Rule is best used in conjunction with other Rules --
|
||||
// instead of having to write an AuthorFilter for every Rule where you want to skip it based on Author criteria
|
||||
// you can write one Author Rule and make it fail on the required criteria
|
||||
// so that the check fails and Actions don't run
|
||||
"name": "nonvet",
|
||||
"kind": "author",
|
||||
"exclude": [
|
||||
{
|
||||
"flairText": ["vet"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
"name": "attr10",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
},
|
||||
{
|
||||
"name": "memes",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
},
|
||||
{
|
||||
"threshold": ">= 3",
|
||||
"subreddits": [
|
||||
"dankmemes",
|
||||
]
|
||||
"name": "memes",
|
||||
"kind": "recentActivity",
|
||||
"lookAt": "submissions",
|
||||
"thresholds": [
|
||||
{
|
||||
"threshold": ">= 3",
|
||||
"subreddits": [
|
||||
"dankmemes",
|
||||
]
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
// will NOT run if the Author for this Submission has the flair "vet"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted in free karma or meme subs recently"
|
||||
// will NOT run if the Author for this Submission has the flair "vet"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted in free karma or meme subs recently"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
checks:
|
||||
- name: non-vetted karma/meme activity
|
||||
description: >-
|
||||
Report if Author has SP and has recent karma/meme sub activity and isn't
|
||||
vetted
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
# The Author Rule is best used in conjunction with other Rules --
|
||||
# instead of having to write an AuthorFilter for every Rule where you want to skip it based on Author criteria
|
||||
# you can write one Author Rule and make it fail on the required criteria
|
||||
# so that the check fails and Actions don't run
|
||||
- name: nonvet
|
||||
kind: author
|
||||
exclude:
|
||||
- flairText:
|
||||
- vet
|
||||
- name: attr10
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
window: 7 days
|
||||
- name: memes
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 3'
|
||||
subreddits:
|
||||
- dankmemes
|
||||
window: 7 days
|
||||
# will NOT run if the Author for this Submission has the flair "vet"
|
||||
actions:
|
||||
- kind: report
|
||||
content: Author has posted in free karma or meme subs recently
|
||||
runs:
|
||||
- checks:
|
||||
- name: non-vetted karma/meme activity
|
||||
description: >-
|
||||
Report if Author has SP and has recent karma/meme sub activity and isn't
|
||||
vetted
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
# The Author Rule is best used in conjunction with other Rules --
|
||||
# instead of having to write an AuthorFilter for every Rule where you want to skip it based on Author criteria
|
||||
# you can write one Author Rule and make it fail on the required criteria
|
||||
# so that the check fails and Actions don't run
|
||||
- name: nonvet
|
||||
kind: author
|
||||
exclude:
|
||||
- flairText:
|
||||
- vet
|
||||
- name: attr10
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
window: 7 days
|
||||
- name: memes
|
||||
kind: recentActivity
|
||||
lookAt: submissions
|
||||
thresholds:
|
||||
- threshold: '>= 3'
|
||||
subreddits:
|
||||
- dankmemes
|
||||
window: 7 days
|
||||
# will NOT run if the Author for this Submission has the flair "vet"
|
||||
actions:
|
||||
- kind: report
|
||||
content: Author has posted in free karma or meme subs recently
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Low Comment Engagement",
|
||||
"description": "Check if Author is submitting much more than they comment",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "lowComm",
|
||||
"kind": "history",
|
||||
"criteria": [
|
||||
"name": "Low Comment Engagement",
|
||||
"description": "Check if Author is submitting much more than they comment",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// look at last 90 days of Author's activities
|
||||
"window": "90 days",
|
||||
// trigger if less than 30% of their activities in this time period are comments
|
||||
"comment": "< 30%"
|
||||
},
|
||||
"name": "lowComm",
|
||||
"kind": "history",
|
||||
"criteria": [
|
||||
{
|
||||
// look at last 90 days of Author's activities
|
||||
"window": "90 days",
|
||||
// trigger if less than 30% of their activities in this time period are comments
|
||||
"comment": "< 30%"
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Low engagement: comments were {{rules.lowcomm.commentPercent}} of {{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Low engagement: comments were {{rules.lowcomm.commentPercent}} of {{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
checks:
|
||||
- name: Low Comment Engagement
|
||||
description: Check if Author is submitting much more than they comment
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: lowComm
|
||||
kind: history
|
||||
criteria:
|
||||
- comment: '< 30%'
|
||||
window:
|
||||
# get author's last 90 days of activities or 100 activities, whichever is less
|
||||
duration: 90 days
|
||||
count: 100
|
||||
# trigger if less than 30% of their activities in this time period are comments
|
||||
runs:
|
||||
- checks:
|
||||
- name: Low Comment Engagement
|
||||
description: Check if Author is submitting much more than they comment
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: lowComm
|
||||
kind: history
|
||||
criteria:
|
||||
- comment: '< 30%'
|
||||
window:
|
||||
# get author's last 90 days of activities or 100 activities, whichever is less
|
||||
duration: 90 days
|
||||
count: 100
|
||||
# trigger if less than 30% of their activities in this time period are comments
|
||||
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Low engagement: comments were {{rules.lowcomm.commentPercent}} of
|
||||
{{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}}
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Low engagement: comments were {{rules.lowcomm.commentPercent}} of
|
||||
{{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}}
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Engaging Own Content Only",
|
||||
"description": "Check if Author is mostly engaging in their own content only",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "opOnly",
|
||||
"kind": "history",
|
||||
"criteria": [
|
||||
"name": "Engaging Own Content Only",
|
||||
"description": "Check if Author is mostly engaging in their own content only",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// look at last 90 days of Author's activities
|
||||
"window": "90 days",
|
||||
// trigger if more than 60% of their activities in this time period are comments as OP
|
||||
"comment": "> 60% OP"
|
||||
},
|
||||
"name": "opOnly",
|
||||
"kind": "history",
|
||||
"criteria": [
|
||||
{
|
||||
// look at last 90 days of Author's activities
|
||||
"window": "90 days",
|
||||
// trigger if more than 60% of their activities in this time period are comments as OP
|
||||
"comment": "> 60% OP"
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Selfish OP: {{rules.oponly.opPercent}} of {{rules.oponly.commentTotal}} comments over {{rules.oponly.window}} are as OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Selfish OP: {{rules.oponly.opPercent}} of {{rules.oponly.commentTotal}} comments over {{rules.oponly.window}} are as OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
checks:
|
||||
- name: Engaging Own Content Only
|
||||
description: Check if Author is mostly engaging in their own content only
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: opOnly
|
||||
kind: history
|
||||
criteria:
|
||||
# trigger if more than 60% of their activities in this time period are comments as OP
|
||||
- comment: '> 60% OP'
|
||||
window:
|
||||
# get author's last 90 days of activities or 100 activities, whichever is less
|
||||
duration: 90 days
|
||||
count: 100
|
||||
runs:
|
||||
- checks:
|
||||
- name: Engaging Own Content Only
|
||||
description: Check if Author is mostly engaging in their own content only
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: opOnly
|
||||
kind: history
|
||||
criteria:
|
||||
# trigger if more than 60% of their activities in this time period are comments as OP
|
||||
- comment: '> 60% OP'
|
||||
window:
|
||||
# get author's last 90 days of activities or 100 activities, whichever is less
|
||||
duration: 90 days
|
||||
count: 100
|
||||
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Selfish OP: {{rules.oponly.opPercent}} of
|
||||
{{rules.oponly.commentTotal}} comments over {{rules.oponly.window}}
|
||||
are as OP
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Selfish OP: {{rules.oponly.opPercent}} of
|
||||
{{rules.oponly.commentTotal}} comments over {{rules.oponly.window}}
|
||||
are as OP
|
||||
|
||||
@@ -1,66 +1,70 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Flair OF submitters",
|
||||
"description": "Flair submission as OF if user does not have Verified flair and has certain keywords in their profile",
|
||||
"kind": "submission",
|
||||
"authorIs": {
|
||||
"exclude": [
|
||||
{
|
||||
"flairCssClass": ["verified"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "OnlyFans strings in description",
|
||||
"kind": "author",
|
||||
"include": [
|
||||
"name": "Flair OF submitters",
|
||||
"description": "Flair submission as OF if user does not have Verified flair and has certain keywords in their profile",
|
||||
"kind": "submission",
|
||||
"authorIs": {
|
||||
"exclude": [
|
||||
{
|
||||
"flairCssClass": ["verified"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"description": [
|
||||
"/(cashapp|allmylinks|linktr|onlyfans\\.com)/i",
|
||||
"/(see|check|my|view) (out|of|onlyfans|kik|skype|insta|ig|profile|links)/i",
|
||||
"my links",
|
||||
"$"
|
||||
"name": "OnlyFans strings in description",
|
||||
"kind": "author",
|
||||
"include": [
|
||||
{
|
||||
"description": [
|
||||
"/(cashapp|allmylinks|linktr|onlyfans\\.com)/i",
|
||||
"/(see|check|my|view) (out|of|onlyfans|kik|skype|insta|ig|profile|links)/i",
|
||||
"my links",
|
||||
"$"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "Set OnlyFans user flair",
|
||||
"kind": "userflair",
|
||||
"flair_template_id": "put-your-onlyfans-user-flair-id-here"
|
||||
},
|
||||
{
|
||||
"name":"Set OF Creator SUBMISSION flair",
|
||||
"kind": "flair",
|
||||
"flair_template_id": "put-your-onlyfans-post-flair-id-here"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Flair posts of OF submitters",
|
||||
"description": "Flair submission as OnlyFans if submitter has OnlyFans userflair (override post flair set by submitter)",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"name": "Include OF submitters",
|
||||
"kind": "author",
|
||||
"include": [
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"flairCssClass": ["onlyfans"]
|
||||
"name": "Set OnlyFans user flair",
|
||||
"kind": "userflair",
|
||||
"flair_template_id": "put-your-onlyfans-user-flair-id-here"
|
||||
},
|
||||
{
|
||||
"name":"Set OF Creator SUBMISSION flair",
|
||||
"kind": "flair",
|
||||
"flair_template_id": "put-your-onlyfans-post-flair-id-here"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
},
|
||||
|
||||
{
|
||||
"name":"Set OF Creator SUBMISSION flair",
|
||||
"kind": "flair",
|
||||
"flair_template_id": "put-your-onlyfans-post-flair-id-here"
|
||||
"name": "Flair posts of OF submitters",
|
||||
"description": "Flair submission as OnlyFans if submitter has OnlyFans userflair (override post flair set by submitter)",
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"name": "Include OF submitters",
|
||||
"kind": "author",
|
||||
"include": [
|
||||
{
|
||||
"flairCssClass": ["onlyfans"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name":"Set OF Creator SUBMISSION flair",
|
||||
"kind": "flair",
|
||||
"flair_template_id": "put-your-onlyfans-post-flair-id-here"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
checks:
|
||||
- name: Flair OF submitters
|
||||
description: Flair submission as OF if user does not have Verified flair and has
|
||||
certain keywords in their profile
|
||||
kind: submission
|
||||
authorIs:
|
||||
exclude:
|
||||
- flairCssClass:
|
||||
- verified
|
||||
rules:
|
||||
- name: OnlyFans strings in description
|
||||
kind: author
|
||||
include:
|
||||
- description:
|
||||
- '/(cashapp|allmylinks|linktr|onlyfans\.com)/i'
|
||||
- '/(see|check|my|view) (out|of|onlyfans|kik|skype|insta|ig|profile|links)/i'
|
||||
- my links
|
||||
- "$"
|
||||
actions:
|
||||
- name: Set OnlyFans user flair
|
||||
kind: userflair
|
||||
flair_template_id: put-your-onlyfans-user-flair-id-here
|
||||
- name: Set OF Creator SUBMISSION flair
|
||||
kind: flair
|
||||
flair_template_id: put-your-onlyfans-post-flair-id-here
|
||||
- name: Flair posts of OF submitters
|
||||
description: Flair submission as OnlyFans if submitter has OnlyFans userflair (override post flair set by submitter)
|
||||
kind: submission
|
||||
rules:
|
||||
- name: Include OF submitters
|
||||
kind: author
|
||||
include:
|
||||
- flairCssClass:
|
||||
- onlyfans
|
||||
actions:
|
||||
- name: Set OF Creator SUBMISSION flair
|
||||
kind: flair
|
||||
flair_template_id: put-your-onlyfans-post-flair-id-here
|
||||
runs:
|
||||
- checks:
|
||||
- name: Flair OF submitters
|
||||
description: Flair submission as OF if user does not have Verified flair and has
|
||||
certain keywords in their profile
|
||||
kind: submission
|
||||
authorIs:
|
||||
exclude:
|
||||
- flairCssClass:
|
||||
- verified
|
||||
rules:
|
||||
- name: OnlyFans strings in description
|
||||
kind: author
|
||||
include:
|
||||
- description:
|
||||
- '/(cashapp|allmylinks|linktr|onlyfans\.com)/i'
|
||||
- '/(see|check|my|view) (out|of|onlyfans|kik|skype|insta|ig|profile|links)/i'
|
||||
- my links
|
||||
- "$"
|
||||
actions:
|
||||
- name: Set OnlyFans user flair
|
||||
kind: userflair
|
||||
flair_template_id: put-your-onlyfans-user-flair-id-here
|
||||
- name: Set OF Creator SUBMISSION flair
|
||||
kind: flair
|
||||
flair_template_id: put-your-onlyfans-post-flair-id-here
|
||||
- name: Flair posts of OF submitters
|
||||
description: Flair submission as OnlyFans if submitter has OnlyFans userflair (override post flair set by submitter)
|
||||
kind: submission
|
||||
rules:
|
||||
- name: Include OF submitters
|
||||
kind: author
|
||||
include:
|
||||
- flairCssClass:
|
||||
- onlyfans
|
||||
actions:
|
||||
- name: Set OF Creator SUBMISSION flair
|
||||
kind: flair
|
||||
flair_template_id: put-your-onlyfans-post-flair-id-here
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Free Karma Alert",
|
||||
"description": "Check if author has posted in 'freekarma' subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"useSubmissionAsReference": false,
|
||||
// when `lookAt` is not present this rule will look for submissions and comments
|
||||
// lookAt: "submissions"
|
||||
// lookAt: "comments"
|
||||
"thresholds": [
|
||||
"name": "Free Karma Alert",
|
||||
"description": "Check if author has posted in 'freekarma' subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// for all subreddits, if the number of activities (sub/comment) is equal to or greater than 1 then the rule is triggered
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"useSubmissionAsReference": false,
|
||||
// when `lookAt` is not present this rule will look for submissions and comments
|
||||
// lookAt: "submissions"
|
||||
// lookAt: "comments"
|
||||
"thresholds": [
|
||||
{
|
||||
// for all subreddits, if the number of activities (sub/comment) is equal to or greater than 1 then the rule is triggered
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
}
|
||||
],
|
||||
// will look at all of the Author's activities in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
// will look at all of the Author's activities in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.freekarma.totalCount}} activities in karma {{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}: {{rules.freekarma.subSummary}}"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.freekarma.totalCount}} activities in karma {{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}: {{rules.freekarma.subSummary}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
checks:
|
||||
- name: Free Karma Alert
|
||||
description: Check if author has posted in 'freekarma' subreddits
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
# // when lookAt is not present this rule will look for submissions and comments
|
||||
#lookAt: comments
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
# if the number of activities (sub/comment) found CUMULATIVELY in the subreddits listed is
|
||||
# equal to or greater than 1 then the rule is triggered
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.freekarma.totalCount}} activities in karma
|
||||
{{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}:
|
||||
{{rules.freekarma.subSummary}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Free Karma Alert
|
||||
description: Check if author has posted in 'freekarma' subreddits
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
# // when lookAt is not present this rule will look for submissions and comments
|
||||
#lookAt: comments
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
# if the number of activities (sub/comment) found CUMULATIVELY in the subreddits listed is
|
||||
# equal to or greater than 1 then the rule is triggered
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
{{rules.freekarma.totalCount}} activities in karma
|
||||
{{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}:
|
||||
{{rules.freekarma.subSummary}}
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Free Karma On Submission Alert",
|
||||
"description": "Check if author has posted this submission in 'freekarma' subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "freekarmasub",
|
||||
"kind": "recentActivity",
|
||||
// rule will only look at Author's submissions in these subreddits
|
||||
"lookAt": "submissions",
|
||||
// rule will only look at Author's submissions in these subreddits that have the same content (link) as the submission this event was made on
|
||||
// In simpler terms -- rule will only check to see if the same link the author just posted is also posted in these subreddits
|
||||
"useSubmissionAsReference":true,
|
||||
"thresholds": [
|
||||
"name": "Free Karma On Submission Alert",
|
||||
"description": "Check if author has posted this submission in 'freekarma' subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
// for all subreddits, if the number of activities (sub/comment) is equal to or greater than 1 then the rule is triggered
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
"name": "freekarmasub",
|
||||
"kind": "recentActivity",
|
||||
// rule will only look at Author's submissions in these subreddits
|
||||
"lookAt": "submissions",
|
||||
// rule will only look at Author's submissions in these subreddits that have the same content (link) as the submission this event was made on
|
||||
// In simpler terms -- rule will only check to see if the same link the author just posted is also posted in these subreddits
|
||||
"useSubmissionAsReference":true,
|
||||
"thresholds": [
|
||||
{
|
||||
// for all subreddits, if the number of activities (sub/comment) is equal to or greater than 1 then the rule is triggered
|
||||
"threshold": ">= 1",
|
||||
"subreddits": [
|
||||
"DeFreeKarma",
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"upvote"
|
||||
]
|
||||
}
|
||||
],
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Submission posted {{rules.freekarmasub.totalCount}} times in karma {{rules.freekarmasub.subCount}} subs over {{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Submission posted {{rules.freekarmasub.totalCount}} times in karma {{rules.freekarmasub.subCount}} subs over {{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
checks:
|
||||
- name: Free Karma On Submission Alert
|
||||
description: Check if author has posted this submission in 'freekarma' subreddits
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarmasub
|
||||
kind: recentActivity
|
||||
# rule will only look at Author's submissions in these subreddits
|
||||
lookAt: submissions
|
||||
# rule will only look at Author's submissions in these subreddits that have the same content (link) as the submission this event was made on
|
||||
# In simpler terms -- rule will only check to see if the same link the author just posted is also posted in these subreddits
|
||||
useSubmissionAsReference: true
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Submission posted {{rules.freekarmasub.totalCount}} times in karma
|
||||
{{rules.freekarmasub.subCount}} subs over
|
||||
{{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Free Karma On Submission Alert
|
||||
description: Check if author has posted this submission in 'freekarma' subreddits
|
||||
kind: submission
|
||||
rules:
|
||||
- name: freekarmasub
|
||||
kind: recentActivity
|
||||
# rule will only look at Author's submissions in these subreddits
|
||||
lookAt: submissions
|
||||
# rule will only look at Author's submissions in these subreddits that have the same content (link) as the submission this event was made on
|
||||
# In simpler terms -- rule will only check to see if the same link the author just posted is also posted in these subreddits
|
||||
useSubmissionAsReference: true
|
||||
thresholds:
|
||||
- threshold: '>= 1'
|
||||
subreddits:
|
||||
- DeFreeKarma
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- upvote
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Submission posted {{rules.freekarmasub.totalCount}} times in karma
|
||||
{{rules.freekarmasub.subCount}} subs over
|
||||
{{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}}
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "remove discord spam",
|
||||
"notifyOnTrigger": true,
|
||||
"description": "remove comments from users who are spamming discord links",
|
||||
"kind": "comment",
|
||||
"authorIs": {
|
||||
"exclude": [
|
||||
{
|
||||
"isMod": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemIs": [
|
||||
"checks": [
|
||||
{
|
||||
"removed": false,
|
||||
"approved": false,
|
||||
}
|
||||
],
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
{
|
||||
// set to false if you want to allow comments with a discord link ONLY IF
|
||||
// the author doesn't have a history of spamming discord links
|
||||
// -- basically allows one-off/organic discord links
|
||||
"enable": true,
|
||||
"name": "linkOnlySpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
"name": "remove discord spam",
|
||||
"notifyOnTrigger": true,
|
||||
"description": "remove comments from users who are spamming discord links",
|
||||
"kind": "comment",
|
||||
"authorIs": {
|
||||
"exclude": [
|
||||
{
|
||||
"isMod": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemIs": [
|
||||
{
|
||||
"name": "only link",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+)$/i",
|
||||
"removed": false,
|
||||
"approved": false,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "AND",
|
||||
],
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
{
|
||||
"name": "linkAnywhereSpam",
|
||||
// set to false if you want to allow comments with a discord link ONLY IF
|
||||
// the author doesn't have a history of spamming discord links
|
||||
// -- basically allows one-off/organic discord links
|
||||
"enable": true,
|
||||
"name": "linkOnlySpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains link anywhere",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"name": "only link",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+)$/i",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "linkAnywhereHistoricalSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "contains links anywhere historically",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"totalMatchThreshold": ">= 3",
|
||||
"lookAt": "comments",
|
||||
"window": 10
|
||||
"name": "linkAnywhereSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains link anywhere",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "linkAnywhereHistoricalSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains links anywhere historically",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"totalMatchThreshold": ">= 3",
|
||||
"lookAt": "comments",
|
||||
"window": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
checks:
|
||||
- name: remove discord spam
|
||||
notifyOnTrigger: true
|
||||
description: remove comments from users who are spamming discord links
|
||||
kind: comment
|
||||
authorIs:
|
||||
exclude:
|
||||
- isMod: true
|
||||
itemIs:
|
||||
- removed: false
|
||||
approved: false
|
||||
condition: OR
|
||||
rules:
|
||||
- enable: true
|
||||
name: linkOnlySpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: only link
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+)$/i'
|
||||
- condition: AND
|
||||
runs:
|
||||
- checks:
|
||||
- name: remove discord spam
|
||||
notifyOnTrigger: true
|
||||
description: remove comments from users who are spamming discord links
|
||||
kind: comment
|
||||
authorIs:
|
||||
exclude:
|
||||
- isMod: true
|
||||
itemIs:
|
||||
- removed: false
|
||||
approved: false
|
||||
condition: OR
|
||||
rules:
|
||||
- name: linkAnywhereSpam
|
||||
- enable: true
|
||||
name: linkOnlySpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains link anywhere
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
- name: linkAnywhereHistoricalSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains links anywhere historically
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
totalMatchThreshold: '>= 3'
|
||||
lookAt: comments
|
||||
window: 10
|
||||
actions:
|
||||
- kind: remove
|
||||
- name: only link
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+)$/i'
|
||||
- condition: AND
|
||||
rules:
|
||||
- name: linkAnywhereSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains link anywhere
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
- name: linkAnywhereHistoricalSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains links anywhere historically
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
totalMatchThreshold: '>= 3'
|
||||
lookAt: comments
|
||||
window: 10
|
||||
actions:
|
||||
- kind: remove
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Burstpost Spam",
|
||||
"description": "Check if Author is crossposting in short bursts",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "burstpost",
|
||||
"kind": "repeatActivity",
|
||||
// will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
"useSubmissionAsReference": true,
|
||||
// the number of non-repeat activities (submissions or comments) to ignore between repeat submissions
|
||||
"gapAllowance": 3,
|
||||
// if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger
|
||||
"threshold": ">= 6",
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has burst-posted this link {{rules.burstpost.largestRepeat}} times over {{rules.burstpost.window}}"
|
||||
"name": "Burstpost Spam",
|
||||
"description": "Check if Author is crossposting in short bursts",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"name": "burstpost",
|
||||
"kind": "repeatActivity",
|
||||
// will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
"useSubmissionAsReference": true,
|
||||
// the number of non-repeat activities (submissions or comments) to ignore between repeat submissions
|
||||
"gapAllowance": 3,
|
||||
// if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger
|
||||
"threshold": ">= 6",
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has burst-posted this link {{rules.burstpost.largestRepeat}} times over {{rules.burstpost.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
checks:
|
||||
- name: Burstpost Spam
|
||||
description: Check if Author is crossposting in short bursts
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: burstpost
|
||||
kind: repeatActivity
|
||||
# will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
useSubmissionAsReference: true
|
||||
# the number of non-repeat activities (submissions or comments) to ignore between repeat submissions
|
||||
gapAllowance: 3
|
||||
# if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger
|
||||
threshold: '>= 6'
|
||||
# look at all of the Author's submissions in the last 7 days or 100 submissions
|
||||
window:
|
||||
duration: 7 days
|
||||
count: 100
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has burst-posted this link {{rules.burstpost.largestRepeat}}
|
||||
times over {{rules.burstpost.window}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Burstpost Spam
|
||||
description: Check if Author is crossposting in short bursts
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: burstpost
|
||||
kind: repeatActivity
|
||||
# will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
useSubmissionAsReference: true
|
||||
# the number of non-repeat activities (submissions or comments) to ignore between repeat submissions
|
||||
gapAllowance: 3
|
||||
# if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger
|
||||
threshold: '>= 6'
|
||||
# look at all of the Author's submissions in the last 7 days or 100 submissions
|
||||
window:
|
||||
duration: 7 days
|
||||
count: 100
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has burst-posted this link {{rules.burstpost.largestRepeat}}
|
||||
times over {{rules.burstpost.window}}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Crosspost Spam",
|
||||
"description": "Check if Author is spamming Submissions across subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "xpostspam",
|
||||
"kind": "repeatActivity",
|
||||
// will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
"useSubmissionAsReference": true,
|
||||
// if the Author has posted this Submission 5 times consecutively then this rule will trigger
|
||||
"threshold": ">= 5",
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted this link {{rules.xpostspam.largestRepeat}} times over {{rules.xpostspam.window}}"
|
||||
"name": "Crosspost Spam",
|
||||
"description": "Check if Author is spamming Submissions across subreddits",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xpostspam",
|
||||
"kind": "repeatActivity",
|
||||
// will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
"useSubmissionAsReference": true,
|
||||
// if the Author has posted this Submission 5 times consecutively then this rule will trigger
|
||||
"threshold": ">= 5",
|
||||
// look at all of the Author's submissions in the last 7 days
|
||||
"window": "7 days"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Author has posted this link {{rules.xpostspam.largestRepeat}} times over {{rules.xpostspam.window}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
checks:
|
||||
- name: Crosspost Spam
|
||||
description: Check if Author is spamming Submissions across subreddits
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: xpostspam
|
||||
kind: repeatActivity
|
||||
# will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
useSubmissionAsReference: true
|
||||
# if the Author has posted this Submission 5 times consecutively then this rule will trigger
|
||||
threshold: '>= 5'
|
||||
# look at all of the Author's submissions in the last 7 days
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has posted this link {{rules.xpostspam.largestRepeat}} times
|
||||
over {{rules.xpostspam.window}}
|
||||
runs:
|
||||
- checks:
|
||||
- name: Crosspost Spam
|
||||
description: Check if Author is spamming Submissions across subreddits
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: xpostspam
|
||||
kind: repeatActivity
|
||||
# will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by
|
||||
useSubmissionAsReference: true
|
||||
# if the Author has posted this Submission 5 times consecutively then this rule will trigger
|
||||
threshold: '>= 5'
|
||||
# look at all of the Author's submissions in the last 7 days
|
||||
window: 7 days
|
||||
actions:
|
||||
- kind: report
|
||||
content: >-
|
||||
Author has posted this link {{rules.xpostspam.largestRepeat}} times
|
||||
over {{rules.xpostspam.window}}
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
{
|
||||
"polling": ["newComm"],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
//
|
||||
// Stop users who spam the same comment many times
|
||||
//
|
||||
// Remove a COMMENT if the user has crossposted it at least 4 times in recent history
|
||||
//
|
||||
"name": "low xp comment spam",
|
||||
"description": "X-posted comment >=4x",
|
||||
"kind": "comment",
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
}
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> Posted same comment {{rules.xpostlow.largestRepeat}}x times"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
//
|
||||
// Stop users who spam the same comment many times
|
||||
//
|
||||
// Remove a COMMENT if the user has crossposted it at least 4 times in recent history
|
||||
//
|
||||
"name": "low xp comment spam",
|
||||
"description": "X-posted comment >=4x",
|
||||
"kind": "comment",
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
}
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> Posted same comment {{rules.xpostlow.largestRepeat}}x times"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
polling:
|
||||
- newComm
|
||||
checks:
|
||||
# Stop users who spam the same comment many times
|
||||
- name: low xp comment spam
|
||||
description: X-posted comment >=4x
|
||||
kind: comment
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
# number of "non-repeat" comments allowed between "repeat comments"
|
||||
gapAllowance: 2
|
||||
# greater or more than 4 repeat comments triggers this rule
|
||||
threshold: '>= 4'
|
||||
# retrieve either last 50 comments or 6 months' of history, whichever is less
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove => Posted same comment {{rules.xpostlow.largestRepeat}}x times'
|
||||
- kind: remove
|
||||
enable: true
|
||||
runs:
|
||||
- checks:
|
||||
# Stop users who spam the same comment many times
|
||||
- name: low xp comment spam
|
||||
description: X-posted comment >=4x
|
||||
kind: comment
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
# number of "non-repeat" comments allowed between "repeat comments"
|
||||
gapAllowance: 2
|
||||
# greater or more than 4 repeat comments triggers this rule
|
||||
threshold: '>= 4'
|
||||
# retrieve either last 50 comments or 6 months' of history, whichever is less
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove => Posted same comment {{rules.xpostlow.largestRepeat}}x times'
|
||||
- kind: remove
|
||||
enable: true
|
||||
|
||||
@@ -1,77 +1,81 @@
|
||||
{
|
||||
"polling": ["unmoderated"],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
//
|
||||
// Stop users who post low-effort, crossposted spam
|
||||
//
|
||||
// Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
// less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "low xp spam and engagement",
|
||||
"description": "X-posted 4x and low comment engagement",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
"checks": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
//
|
||||
// Stop users who post low-effort, crossposted spam
|
||||
//
|
||||
// Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
// less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "low xp spam and engagement",
|
||||
"description": "X-posted 4x and low comment engagement",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 100,
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
}
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=>{{rules.xpostlow.largestRepeat}} X-P => {{rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you cross-posted it {{rules.xpostlow.largestRepeat}} times and you have very low engagement outside of making submissions",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=>{{rules.xpostlow.largestRepeat}} X-P => {{rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you cross-posted it {{rules.xpostlow.largestRepeat}} times and you have very low engagement outside of making submissions",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
polling:
|
||||
- unmoderated
|
||||
checks:
|
||||
# stop users who post low-effort, crossposted spam submissions
|
||||
#
|
||||
# Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
# less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
- name: low xp spam and engagement
|
||||
description: X-posted 4x and low comment engagement
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
gapAllowance: 2
|
||||
threshold: '>= 4'
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
runs:
|
||||
- checks:
|
||||
# stop users who post low-effort, crossposted spam submissions
|
||||
#
|
||||
# Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
# less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
- name: low xp spam and engagement
|
||||
description: X-posted 4x and low comment engagement
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
gapAllowance: 2
|
||||
threshold: '>= 4'
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
Remove=>{{rules.xpostlow.largestRepeat}} X-P =>
|
||||
{{rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: true
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you cross-posted it
|
||||
{{rules.xpostlow.largestRepeat}} times and you have very low
|
||||
engagement outside of making submissions
|
||||
distinguish: true
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
Remove=>{{rules.xpostlow.largestRepeat}} X-P =>
|
||||
{{rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: true
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you cross-posted it
|
||||
{{rules.xpostlow.largestRepeat}} times and you have very low
|
||||
engagement outside of making submissions
|
||||
distinguish: true
|
||||
|
||||
@@ -1,75 +1,79 @@
|
||||
{
|
||||
"polling": ["newComm"],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "ban discord only spammer",
|
||||
"description": "ban a user who spams only a discord link many times historically",
|
||||
"kind": "comment",
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
"linkOnlySpam",
|
||||
"linkAnywhereHistoricalSpam",
|
||||
],
|
||||
"actions": [
|
||||
"checks": [
|
||||
{
|
||||
"kind": "remove"
|
||||
},
|
||||
{
|
||||
"kind": "ban",
|
||||
"content": "spamming discord links"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "remove discord spam",
|
||||
"description": "remove comments from users who only link to discord or mention discord link many times historically",
|
||||
"kind": "comment",
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
{
|
||||
"name": "linkOnlySpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
"name": "ban discord only spammer",
|
||||
"description": "ban a user who spams only a discord link many times historically",
|
||||
"kind": "comment",
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
"linkOnlySpam",
|
||||
"linkAnywhereHistoricalSpam",
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "only link",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+)$/i",
|
||||
"kind": "remove"
|
||||
},
|
||||
{
|
||||
"kind": "ban",
|
||||
"content": "spamming discord links"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "AND",
|
||||
"name": "remove discord spam",
|
||||
"description": "remove comments from users who only link to discord or mention discord link many times historically",
|
||||
"kind": "comment",
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
{
|
||||
"name": "linkAnywhereSpam",
|
||||
"name": "linkOnlySpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains link anywhere",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"name": "only link",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+)$/i",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "linkAnywhereHistoricalSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "contains links anywhere historically",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"totalMatchThreshold": ">= 3",
|
||||
"lookAt": "comments",
|
||||
"window": 10
|
||||
"name": "linkAnywhereSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains link anywhere",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "linkAnywhereHistoricalSpam",
|
||||
"kind": "regex",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "contains links anywhere historically",
|
||||
"regex": "/^.*(discord\\.gg\\/[\\w\\d]+).*$/i",
|
||||
"totalMatchThreshold": ">= 3",
|
||||
"lookAt": "comments",
|
||||
"window": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "remove"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
polling:
|
||||
- newComm
|
||||
checks:
|
||||
- name: ban discord only spammer
|
||||
description: ban a user who spams only a discord link many times historically
|
||||
kind: comment
|
||||
condition: AND
|
||||
rules:
|
||||
runs:
|
||||
- checks:
|
||||
- name: ban discord only spammer
|
||||
description: ban a user who spams only a discord link many times historically
|
||||
kind: comment
|
||||
condition: AND
|
||||
rules:
|
||||
- linkOnlySpam
|
||||
- linkAnywhereHistoricalSpam
|
||||
actions:
|
||||
actions:
|
||||
- kind: remove
|
||||
- kind: ban
|
||||
content: spamming discord links
|
||||
- name: remove discord spam
|
||||
description: >-
|
||||
- name: remove discord spam
|
||||
description: >-
|
||||
remove comments from users who only link to discord or mention discord
|
||||
link many times historically
|
||||
kind: comment
|
||||
condition: OR
|
||||
rules:
|
||||
kind: comment
|
||||
condition: OR
|
||||
rules:
|
||||
- name: linkOnlySpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: only link
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+)$/i'
|
||||
- name: only link
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+)$/i'
|
||||
- condition: AND
|
||||
rules:
|
||||
- name: linkAnywhereSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains link anywhere
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
- name: linkAnywhereHistoricalSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains links anywhere historically
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
totalMatchThreshold: '>= 3'
|
||||
lookAt: comments
|
||||
window: 10
|
||||
actions:
|
||||
- name: linkAnywhereSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains link anywhere
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
- name: linkAnywhereHistoricalSpam
|
||||
kind: regex
|
||||
criteria:
|
||||
- name: contains links anywhere historically
|
||||
# single quotes are required to escape special characters
|
||||
regex: '/^.*(discord\.gg\/[\w\d]+).*$/i'
|
||||
totalMatchThreshold: '>= 3'
|
||||
lookAt: comments
|
||||
window: 10
|
||||
actions:
|
||||
- kind: remove
|
||||
|
||||
@@ -2,135 +2,139 @@
|
||||
"polling": [
|
||||
"unmoderated"
|
||||
],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
//
|
||||
// Stop users who post low-effort, crossposted spam
|
||||
//
|
||||
// Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
// less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "remove on low xp spam and engagement",
|
||||
"description": "X-posted 4x and low comment engagement",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
"checks": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
//
|
||||
// Stop users who post low-effort, crossposted spam
|
||||
//
|
||||
// Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
// less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "remove on low xp spam and engagement",
|
||||
"description": "X-posted 4x and low comment engagement",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "xPostLow",
|
||||
"kind": "repeatActivity",
|
||||
"gapAllowance": 2,
|
||||
"threshold": ">= 4",
|
||||
"window": {
|
||||
"count": 100,
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
}
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=>{{rules.xpostlow.largestRepeat}} X-P => {{rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you cross-posted it {{rules.xpostlow.largestRepeat}} times and you have very low engagement outside of making submissions",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
//
|
||||
// Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
//
|
||||
"name": "freekarma removal",
|
||||
"description": "Remove submission if user has used freekarma sub recently",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"useSubmissionAsReference": false,
|
||||
"thresholds": [
|
||||
{
|
||||
"subreddits": [
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"KarmaStore",
|
||||
"promote",
|
||||
"shamelessplug",
|
||||
"upvote"
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=>{{rules.xpostlow.largestRepeat}} X-P => {{rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you cross-posted it {{rules.xpostlow.largestRepeat}} times and you have very low engagement outside of making submissions",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> {{rules.newtube.totalCount}} activities in freekarma subs"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you have recent activity in 'freekarma' subs",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
//
|
||||
// Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
//
|
||||
"name": "freekarma removal",
|
||||
"description": "Remove submission if user has used freekarma sub recently",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"useSubmissionAsReference": false,
|
||||
"thresholds": [
|
||||
{
|
||||
"subreddits": [
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"KarmaStore",
|
||||
"promote",
|
||||
"shamelessplug",
|
||||
"upvote"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> {{rules.newtube.totalCount}} activities in freekarma subs"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you have recent activity in 'freekarma' subs",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,84 +1,85 @@
|
||||
polling:
|
||||
- unmoderated
|
||||
checks:
|
||||
# stop users who post low-effort, crossposted spam submissions
|
||||
#
|
||||
# Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
# less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
- name: remove on low xp spam and engagement
|
||||
description: X-posted 4x and low comment engagement
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
gapAllowance: 2
|
||||
threshold: '>= 4'
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
runs:
|
||||
- checks:
|
||||
# stop users who post low-effort, crossposted spam submissions
|
||||
#
|
||||
# Remove a SUBMISSION if the user has crossposted it at least 4 times in recent history AND
|
||||
# less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
- name: remove on low xp spam and engagement
|
||||
description: X-posted 4x and low comment engagement
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: xPostLow
|
||||
kind: repeatActivity
|
||||
gapAllowance: 2
|
||||
threshold: '>= 4'
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
Remove=>{{rules.xpostlow.largestRepeat}} X-P =>
|
||||
{{rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you cross-posted it
|
||||
{{rules.xpostlow.largestRepeat}} times and you have very low
|
||||
engagement outside of making submissions
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
# Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
- name: freekarma removal
|
||||
description: Remove submission if user has used freekarma sub recently
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
Remove=>{{rules.xpostlow.largestRepeat}} X-P =>
|
||||
{{rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you cross-posted it
|
||||
{{rules.xpostlow.largestRepeat}} times and you have very low
|
||||
engagement outside of making submissions
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
# Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
- name: freekarma removal
|
||||
description: Remove submission if user has used freekarma sub recently
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
- subreddits:
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- KarmaStore
|
||||
- promote
|
||||
- shamelessplug
|
||||
- upvote
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove=> {{rules.newtube.totalCount}} activities in freekarma subs'
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you have recent activity in
|
||||
'freekarma' subs
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
- subreddits:
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- KarmaStore
|
||||
- promote
|
||||
- shamelessplug
|
||||
- upvote
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove=> {{rules.newtube.totalCount}} activities in freekarma subs'
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed because you have recent activity in
|
||||
'freekarma' subs
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
|
||||
@@ -2,63 +2,67 @@
|
||||
"polling": [
|
||||
"unmoderated"
|
||||
],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
//
|
||||
// Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
//
|
||||
"name": "freekarma removal",
|
||||
"description": "Remove submission if user has used freekarma sub recently",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
"checks": [
|
||||
{
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"useSubmissionAsReference": false,
|
||||
"thresholds": [
|
||||
//
|
||||
// Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
//
|
||||
"name": "freekarma removal",
|
||||
"description": "Remove submission if user has used freekarma sub recently",
|
||||
"kind": "submission",
|
||||
"itemIs": [
|
||||
{
|
||||
"subreddits": [
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"KarmaStore",
|
||||
"upvote"
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"name": "freekarma",
|
||||
"kind": "recentActivity",
|
||||
"window": {
|
||||
"count": 50,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"useSubmissionAsReference": false,
|
||||
"thresholds": [
|
||||
{
|
||||
"subreddits": [
|
||||
"FreeKarma4U",
|
||||
"FreeKarma4You",
|
||||
"KarmaStore",
|
||||
"upvote"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> {{rules.newtube.totalCount}} activities in freekarma subs"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true,
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you have recent activity in 'freekarma' subs",
|
||||
"distinguish": true,
|
||||
"dryRun": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
// remove this after confirming behavior is acceptable
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "Remove=> {{rules.newtube.totalCount}} activities in freekarma subs"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true,
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed because you have recent activity in 'freekarma' subs",
|
||||
"distinguish": true,
|
||||
"dryRun": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
polling:
|
||||
- unmoderated
|
||||
checks:
|
||||
# Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
- name: freekarma removal
|
||||
description: Remove submission if user has used freekarma sub recently
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
- subreddits:
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- KarmaStore
|
||||
- upvote
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove=> {{rules.newtube.totalCount}} activities in freekarma subs'
|
||||
- kind: remove
|
||||
enable: true
|
||||
- kind: comment
|
||||
enable: false
|
||||
content: >-
|
||||
Your submission has been removed because you have recent activity in
|
||||
'freekarma' subs
|
||||
distinguish: true
|
||||
runs:
|
||||
- checks:
|
||||
# Remove submissions from users who have recent activity in freekarma subs within the last 50 activities or 6 months (whichever is less)
|
||||
- name: freekarma removal
|
||||
description: Remove submission if user has used freekarma sub recently
|
||||
kind: submission
|
||||
itemIs:
|
||||
- removed: false
|
||||
condition: AND
|
||||
rules:
|
||||
- name: freekarma
|
||||
kind: recentActivity
|
||||
window:
|
||||
count: 50
|
||||
duration: 6 months
|
||||
useSubmissionAsReference: false
|
||||
thresholds:
|
||||
- subreddits:
|
||||
- FreeKarma4U
|
||||
- FreeKarma4You
|
||||
- KarmaStore
|
||||
- upvote
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: 'Remove=> {{rules.newtube.totalCount}} activities in freekarma subs'
|
||||
- kind: remove
|
||||
enable: true
|
||||
- kind: comment
|
||||
enable: false
|
||||
content: >-
|
||||
Your submission has been removed because you have recent activity in
|
||||
'freekarma' subs
|
||||
distinguish: true
|
||||
|
||||
@@ -2,48 +2,30 @@
|
||||
"polling": [
|
||||
"unmoderated"
|
||||
],
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
//
|
||||
// Stop users who make link submissions with a self-promotional agenda (with reddit's suggested 10% rule)
|
||||
// https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit
|
||||
//
|
||||
// Remove a SUBMISSION if the link comprises more than or equal to 10% of users history (100 activities or 6 months) OR
|
||||
//
|
||||
// if link comprises 10% of submission history (100 activities or 6 months)
|
||||
// AND less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "Self-promo all AND low engagement",
|
||||
"description": "Self-promo is >10% for all or just sub and low comment engagement",
|
||||
"kind": "submission",
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": ">= 10%",
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"domains": [
|
||||
"AGG:SELF"
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"condition": "AND",
|
||||
//
|
||||
// Stop users who make link submissions with a self-promotional agenda (with reddit's suggested 10% rule)
|
||||
// https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit
|
||||
//
|
||||
// Remove a SUBMISSION if the link comprises more than or equal to 10% of users history (100 activities or 6 months) OR
|
||||
//
|
||||
// if link comprises 10% of submission history (100 activities or 6 months)
|
||||
// AND less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
//
|
||||
"name": "Self-promo all AND low engagement",
|
||||
"description": "Self-promo is >10% for all or just sub and low comment engagement",
|
||||
"kind": "submission",
|
||||
"condition": "OR",
|
||||
"rules": [
|
||||
{
|
||||
"name": "attrsub",
|
||||
"name": "attr",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": ">= 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
@@ -52,52 +34,74 @@
|
||||
"AGG:SELF"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
"condition": "AND",
|
||||
"rules": [
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
"name": "attrsub",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": ">= 10%",
|
||||
"thresholdOn": "submissions",
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"domains": [
|
||||
"AGG:SELF"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
"name": "lowOrOpComm",
|
||||
"kind": "history",
|
||||
"criteriaJoin": "OR",
|
||||
"criteria": [
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "< 50%"
|
||||
},
|
||||
{
|
||||
"window": {
|
||||
"count": 100,
|
||||
"duration": "6 months"
|
||||
},
|
||||
"comment": "> 40% OP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}} of {{rules.attr.activityTotal}}{{rules.attrsub.activityTotal}} items ({{rules.attr.window}}{{rules.attrsub.window}}){{#rules.loworopcomm.thresholdSummary}} => {{rules.loworopcomm.thresholdSummary}}{{/rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed it comprises 10% or more of your recent history ({{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}}). This is against [reddit's self promotional guidelines.](https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit)",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "report",
|
||||
"content": "{{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}} of {{rules.attr.activityTotal}}{{rules.attrsub.activityTotal}} items ({{rules.attr.window}}{{rules.attrsub.window}}){{#rules.loworopcomm.thresholdSummary}} => {{rules.loworopcomm.thresholdSummary}}{{/rules.loworopcomm.thresholdSummary}}"
|
||||
},
|
||||
//
|
||||
//
|
||||
{
|
||||
"kind": "remove",
|
||||
// remove the line below after confirming behavior is acceptable
|
||||
"dryRun": true
|
||||
},
|
||||
// optionally remove "dryRun" from below if you want to leave a comment on removal
|
||||
// PROTIP: the comment is bland, you should make it better
|
||||
{
|
||||
"kind": "comment",
|
||||
"content": "Your submission has been removed it comprises 10% or more of your recent history ({{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}}). This is against [reddit's self promotional guidelines.](https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit)",
|
||||
"distinguish": true,
|
||||
"dryRun": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,71 +1,72 @@
|
||||
polling:
|
||||
- unmoderated
|
||||
checks:
|
||||
#
|
||||
# Stop users who make link submissions with a self-promotional agenda (with reddit's suggested 10% rule)
|
||||
# https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit
|
||||
#
|
||||
# Remove a SUBMISSION if the link comprises more than or equal to 10% of users history (100 activities or 6 months) OR
|
||||
#
|
||||
# if link comprises 10% of submission history (100 activities or 6 months)
|
||||
# AND less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
#
|
||||
- name: Self-promo all AND low engagement
|
||||
description: Self-promo is >10% for all or just sub and low comment engagement
|
||||
kind: submission
|
||||
condition: OR
|
||||
rules:
|
||||
- name: attr
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '>= 10%'
|
||||
window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
domains:
|
||||
- 'AGG:SELF'
|
||||
- condition: AND
|
||||
runs:
|
||||
- checks:
|
||||
#
|
||||
# Stop users who make link submissions with a self-promotional agenda (with reddit's suggested 10% rule)
|
||||
# https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit
|
||||
#
|
||||
# Remove a SUBMISSION if the link comprises more than or equal to 10% of users history (100 activities or 6 months) OR
|
||||
#
|
||||
# if link comprises 10% of submission history (100 activities or 6 months)
|
||||
# AND less than 50% of their activity is comments OR more than 40% of those comments are as OP (in the own submissions)
|
||||
#
|
||||
- name: Self-promo all AND low engagement
|
||||
description: Self-promo is >10% for all or just sub and low comment engagement
|
||||
kind: submission
|
||||
condition: OR
|
||||
rules:
|
||||
- name: attrsub
|
||||
- name: attr
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '>= 10%'
|
||||
thresholdOn: submissions
|
||||
window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
domains:
|
||||
- 'AGG:SELF'
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
{{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}} of
|
||||
{{rules.attr.activityTotal}}{{rules.attrsub.activityTotal}} items
|
||||
({{rules.attr.window}}{{rules.attrsub.window}}){{#rules.loworopcomm.thresholdSummary}}
|
||||
=>
|
||||
{{rules.loworopcomm.thresholdSummary}}{{/rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed it comprises 10% or more of your
|
||||
recent history
|
||||
({{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}}). This
|
||||
is against [reddit's self promotional
|
||||
guidelines.](https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit)
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
- condition: AND
|
||||
rules:
|
||||
- name: attrsub
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '>= 10%'
|
||||
thresholdOn: submissions
|
||||
window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
domains:
|
||||
- 'AGG:SELF'
|
||||
- name: lowOrOpComm
|
||||
kind: history
|
||||
criteriaJoin: OR
|
||||
criteria:
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: < 50%
|
||||
- window:
|
||||
count: 100
|
||||
duration: 6 months
|
||||
comment: '> 40% OP'
|
||||
actions:
|
||||
- kind: report
|
||||
enable: true
|
||||
content: >-
|
||||
{{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}} of
|
||||
{{rules.attr.activityTotal}}{{rules.attrsub.activityTotal}} items
|
||||
({{rules.attr.window}}{{rules.attrsub.window}}){{#rules.loworopcomm.thresholdSummary}}
|
||||
=>
|
||||
{{rules.loworopcomm.thresholdSummary}}{{/rules.loworopcomm.thresholdSummary}}
|
||||
- kind: remove
|
||||
enable: false
|
||||
- kind: comment
|
||||
enable: true
|
||||
content: >-
|
||||
Your submission has been removed it comprises 10% or more of your
|
||||
recent history
|
||||
({{rules.attr.largestPercent}}{{rules.attrsub.largestPercent}}). This
|
||||
is against [reddit's self promotional
|
||||
guidelines.](https://www.reddit.com/wiki/selfpromotion#wiki_guidelines_for_self-promotion_on_reddit)
|
||||
distinguish: true
|
||||
dryRun: true
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Tag SP only if user does not have good contributor user note",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"author": {
|
||||
"exclude": [
|
||||
{
|
||||
// the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
// rule will not run if current usernote on Author is of type 'gooduser'
|
||||
"type": "gooduser"
|
||||
}
|
||||
]
|
||||
},
|
||||
"criteria": [
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Tag SP only if user does not have good contributor user note",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"author": {
|
||||
"exclude": [
|
||||
{
|
||||
// the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
// rule will not run if current usernote on Author is of type 'gooduser'
|
||||
"type": "gooduser"
|
||||
}
|
||||
]
|
||||
},
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "usernote",
|
||||
// the key of usernote type
|
||||
// https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
"type": "spamwarn",
|
||||
// content is mustache templated as usual
|
||||
"content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "usernote",
|
||||
// the key of usernote type
|
||||
// https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
"type": "spamwarn",
|
||||
// content is mustache templated as usual
|
||||
"content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
checks:
|
||||
- name: Self Promo Activities
|
||||
description: Tag SP only if user does not have good contributor user note
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
author:
|
||||
exclude:
|
||||
# the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
# rule will not run if current usernote on Author is of type 'gooduser'
|
||||
- type: gooduser
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: usernote
|
||||
# the key of usernote type
|
||||
# https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
type: spamwarn
|
||||
# content is mustache templated
|
||||
content: >-
|
||||
Self Promotion: {{rules.attr10all.titlesDelim}}
|
||||
{{rules.attr10sub.largestPercent}}%
|
||||
runs:
|
||||
- checks:
|
||||
- name: Self Promo Activities
|
||||
description: Tag SP only if user does not have good contributor user note
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
author:
|
||||
exclude:
|
||||
# the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
# rule will not run if current usernote on Author is of type 'gooduser'
|
||||
- type: gooduser
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: usernote
|
||||
# the key of usernote type
|
||||
# https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
type: spamwarn
|
||||
# content is mustache templated
|
||||
content: >-
|
||||
Self Promotion: {{rules.attr10all.titlesDelim}}
|
||||
{{rules.attr10sub.largestPercent}}%
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
{
|
||||
"checks": [
|
||||
"runs": [
|
||||
{
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of entire history",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
"checks": [
|
||||
{
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
"name": "Self Promo Activities",
|
||||
"description": "Check if any of Author's aggregated submission origins are >10% of entire history",
|
||||
// check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
"kind": "submission",
|
||||
"rules": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
"name": "attr10all",
|
||||
"kind": "attribution",
|
||||
"criteria": [
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": "90 days"
|
||||
},
|
||||
{
|
||||
"threshold": "> 10%",
|
||||
"window": 100
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"kind": "usernote",
|
||||
// the key of usernote type
|
||||
// https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
"type": "spamwarn",
|
||||
// content is mustache templated as usual
|
||||
"content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%"
|
||||
"actions": [
|
||||
{
|
||||
"kind": "usernote",
|
||||
// the key of usernote type
|
||||
// https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
"type": "spamwarn",
|
||||
// content is mustache templated as usual
|
||||
"content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
checks:
|
||||
- name: Self Promo Activities
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of entire
|
||||
history
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: usernote
|
||||
# the key of usernote type
|
||||
# https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
type: spamwarn
|
||||
content: >-
|
||||
Self Promotion: {{rules.attr10all.titlesDelim}}
|
||||
{{rules.attr10sub.largestPercent}}%
|
||||
runs:
|
||||
- checks:
|
||||
- name: Self Promo Activities
|
||||
# check will run on a new submission in your subreddit and look at the Author of that submission
|
||||
description: >-
|
||||
Check if any of Author's aggregated submission origins are >10% of entire
|
||||
history
|
||||
kind: submission
|
||||
rules:
|
||||
- name: attr10all
|
||||
kind: attribution
|
||||
criteria:
|
||||
- threshold: '> 10%'
|
||||
window: 90 days
|
||||
- threshold: '> 10%'
|
||||
window: 100
|
||||
actions:
|
||||
- kind: usernote
|
||||
# the key of usernote type
|
||||
# https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types
|
||||
type: spamwarn
|
||||
content: >-
|
||||
Self Promotion: {{rules.attr10all.titlesDelim}}
|
||||
{{rules.attr10sub.largestPercent}}%
|
||||
|
||||
@@ -81,7 +81,7 @@ The default location for this page is at `https://old.reddit.com/r/YOURSUBERDDIT
|
||||
|
||||
The bot automatically tries to create its configuration wiki page. You can find the result of this in the log for your subreddit in the web interface.
|
||||
|
||||
If this fails for some reason you can create the wiki page through the web interface by navigating to your subreddit's tab, opening the [built-in editor (click **View**)](/docs/screenshots/configBox.png), and following the directions in **Create configuration for...** link found there.
|
||||
If this fails for some reason you can create the wiki page through the web interface by navigating to your subreddit's tab, opening the [built-in editor (click **View**)](/docs/images/configBox.png), and following the directions in **Create configuration for...** link found there.
|
||||
|
||||
If neither of the above approaches work, or you do not wish to use the web interface, expand the section below for directions on how to manually setup the wiki page:
|
||||
|
||||
@@ -134,7 +134,7 @@ PROTIP: Find an [example config](#using-an-example-config) to use as a starting
|
||||
In the web interface each subreddit's tab has access to the built-in editor. Use this built-in editor to automatically create, load, or save the configuration for that subreddit's wiki.
|
||||
|
||||
* Visit the tab for the subreddit you want to edit the configuration of
|
||||
* Open the [built-in editor by click **View**](/docs/screenshots/configBox.png)
|
||||
* Open the [built-in editor by click **View**](/docs/images/configBox.png)
|
||||
* Edit your configuration
|
||||
* Follow the directions on the **Save to r/..** link found at the top of the editor to automatically save your configuration
|
||||
|
||||
|
||||
@@ -22,13 +22,14 @@ PROTIP: Using a container management tool like [Portainer.io CE](https://www.por
|
||||
|
||||
### [Dockerhub](https://hub.docker.com/r/foxxmd/context-mod)
|
||||
|
||||
```
|
||||
foxxmd/context-mod:latest
|
||||
```
|
||||
An example of starting the container using the [minimum configuration](/docs/operatorConfiguration.md#minimum-config) with a [configuration file](/docs/operatorConfiguration.md#defining-configuration-via-file):
|
||||
|
||||
* Bind the folder where the config is located on your host machine into the container `-v /host/path/folder:/config`
|
||||
* Tell CM where to find the config using an env `-e "OPERATOR_CONFIG=/config/myConfig.yaml"`
|
||||
* Expose the web interface using the container port `8085`
|
||||
|
||||
Adding **environmental variables** to your `docker run` command will pass them through to the app EX:
|
||||
```
|
||||
docker run -d -e "CLIENT_ID=myId" ... foxxmd/context-mod
|
||||
docker run -d -e "OPERATOR_CONFIG=/config/myConfig.yaml" -v /host/path/folder:/config -p 8085:8085 foxxmd/context-mod
|
||||
```
|
||||
|
||||
### Locally
|
||||
@@ -47,6 +48,12 @@ npm install
|
||||
tsc -p .
|
||||
```
|
||||
|
||||
An example of running CM using the [minimum configuration](/docs/operatorConfiguration.md#minimum-config) with a [configuration file](/docs/operatorConfiguration.md#defining-configuration-via-file):
|
||||
|
||||
```bash
|
||||
node src/index.js run
|
||||
```
|
||||
|
||||
### [Heroku Quick Deploy](https://heroku.com/about)
|
||||
[](https://dashboard.heroku.com/new?template=https://github.com/FoxxMD/context-mod)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/images/diagram-highlevel.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 479 KiB After Width: | Height: | Size: 479 KiB |
@@ -41,8 +41,10 @@ configuration.
|
||||
**Note:** When reading the **schema** if the variable is available at a level of configuration other than **FILE** it will be
|
||||
noted with the same symbol as above. The value shown is the default.
|
||||
|
||||
* To load a JSON configuration (for **FILE**) **from the command line** use the `-c` cli argument EX: `node src/index.js -c /path/to/JSON/config.json`
|
||||
* To load a JSON configuration (for **FILE**) **using an environmental variable** use `OPERATOR_CONFIG` EX: `OPERATOR_CONFIG=/path/to/JSON/config.json`
|
||||
## Defining Configuration Via File
|
||||
|
||||
* **from the command line** use the `-c` cli argument EX: `node src/index.js -c /path/to/JSON/config.json`
|
||||
* **using an environmental variable** use `OPERATOR_CONFIG` EX: `OPERATOR_CONFIG=/path/to/JSON/config.json`
|
||||
|
||||
[**See the Operator Config Schema here**](https://json-schema.app/view/%23?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fcontext-mod%2Fmaster%2Fsrc%2FSchema%2FOperatorConfig.json)
|
||||
|
||||
@@ -121,28 +123,41 @@ Below are examples of the minimum required config to run the application using a
|
||||
Using **FILE**
|
||||
<details>
|
||||
|
||||
CM will look for a file configuration at `PROJECT_DIR/config.yaml` by default [or you can specify your own location.](#defining-configuration-via-file)
|
||||
|
||||
YAML
|
||||
```yaml
|
||||
operator:
|
||||
name: YourRedditUsername
|
||||
bots:
|
||||
- credentials:
|
||||
clientId: f4b4df1c7b2
|
||||
clientSecret: 34v5q1c56ub
|
||||
refreshToken: 34_f1w1v4
|
||||
accessToken: p75_1c467b2
|
||||
web:
|
||||
credentials:
|
||||
clientId: f4b4df1c7b2
|
||||
clientSecret: 34v5q1c56ub
|
||||
```
|
||||
JSON
|
||||
```json5
|
||||
{
|
||||
"operator": {
|
||||
"name": "YourRedditUsername"
|
||||
},
|
||||
"bots": [
|
||||
{
|
||||
"credentials": {
|
||||
"clientId": "f4b4df1c7b2",
|
||||
"clientSecret": "34v5q1c56ub",
|
||||
"refreshToken": "34_f1w1v4",
|
||||
"accessToken": "p75_1c467b2"
|
||||
"clientSecret": "34v5q1c56ub"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"web": {
|
||||
"credentials": {
|
||||
"clientId": "f4b4df1c7b2",
|
||||
"clientSecret": "34v5q1c56ub"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -153,10 +168,9 @@ Using **ENV** (`.env`)
|
||||
<details>
|
||||
|
||||
```
|
||||
OPERATOR=YourRedditUsername
|
||||
CLIENT_ID=f4b4df1c7b2
|
||||
CLIENT_SECRET=34v5q1c56ub
|
||||
REFRESH_TOKEN=34_f1w1v4
|
||||
ACCESS_TOKEN=p75_1c467b2
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
## Editing/Updating Your Config
|
||||
|
||||
* Open the editor for your subreddit
|
||||
* In the web dashboard \-> r/YourSubreddit \-> Config -> **View** [(here)](/docs/screenshots/config/config.jpg)
|
||||
* Follow the directions on the [link at the top of the window](/docs/screenshots/config/save.png) to enable config editing using your moderator account
|
||||
* In the web dashboard \-> r/YourSubreddit \-> Config -> **View** [(here)](/docs/images/config/config.jpg)
|
||||
* Follow the directions on the [link at the top of the window](/docs/images/config/save.png) to enable config editing using your moderator account
|
||||
* After enabling editing just click "save" at any time to save your config
|
||||
* After you have added/edited your config the bot will detect changes within 5 minutes or you can manually trigger it by clicking **Update**
|
||||
|
||||
## General Config (Editor) Tips
|
||||
|
||||
* The editor will automatically validate your [syntax (formatting)](/docs/screenshots/config/syntax.png) and [config correctness](/docs/screenshots/config/correctness.png) (property names, required properties, etc.)
|
||||
* These show up as squiggly lines like in Microsoft Word and as a [list at the bottom of the editor](/docs/screenshots/config/errors.png)
|
||||
* The editor will automatically validate your [syntax (formatting)](/docs/images/config/syntax.png) and [config correctness](/docs/images/config/correctness.png) (property names, required properties, etc.)
|
||||
* These show up as squiggly lines like in Microsoft Word and as a [list at the bottom of the editor](/docs/images/config/errors.png)
|
||||
* In your config all **Checks** and **Actions** have two properties that control how they behave:
|
||||
* [**Enable**](/docs/screenshots/config/enable.png) (defaults to `enable: true`) -- Determines if the check or action is run, at all
|
||||
* [**Enable**](/docs/images/config/enable.png) (defaults to `enable: true`) -- Determines if the check or action is run, at all
|
||||
* **Dryrun** (defaults to `dryRun: false`) -- When `true` the check or action will run but any **Actions** that may be triggered will "pretend" to execute but not actually talk to the Reddit API.
|
||||
* Use `dryRun` to test your config without the bot making any changes on reddit
|
||||
* When starting out with a new config it is recommended running the bot with remove/ban actions **disabled**
|
||||
@@ -20,11 +20,11 @@
|
||||
|
||||
## Web Dashboard Tips
|
||||
|
||||
* Use the [**Overview** section](/docs/screenshots/botOperations.png) to control the bot at a high-level
|
||||
* You can **manually run** the bot on any activity (comment/submission) by pasting its permalink into the [input field below the Overview section](/docs/screenshots/runInput.png) and hitting one of the **run buttons**
|
||||
* Use the [**Overview** section](/docs/images/botOperations.png) to control the bot at a high-level
|
||||
* You can **manually run** the bot on any activity (comment/submission) by pasting its permalink into the [input field below the Overview section](/docs/images/runInput.png) and hitting one of the **run buttons**
|
||||
* **Dry run** will make the bot run on the activity but it will only **pretend** to run actions, if triggered. This is super useful for testing your config without consequences
|
||||
* **Run** will do everything
|
||||
* All of the bot's activity is shown in real-time in the [log section](/docs/screenshots/logs.png)
|
||||
* All of the bot's activity is shown in real-time in the [log section](/docs/images/logs.png)
|
||||
* This will output the results of all run checks/rules and any actions that run
|
||||
* You can view summaries of all activities that triggered a check (had actions run) by clicking on [Actioned Events](/docs/screenshots/actionsEvents.png)
|
||||
* You can view summaries of all activities that triggered a check (had actions run) by clicking on [Actioned Events](/docs/images/actionsEvents.png)
|
||||
* This includes activities run with dry run
|
||||
|
||||
132
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"leven": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^6.0.0",
|
||||
"migrate": "github:johsunds/node-migrate#49b0054de0a9295857aa8b8eea9a3cdeb2643913",
|
||||
"mustache": "^4.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"normalize-url": "^6.1.0",
|
||||
@@ -1430,6 +1431,14 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
|
||||
"integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.10.7",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
|
||||
@@ -1562,6 +1571,14 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
|
||||
"integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -2864,6 +2881,38 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/migrate": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "git+ssh://git@github.com/johsunds/node-migrate.git#49b0054de0a9295857aa8b8eea9a3cdeb2643913",
|
||||
"integrity": "sha512-qBdMfn2zo48Hno0UzS0G+iaYESTKP2Qkhw+T21ROnN14uwaoQYMZ2z3clay2FH52c/8hBLI+7OsbauQJfIKS6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"commander": "^2.19.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"inherits": "^2.0.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"slug": "^0.9.2"
|
||||
},
|
||||
"bin": {
|
||||
"migrate": "bin/migrate",
|
||||
"migrate-create": "bin/migrate-create",
|
||||
"migrate-down": "bin/migrate-down",
|
||||
"migrate-init": "bin/migrate-init",
|
||||
"migrate-list": "bin/migrate-list",
|
||||
"migrate-up": "bin/migrate-up"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/migrate/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -2918,6 +2967,17 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
@@ -3832,6 +3892,17 @@
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/slug": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/slug/-/slug-0.9.4.tgz",
|
||||
"integrity": "sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g==",
|
||||
"dependencies": {
|
||||
"unicode": ">= 0.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"slug": "bin/slug.js"
|
||||
}
|
||||
},
|
||||
"node_modules/snekfetch": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz",
|
||||
@@ -4367,6 +4438,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||
},
|
||||
"node_modules/unicode": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz",
|
||||
"integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -5867,6 +5946,11 @@
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
|
||||
"integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.10.7",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
|
||||
@@ -5959,6 +6043,11 @@
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
|
||||
"integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -6995,6 +7084,28 @@
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"migrate": {
|
||||
"version": "git+ssh://git@github.com/johsunds/node-migrate.git#49b0054de0a9295857aa8b8eea9a3cdeb2643913",
|
||||
"integrity": "sha512-qBdMfn2zo48Hno0UzS0G+iaYESTKP2Qkhw+T21ROnN14uwaoQYMZ2z3clay2FH52c/8hBLI+7OsbauQJfIKS6Q==",
|
||||
"from": "migrate@github:johsunds/node-migrate#49b0054de0a9295857aa8b8eea9a3cdeb2643913",
|
||||
"requires": {
|
||||
"chalk": "^2.4.1",
|
||||
"commander": "^2.19.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"inherits": "^2.0.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"slug": "^0.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
@@ -7031,6 +7142,14 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
@@ -7696,6 +7815,14 @@
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"slug": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/slug/-/slug-0.9.4.tgz",
|
||||
"integrity": "sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g==",
|
||||
"requires": {
|
||||
"unicode": ">= 0.3.1"
|
||||
}
|
||||
},
|
||||
"snekfetch": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz",
|
||||
@@ -8105,6 +8232,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||
},
|
||||
"unicode": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz",
|
||||
"integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg=="
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"leven": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^6.0.0",
|
||||
"migrate": "github:johsunds/node-migrate#49b0054de0a9295857aa8b8eea9a3cdeb2643913",
|
||||
"mustache": "^4.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"normalize-url": "^6.1.0",
|
||||
|
||||
@@ -52,6 +52,7 @@ export class CommentAction extends Action {
|
||||
};
|
||||
}
|
||||
const touchedEntities = [];
|
||||
let modifiers = [];
|
||||
let reply: Comment;
|
||||
if(!dryRun) {
|
||||
// @ts-ignore
|
||||
@@ -59,29 +60,29 @@ export class CommentAction extends Action {
|
||||
touchedEntities.push(reply);
|
||||
}
|
||||
if (this.lock) {
|
||||
modifiers.push('Locked');
|
||||
if (!dryRun) {
|
||||
// snoopwrap typing issue, thinks comments can't be locked
|
||||
// @ts-ignore
|
||||
await item.lock();
|
||||
touchedEntities.push(item);
|
||||
await reply.lock();
|
||||
}
|
||||
}
|
||||
if (this.distinguish && !dryRun) {
|
||||
// @ts-ignore
|
||||
await reply.distinguish({sticky: this.sticky});
|
||||
}
|
||||
let modifiers = [];
|
||||
if(this.distinguish) {
|
||||
modifiers.push('Distinguished');
|
||||
if(this.sticky) {
|
||||
modifiers.push('Stickied');
|
||||
}
|
||||
if(!dryRun) {
|
||||
// @ts-ignore
|
||||
await reply.distinguish({sticky: this.sticky});
|
||||
}
|
||||
}
|
||||
if(this.sticky) {
|
||||
modifiers.push('Stickied');
|
||||
}
|
||||
|
||||
const modifierStr = modifiers.length === 0 ? '' : `[${modifiers.join(' | ')}]`;
|
||||
return {
|
||||
dryRun,
|
||||
success: true,
|
||||
result: `${modifierStr}${this.lock ? ' - Locked Author\'s Activity - ' : ''}${truncateStringToLength(100)(body)}`,
|
||||
result: `${modifierStr}${truncateStringToLength(100)(body)}`,
|
||||
touchedEntities,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class UserFlairAction extends Action {
|
||||
const flairSummary = flairParts.length === 0 ? 'Unflair user' : flairParts.join(' | ');
|
||||
this.logger.verbose(flairSummary);
|
||||
|
||||
if (!this.dryRun) {
|
||||
if (!dryRun) {
|
||||
if (this.flair_template_id !== undefined) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
@@ -72,7 +72,9 @@ export class UserFlairAction extends Action {
|
||||
item.author_flair_css_class = this.css ?? null;
|
||||
}
|
||||
await this.resources.resetCacheForItem(item);
|
||||
await this.resources.resetCacheForItem(item.author);
|
||||
if(typeof item.author !== 'string') {
|
||||
await this.resources.resetCacheForItem(item.author);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -33,8 +33,15 @@ export class UserNoteAction extends Action {
|
||||
|
||||
if (!this.allowDuplicate) {
|
||||
const notes = await this.resources.userNotes.getUserNotes(item.author);
|
||||
const existingNote = notes.find((x) => x.link.includes(item.id));
|
||||
if (existingNote) {
|
||||
let existingNote = notes.find((x) => x.link !== null && x.link.includes(item.id));
|
||||
if(existingNote === undefined && notes.length > 0) {
|
||||
const lastNote = notes[notes.length - 1];
|
||||
// possibly notes don't have a reference link so check if last one has same text
|
||||
if(lastNote.link === null && lastNote.text === renderedContent) {
|
||||
existingNote = lastNote;
|
||||
}
|
||||
}
|
||||
if (existingNote !== undefined && existingNote.noteType === this.type) {
|
||||
this.logger.info(`Will not add note because one already exists for this Activity (${existingNote.time.local().format()}) and allowDuplicate=false`);
|
||||
return {
|
||||
dryRun,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Comment, Submission} from "snoowrap";
|
||||
import {Logger} from "winston";
|
||||
import {RuleResult} from "../Rule";
|
||||
import {checkAuthorFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {checkAuthorFilter, checkItemFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {ActionProcessResult, ActionResult, ChecksActivityState, TypedActivityStates} from "../Common/interfaces";
|
||||
import Author, {AuthorOptions} from "../Author/Author";
|
||||
import {mergeArr} from "../util";
|
||||
@@ -69,17 +69,24 @@ export abstract class Action {
|
||||
success: false,
|
||||
};
|
||||
try {
|
||||
const itemPass = await this.resources.testItemCriteria(item, this.itemIs);
|
||||
const [itemPass, itemFilterType, itemFilterResults] = await checkItemFilter(item, this.itemIs, this.resources, this.logger);
|
||||
if (!itemPass) {
|
||||
this.logger.verbose(`Activity did not pass 'itemIs' test, Action not run`);
|
||||
actRes.runReason = `Activity did not pass 'itemIs' test, Action not run`;
|
||||
actRes.itemIs = itemFilterResults;
|
||||
return actRes;
|
||||
} else if(this.itemIs.length > 0) {
|
||||
actRes.itemIs = itemFilterResults;
|
||||
}
|
||||
const [authFilterResult, authFilterType] = await checkAuthorFilter(item, this.authorIs, this.resources, this.logger);
|
||||
if(!authFilterResult) {
|
||||
|
||||
const [authPass, authFilterType, authorFilterResult] = await checkAuthorFilter(item, this.authorIs, this.resources, this.logger);
|
||||
if(!authPass) {
|
||||
this.logger.verbose(`${authFilterType} author criteria not matched, Action not run`);
|
||||
actRes.runReason = `${authFilterType} author criteria not matched`;
|
||||
actRes.authorIs = authorFilterResult;
|
||||
return actRes;
|
||||
} else if(authFilterType !== undefined) {
|
||||
actRes.authorIs = authorFilterResult;
|
||||
}
|
||||
|
||||
actRes.run = true;
|
||||
|
||||
@@ -6,7 +6,7 @@ import EventEmitter from "events";
|
||||
import {
|
||||
BotInstanceConfig,
|
||||
FilterCriteriaDefaults,
|
||||
Invokee,
|
||||
Invokee, LogInfo,
|
||||
PAUSED,
|
||||
PollOn,
|
||||
RUNNING,
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
USER
|
||||
} from "../Common/interfaces";
|
||||
import {
|
||||
createRetryHandler,
|
||||
formatNumber, getExceptionMessage,
|
||||
createRetryHandler, difference,
|
||||
formatNumber, getExceptionMessage, getUserAgent,
|
||||
mergeArr,
|
||||
parseBool,
|
||||
parseDuration, parseMatchMessage,
|
||||
parseDuration, parseMatchMessage, parseRedditEntity,
|
||||
parseSubredditName, RetryOptions,
|
||||
sleep,
|
||||
snooLogWrapper
|
||||
@@ -38,6 +38,7 @@ class Bot {
|
||||
|
||||
client!: ExtendedSnoowrap;
|
||||
logger!: Logger;
|
||||
logs: LogInfo[] = [];
|
||||
wikiLocation: string;
|
||||
dryRun?: true | undefined;
|
||||
running: boolean = false;
|
||||
@@ -78,6 +79,8 @@ class Bot {
|
||||
|
||||
cacheManager: BotResourcesManager;
|
||||
|
||||
config: BotInstanceConfig;
|
||||
|
||||
getBotName = () => {
|
||||
return this.botName;
|
||||
}
|
||||
@@ -98,6 +101,7 @@ class Bot {
|
||||
dryRun,
|
||||
heartbeatInterval,
|
||||
},
|
||||
userAgent,
|
||||
credentials: {
|
||||
reddit: {
|
||||
clientId,
|
||||
@@ -129,8 +133,7 @@ class Bot {
|
||||
}
|
||||
} = config;
|
||||
|
||||
this.cacheManager = new BotResourcesManager(config);
|
||||
|
||||
this.config = config;
|
||||
this.dryRun = parseBool(dryRun) === true ? true : undefined;
|
||||
this.softLimit = softLimit;
|
||||
this.hardLimit = hardLimit;
|
||||
@@ -151,6 +154,14 @@ class Bot {
|
||||
}
|
||||
}, mergeArr);
|
||||
|
||||
this.logger.stream().on('log', (log: LogInfo) => {
|
||||
if(log.bot !== undefined && log.bot === this.getBotName() && log.subreddit === undefined) {
|
||||
this.logs = [log, ...this.logs].slice(0, 301);
|
||||
}
|
||||
});
|
||||
|
||||
this.cacheManager = new BotResourcesManager(config, this.logger);
|
||||
|
||||
let mw = maxWorkers;
|
||||
if(maxWorkers < 1) {
|
||||
this.logger.warn(`Max queue workers must be greater than or equal to 1 (Specified: ${maxWorkers})`);
|
||||
@@ -166,7 +177,9 @@ class Bot {
|
||||
this.excludeSubreddits = exclude.map(parseSubredditName);
|
||||
|
||||
let creds: any = {
|
||||
get userAgent() { return getUserName() },
|
||||
get userAgent() {
|
||||
return getUserAgent(`web:contextBot:{VERSION}{FRAG}:BOT-${getBotName()}`, userAgent)
|
||||
},
|
||||
clientId,
|
||||
clientSecret,
|
||||
refreshToken,
|
||||
@@ -340,6 +353,31 @@ class Bot {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
subreddits: {
|
||||
overrides = [],
|
||||
} = {}
|
||||
} = this.config;
|
||||
if(overrides.length > 0) {
|
||||
// check for overrides that don't match subs to run and warn operator
|
||||
const subsToRunNames = subsToRun.map(x => x.display_name.toLowerCase());
|
||||
|
||||
const normalizedOverrideNames = overrides.reduce((acc: string[], curr) => {
|
||||
try {
|
||||
const ent = parseRedditEntity(curr.name);
|
||||
return acc.concat(ent.name.toLowerCase());
|
||||
} catch (e) {
|
||||
this.logger.warn(new ErrorWithCause(`Could not use subreddit override because name was not valid: ${curr.name}`, {cause: e}));
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
const notMatched = difference(normalizedOverrideNames, subsToRunNames);
|
||||
if(notMatched.length > 0) {
|
||||
this.logger.warn(`There are overrides defined for subreddits the bot is not running. Check your spelling! Overrides not matched: ${notMatched.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get configs for subs we want to run on and build/validate them
|
||||
for (const sub of subsToRun) {
|
||||
try {
|
||||
@@ -477,6 +515,29 @@ class Bot {
|
||||
}
|
||||
|
||||
createManager(sub: Subreddit): Manager {
|
||||
const {
|
||||
flowControlDefaults: {
|
||||
maxGotoDepth: botMaxDefault
|
||||
} = {},
|
||||
subreddits: {
|
||||
overrides = [],
|
||||
} = {}
|
||||
} = this.config;
|
||||
|
||||
const override = overrides.find(x => {
|
||||
const configName = parseRedditEntity(x.name).name;
|
||||
if(configName !== undefined) {
|
||||
return configName.toLowerCase() === sub.display_name.toLowerCase();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const {
|
||||
flowControlDefaults: {
|
||||
maxGotoDepth: subMax = undefined,
|
||||
} = {}
|
||||
} = override || {};
|
||||
|
||||
const manager = new Manager(sub, this.client, this.logger, this.cacheManager, {
|
||||
dryRun: this.dryRun,
|
||||
sharedStreams: this.sharedStreams,
|
||||
@@ -484,6 +545,7 @@ class Bot {
|
||||
botName: this.botName as string,
|
||||
maxWorkers: this.maxWorkers,
|
||||
filterCriteriaDefaults: this.filterCriteriaDefaults,
|
||||
maxGotoDepth: subMax ?? botMaxDefault
|
||||
});
|
||||
// all errors from managers will count towards bot-level retry count
|
||||
manager.on('error', async (err) => await this.panicOnRetries(err));
|
||||
|
||||
@@ -7,7 +7,7 @@ import {actionFactory} from "../Action/ActionFactory";
|
||||
import {ruleFactory} from "../Rule/RuleFactory";
|
||||
import {
|
||||
boolToString,
|
||||
createAjvFactory,
|
||||
createAjvFactory, determineNewResults,
|
||||
FAIL,
|
||||
mergeArr,
|
||||
PASS,
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
truncateStringToLength
|
||||
} from "../util";
|
||||
import {
|
||||
ActionResult,
|
||||
ChecksActivityState,
|
||||
ActionResult, ActivityType, CheckResult,
|
||||
ChecksActivityState, CheckSummary,
|
||||
CommentState,
|
||||
JoinCondition,
|
||||
JoinOperands,
|
||||
JoinOperands, NotificationEventPayload, PostBehavior, PostBehaviorTypes,
|
||||
SubmissionState,
|
||||
TypedActivityStates, UserResultCache
|
||||
} from "../Common/interfaces";
|
||||
@@ -28,11 +28,14 @@ import * as RuleSchema from '../Schema/Rule.json';
|
||||
import * as RuleSetSchema from '../Schema/RuleSet.json';
|
||||
import * as ActionSchema from '../Schema/Action.json';
|
||||
import {ActionObjectJson, RuleJson, RuleObjectJson, ActionJson as ActionTypeJson} from "../Common/types";
|
||||
import {checkAuthorFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {checkAuthorFilter, checkItemFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {Author, AuthorCriteria, AuthorOptions} from '..';
|
||||
import {ExtendedSnoowrap} from '../Utils/SnoowrapClients';
|
||||
import {isRateLimitError} from "../Utils/Errors";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {CheckProcessingError, isRateLimitError} from "../Utils/Errors";
|
||||
import {ErrorWithCause, stackWithCauses} from "pony-cause";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import EventEmitter from "events";
|
||||
import {itemContentPeek} from "../Utils/SnoowrapUtils";
|
||||
|
||||
const checkLogName = truncateStringToLength(25);
|
||||
|
||||
@@ -51,9 +54,13 @@ export abstract class Check implements ICheck {
|
||||
notifyOnTrigger: boolean;
|
||||
resources: SubredditResources;
|
||||
client: ExtendedSnoowrap;
|
||||
postTrigger: PostBehaviorTypes;
|
||||
postFail: PostBehaviorTypes;
|
||||
emitter: EventEmitter;
|
||||
|
||||
constructor(options: CheckOptions) {
|
||||
const {
|
||||
emitter,
|
||||
enable = true,
|
||||
name,
|
||||
resources,
|
||||
@@ -65,6 +72,8 @@ export abstract class Check implements ICheck {
|
||||
notifyOnTrigger = false,
|
||||
subredditName,
|
||||
cacheUserResult = {},
|
||||
postTrigger = 'nextRun',
|
||||
postFail = 'next',
|
||||
itemIs = [],
|
||||
authorIs: {
|
||||
include = [],
|
||||
@@ -75,6 +84,7 @@ export abstract class Check implements ICheck {
|
||||
} = options;
|
||||
|
||||
this.enabled = enable;
|
||||
this.emitter = emitter;
|
||||
|
||||
this.logger = options.logger.child({labels: [`CHK ${checkLogName(name)}`]}, mergeArr);
|
||||
|
||||
@@ -93,6 +103,8 @@ export abstract class Check implements ICheck {
|
||||
exclude: exclude.map(x => new Author(x)),
|
||||
include: include.map(x => new Author(x)),
|
||||
}
|
||||
this.postTrigger = postTrigger;
|
||||
this.postFail = postFail;
|
||||
this.cacheUserResult = {
|
||||
...userResultCacheDefault,
|
||||
...cacheUserResult
|
||||
@@ -186,31 +198,192 @@ export abstract class Check implements ICheck {
|
||||
async setCacheResult(item: Submission | Comment, result: UserResultCache): Promise<void> {
|
||||
}
|
||||
|
||||
async runRules(item: Submission | Comment, existingResults: RuleResult[] = []): Promise<[boolean, RuleResult[], boolean?]> {
|
||||
async handle(activity: (Submission | Comment), allRuleResults: RuleResult[], options: runCheckOptions = {}): Promise<CheckSummary> {
|
||||
|
||||
let checkSum: CheckSummary = {
|
||||
name: this.name,
|
||||
run: this.name,
|
||||
actionResults: [],
|
||||
ruleResults: [],
|
||||
postBehavior: 'next',
|
||||
fromCache: false,
|
||||
triggered: false,
|
||||
condition: this.condition
|
||||
}
|
||||
|
||||
let currentResults: RuleResult[] = [];
|
||||
|
||||
try {
|
||||
if (!this.enabled) {
|
||||
checkSum.error = 'Not enabled';
|
||||
this.logger.info(`Not enabled, skipping...`);
|
||||
return checkSum;
|
||||
}
|
||||
//checksRunNames.push(check.name);
|
||||
//checksRun++;
|
||||
let triggered = false;
|
||||
let runActions: ActionResult[] = [];
|
||||
let checkRes: CheckResult;
|
||||
let checkError: string | undefined;
|
||||
try {
|
||||
checkRes = await this.runRules(activity, allRuleResults);
|
||||
|
||||
checkSum = {
|
||||
...checkSum,
|
||||
...checkRes,
|
||||
}
|
||||
const {
|
||||
triggered: checkTriggered,
|
||||
ruleResults: checkResults,
|
||||
fromCache = false
|
||||
} = checkRes;
|
||||
//isFromCache = fromCache;
|
||||
if (!fromCache) {
|
||||
await this.setCacheResult(activity, {result: checkTriggered, ruleResults: checkResults});
|
||||
} else {
|
||||
checkRes.fromCache = true;
|
||||
//cachedCheckNames.push(check.name);
|
||||
}
|
||||
currentResults = checkResults;
|
||||
//totalRulesRun += checkResults.length;
|
||||
// allRuleResults = allRuleResults.concat(determineNewResults(allRuleResults, checkResults));
|
||||
if (triggered && fromCache && !this.cacheUserResult.runActions) {
|
||||
this.logger.info('Check was triggered but cache result options specified NOT to run actions...counting as check NOT triggered');
|
||||
checkSum.triggered = false;
|
||||
triggered = false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
checkSum.error = `Running rules failed due to uncaught exception: ${err.message}`;
|
||||
const chkLogError = new ErrorWithCause(`[CHK ${this.name}] Running rules failed due to uncaught exception`, {cause: err});
|
||||
if (err.logged !== true) {
|
||||
this.logger.warn(chkLogError);
|
||||
}
|
||||
this.emitter.emit('error', chkLogError);
|
||||
}
|
||||
|
||||
let behaviorT: string;
|
||||
|
||||
if (checkSum.triggered) {
|
||||
try {
|
||||
checkSum.postBehavior = this.postTrigger;
|
||||
|
||||
checkSum.actionResults = await this.runActions(activity, currentResults.filter(x => x.triggered), options.dryRun);
|
||||
// we only can about report and comment actions since those can produce items for newComm and modqueue
|
||||
const recentCandidates = checkSum.actionResults.filter(x => ['report', 'comment'].includes(x.kind.toLocaleLowerCase())).map(x => x.touchedEntities === undefined ? [] : x.touchedEntities).flat();
|
||||
for (const recent of recentCandidates) {
|
||||
await this.resources.setRecentSelf(recent as (Submission | Comment));
|
||||
}
|
||||
//actionsRun = runActions.length;
|
||||
|
||||
if (this.notifyOnTrigger) {
|
||||
const ar = checkSum.actionResults.filter(x => x.success).map(x => x.name).join(', ');
|
||||
const [peek, _] = await itemContentPeek(activity);
|
||||
const notifPayload: NotificationEventPayload = {
|
||||
type: 'eventActioned',
|
||||
title: 'Check Triggered',
|
||||
body: `Check "${this.name}" was triggered on Event: \n\n ${peek} \n\n with the following actions run: ${ar}`
|
||||
}
|
||||
this.emitter.emit('notify', notifPayload)
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.emitter.emit('error', err);
|
||||
checkSum.error = `Running actions failed due to uncaught exception: ${err.message}`;
|
||||
if (err.logged !== true) {
|
||||
const chkLogError = new ErrorWithCause(`[CHK ${this.name}] Running actions failed due to uncaught exception`, {cause: err});
|
||||
this.logger.warn(chkLogError);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkSum.postBehavior = this.postFail;
|
||||
}
|
||||
|
||||
behaviorT = checkSum.triggered ? 'Trigger' : 'Fail';
|
||||
|
||||
switch (checkSum.postBehavior.toLowerCase()) {
|
||||
case 'next':
|
||||
this.logger.debug('Behavior => NEXT => Run next check', {leaf: `Post Check ${behaviorT}`});
|
||||
break;
|
||||
case 'nextrun':
|
||||
this.logger.debug('Behavior => NEXT RUN => Skip remaining checks and go to next Run', {leaf: `Post Check ${behaviorT}`});
|
||||
break;
|
||||
case 'stop':
|
||||
this.logger.debug('Behavior => STOP => Immediately stop current Run and skip all remaining runs', {leaf: `Post Check ${behaviorT}`});
|
||||
break;
|
||||
default:
|
||||
if (checkSum.postBehavior.includes('goto:')) {
|
||||
const gotoContext = checkSum.postBehavior.split(':')[1];
|
||||
this.logger.debug(`Behavior => GOTO => ${gotoContext}`, {leaf: `Post Check ${behaviorT}`});
|
||||
} else {
|
||||
throw new Error(`Post ${behaviorT} Behavior "${checkSum.postBehavior}" was not a valid value. Must be one of => next | nextRun | stop | goto:[path]`);
|
||||
}
|
||||
}
|
||||
return checkSum;
|
||||
} catch (err: any) {
|
||||
if(checkSum.error === undefined) {
|
||||
checkSum.error = stackWithCauses(err);
|
||||
}
|
||||
throw new CheckProcessingError(`[CHK ${this.name}] An uncaught exception occurred while processing Check`, {cause: err}, checkSum);
|
||||
} finally {
|
||||
this.resources.updateHistoricalStats({
|
||||
checksTriggered: checkSum.triggered ? [checkSum.name] : [],
|
||||
checksRun: [checkSum.name],
|
||||
checksFromCache: checkSum.fromCache ? [checkSum.name] : [],
|
||||
actionsRun: checkSum.actionResults.map(x => x.name),
|
||||
rulesRun: checkSum.ruleResults.map(x => x.name),
|
||||
rulesTriggered: checkSum.ruleResults.filter(x => x.triggered).map(x => x.name),
|
||||
rulesCachedTotal: checkSum.ruleResults.filter(x => x.fromCache).length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async runRules(item: Submission | Comment, existingResults: RuleResult[] = []): Promise<CheckResult> {
|
||||
try {
|
||||
let allRuleResults: RuleResult[] = [];
|
||||
let allResults: (RuleResult | RuleSetResult)[] = [];
|
||||
|
||||
const checkResult: CheckResult = {
|
||||
triggered: false,
|
||||
ruleResults: [],
|
||||
}
|
||||
|
||||
// check cache results
|
||||
const cacheResult = await this.getCacheResult(item);
|
||||
if(cacheResult !== undefined) {
|
||||
this.logger.verbose(`Skipping rules run because result was found in cache, Check Triggered Result: ${cacheResult}`);
|
||||
return [cacheResult.result, cacheResult.ruleResults, true];
|
||||
return {
|
||||
triggered: cacheResult.result,
|
||||
ruleResults: cacheResult.ruleResults,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
const itemPass = await this.resources.testItemCriteria(item, this.itemIs);
|
||||
const [itemPass, itemFilterType, itemFilterResults] = await checkItemFilter(item, this.itemIs, this.resources, this.logger);
|
||||
if (!itemPass) {
|
||||
this.logger.verbose(`${FAIL} => Item did not pass 'itemIs' test`);
|
||||
return [false, allRuleResults];
|
||||
return {
|
||||
triggered: false,
|
||||
ruleResults: allRuleResults,
|
||||
itemIs: itemFilterResults
|
||||
};
|
||||
} else if(this.itemIs.length > 0) {
|
||||
checkResult.itemIs = itemFilterResults;
|
||||
}
|
||||
const [authFilterResult, authFilterType] = await checkAuthorFilter(item, this.authorIs, this.resources, this.logger);
|
||||
if(!authFilterResult) {
|
||||
return Promise.resolve([false, allRuleResults]);
|
||||
const [authPass, authFilterType, authorFilterResults] = await checkAuthorFilter(item, this.authorIs, this.resources, this.logger);
|
||||
if(!authPass) {
|
||||
return {
|
||||
triggered: false,
|
||||
ruleResults: allRuleResults,
|
||||
authorIs: authorFilterResults
|
||||
};
|
||||
} else if(authFilterType !== undefined) {
|
||||
checkResult.authorIs = authorFilterResults;
|
||||
}
|
||||
|
||||
if (this.rules.length === 0) {
|
||||
this.logger.info(`${PASS} => No rules to run, check auto-passes`);
|
||||
return [true, allRuleResults];
|
||||
return {
|
||||
triggered: true,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
}
|
||||
|
||||
let runOne = false;
|
||||
@@ -231,24 +404,39 @@ export abstract class Check implements ICheck {
|
||||
if (passed) {
|
||||
if (this.condition === 'OR') {
|
||||
this.logger.info(`${PASS} => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [true, allRuleResults];
|
||||
return {
|
||||
triggered: true,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
}
|
||||
} else if (this.condition === 'AND') {
|
||||
this.logger.verbose(`${FAIL} => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [false, allRuleResults];
|
||||
return {
|
||||
triggered: false,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!runOne) {
|
||||
this.logger.verbose(`${FAIL} => All Rules skipped because of Author checks or itemIs tests`);
|
||||
return [false, allRuleResults];
|
||||
return {
|
||||
triggered: false,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
} else if (this.condition === 'OR') {
|
||||
// if OR and did not return already then none passed
|
||||
this.logger.verbose(`${FAIL} => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [false, allRuleResults];
|
||||
return {
|
||||
triggered: false,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
}
|
||||
// otherwise AND and did not return already so all passed
|
||||
this.logger.info(`${PASS} => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [true, allRuleResults];
|
||||
return {
|
||||
triggered: true,
|
||||
ruleResults: allRuleResults,
|
||||
};
|
||||
} catch (e: any) {
|
||||
throw new ErrorWithCause('Running rules failed due to error', {cause: e});
|
||||
}
|
||||
@@ -279,7 +467,7 @@ export abstract class Check implements ICheck {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICheck extends JoinCondition, ChecksActivityState {
|
||||
export interface ICheck extends JoinCondition, ChecksActivityState, PostBehavior {
|
||||
/**
|
||||
* Friendly name for this Check EX "crosspostSpamCheck"
|
||||
*
|
||||
@@ -332,6 +520,7 @@ export interface CheckOptions extends ICheck {
|
||||
resources: SubredditResources;
|
||||
client: ExtendedSnoowrap;
|
||||
cacheUserResult?: UserResultCacheOptions;
|
||||
emitter: EventEmitter
|
||||
}
|
||||
|
||||
export interface CheckJson extends ICheck {
|
||||
@@ -339,7 +528,7 @@ export interface CheckJson extends ICheck {
|
||||
* The type of event (new submission or new comment) this check should be run against
|
||||
* @examples ["submission", "comment"]
|
||||
*/
|
||||
kind: 'submission' | 'comment'
|
||||
kind: ActivityType
|
||||
/**
|
||||
* A list of Rules to run.
|
||||
*
|
||||
|
||||
95
src/Common/Migrations/Cache/1644350232664-addActivityType.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {Cache} from 'cache-manager';
|
||||
import {ActionedEvent, CheckSummary, RunResult} from "../../interfaces";
|
||||
import {
|
||||
COMMENT_URL_ID,
|
||||
parseLinkIdentifier,
|
||||
parseStringToRegex,
|
||||
redisScanIterator,
|
||||
SUBMISSION_URL_ID
|
||||
} from "../../../util";
|
||||
const commentReg = parseLinkIdentifier([COMMENT_URL_ID]);
|
||||
const submissionReg = parseLinkIdentifier([SUBMISSION_URL_ID]);
|
||||
|
||||
const commentPeekHint = new RegExp(/by .+ in/i);
|
||||
|
||||
export const up = async (context: any, next: any) => {
|
||||
const client = context.client as Cache;
|
||||
const prefix = context.prefix as string | undefined;
|
||||
|
||||
const subredditEventMap: Record<string, any[] | undefined> = {};
|
||||
|
||||
// @ts-ignore
|
||||
if(client.store.name === 'redis') {
|
||||
// @ts-ignore
|
||||
for await (const key of redisScanIterator(client.store.getClient(), { MATCH: `${prefix !== undefined ? prefix : ''}actionedEvents-*` })) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
} else if(client.store.keys !== undefined) {
|
||||
const eventsReg = parseStringToRegex(`/${prefix !== undefined ? prefix : ''}actionedEvents-.*/i`) as RegExp;
|
||||
for (const key of await client.store.keys()) {
|
||||
if(eventsReg.test(key)) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(subredditEventMap)) {
|
||||
const oldEvents = v;
|
||||
if (oldEvents === null || oldEvents === undefined) {
|
||||
continue;
|
||||
}
|
||||
const newEvents = (oldEvents as any[]).map(x => {
|
||||
const {
|
||||
activity,
|
||||
subreddit,
|
||||
author,
|
||||
...rest
|
||||
} = x;
|
||||
|
||||
const {
|
||||
peek,
|
||||
link,
|
||||
} = activity;
|
||||
|
||||
let actType;
|
||||
let id;
|
||||
|
||||
try {
|
||||
// this *should* work
|
||||
const commentId = commentReg(`https://reddit.com${link}`);
|
||||
if(commentId === undefined) {
|
||||
const submissionId = submissionReg(`https://reddit.com${link}`);
|
||||
actType = 'submission';
|
||||
id = submissionId;
|
||||
} else {
|
||||
actType = 'comment';
|
||||
id = commentId;
|
||||
}
|
||||
} catch(e: any) {
|
||||
// but if it doesn't fall back to looking for 'in' in the peek since that means "comment in submission"
|
||||
actType = commentPeekHint.test(peek as string) ? 'comment' : 'submission';
|
||||
}
|
||||
|
||||
const result: ActionedEvent = {
|
||||
activity: {
|
||||
peek,
|
||||
link,
|
||||
type: actType,
|
||||
id,
|
||||
subreddit,
|
||||
author
|
||||
},
|
||||
subreddit,
|
||||
...rest,
|
||||
}
|
||||
return result;
|
||||
});
|
||||
await client.set(k, newEvents, {ttl: 0});
|
||||
}
|
||||
}
|
||||
|
||||
export const down = async (context: any, next: any) => {
|
||||
// backwards compatible with previous structure, not needed
|
||||
}
|
||||
120
src/Common/Migrations/Cache/1644350232664-init.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {Cache} from 'cache-manager';
|
||||
import {ActionedEvent, CheckSummary, RunResult} from "../../interfaces";
|
||||
import {parseStringToRegex, redisScanIterator} from "../../../util";
|
||||
|
||||
export const up = async (context: any, next: any) => {
|
||||
const client = context.client as Cache;
|
||||
const prefix = context.prefix as string | undefined;
|
||||
|
||||
const subredditEventMap: Record<string, any[] | undefined> = {};
|
||||
|
||||
// @ts-ignore
|
||||
if(client.store.name === 'redis') {
|
||||
// @ts-ignore
|
||||
for await (const key of redisScanIterator(client.store.getClient(), { MATCH: `${prefix !== undefined ? prefix : ''}actionedEvents-*` })) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
} else if(client.store.keys !== undefined) {
|
||||
const eventsReg = parseStringToRegex(`/${prefix !== undefined ? prefix : ''}actionedEvents-.*/i`) as RegExp;
|
||||
for (const key of await client.store.keys()) {
|
||||
if(eventsReg.test(key)) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(subredditEventMap)) {
|
||||
const oldEvents = v;
|
||||
if (oldEvents === null || oldEvents === undefined) {
|
||||
continue;
|
||||
}
|
||||
const newEvents = (oldEvents as any[]).map(x => {
|
||||
const {
|
||||
ruleSummary,
|
||||
ruleResults = [],
|
||||
actionResults = [],
|
||||
check,
|
||||
...rest
|
||||
} = x;
|
||||
if (check === undefined || check === null) {
|
||||
// probably new structure, leave it alone
|
||||
return x;
|
||||
}
|
||||
// otherwise wrap in dummy run
|
||||
const result: ActionedEvent = {
|
||||
...rest,
|
||||
runResults: [
|
||||
{
|
||||
name: 'Run1',
|
||||
triggered: true,
|
||||
checkResults: [
|
||||
{
|
||||
name: check,
|
||||
run: 'Run1',
|
||||
postBehavior: 'nextRun',
|
||||
triggered: true,
|
||||
condition: ruleSummary.includes('OR') ? 'OR' : 'AND',
|
||||
ruleResults,
|
||||
actionResults,
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
return result;
|
||||
});
|
||||
await client.set(k, newEvents, {ttl: 0});
|
||||
}
|
||||
}
|
||||
|
||||
export const down = async (context: any, next: any) => {
|
||||
const client = context.client as Cache;
|
||||
const prefix = context.prefix as string | undefined;
|
||||
|
||||
const subredditEventMap: Record<string, any[] | undefined> = {};
|
||||
|
||||
// @ts-ignore
|
||||
if(client.store.name === 'redis') {
|
||||
// @ts-ignore
|
||||
for await (const key of redisScanIterator(client.store.getClient(), { MATCH: `${prefix !== undefined ? prefix : ''}actionedEvents-*` })) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
} else if(client.store.keys !== undefined) {
|
||||
const eventsReg = parseStringToRegex(`/${prefix !== undefined ? prefix : ''}actionedEvents-.*/i`) as RegExp;
|
||||
for (const key of await client.store.keys()) {
|
||||
if(eventsReg.test(key)) {
|
||||
const nonPrefixedKey = prefix !== undefined ? key.replace(prefix, '') : key;
|
||||
subredditEventMap[nonPrefixedKey] = await client.get(nonPrefixedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(subredditEventMap)) {
|
||||
const oldEvents = v;
|
||||
if (oldEvents === null || oldEvents === undefined) {
|
||||
continue;
|
||||
}
|
||||
// don't want to lose any multi-check events so create one event per check
|
||||
const newEvents = (oldEvents as any[]).reduce((acc, curr) => {
|
||||
if (curr.check !== undefined) {
|
||||
// its an old event so just return it
|
||||
acc.push(curr);
|
||||
return acc;
|
||||
}
|
||||
const {runResults = [], ...rest} = curr;
|
||||
const singleEvents = (runResults as RunResult[]).map(y => {
|
||||
return {
|
||||
...rest,
|
||||
ruleResults: y.checkResults[0].ruleResults,
|
||||
actionResults: y.checkResults[0].actionResults,
|
||||
check: y.name,
|
||||
}
|
||||
});
|
||||
return acc.concat(singleEvents);
|
||||
}, []);
|
||||
await client.set(k, newEvents, {ttl: 0});
|
||||
}
|
||||
}
|
||||
69
src/Common/Migrations/CacheMigrationUtils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {Cache} from 'cache-manager';
|
||||
import {Logger} from "winston";
|
||||
import {mergeArr} from "../../util";
|
||||
import * as migrate from 'migrate';
|
||||
import path from "path";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
|
||||
export const cacheMigrationStorage = (client: Cache, resourceLogger: Logger) => {
|
||||
const logger = resourceLogger.child({leaf: 'Cache Migration'}, mergeArr);
|
||||
|
||||
return {
|
||||
load: async function (fn: any) {
|
||||
|
||||
const migrationData = await client.get('migrations');
|
||||
|
||||
if (migrationData === null || migrationData === undefined) {
|
||||
logger.debug('No migration data exists (normal if cache is memory or first-run with anything else)');
|
||||
return fn(null, {})
|
||||
}
|
||||
fn(null, migrationData);
|
||||
},
|
||||
|
||||
save: async function (set: any, fn: any) {
|
||||
|
||||
await client.set('migrations', {lastRun: set.lastRun, migrations: set.migrations}, {ttl: 0});
|
||||
fn()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// with the context stuff use it like this
|
||||
// migrate.load({
|
||||
// stateStore: cacheMigrationStorage(client, logger)
|
||||
// }, (err, set) => {
|
||||
// set.migrate('up', null, (err) => {
|
||||
//
|
||||
// }, {client, subreddit });
|
||||
// });
|
||||
|
||||
export const migrationDir = path.resolve(__dirname, 'Cache');
|
||||
|
||||
export const runMigrations = async (cache: Cache, logger: Logger, prefix?: string) => {
|
||||
const stateStore = cacheMigrationStorage(cache, logger);
|
||||
const context = {client: cache, prefix};
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
migrate.load({
|
||||
migrationsDirectory: migrationDir,
|
||||
stateStore,
|
||||
filterFunction: (file) => {
|
||||
return file.substring(file.length - 3) === '.js';
|
||||
},
|
||||
}, (err, set) => {
|
||||
set.on('migration', function (migration, direction) {
|
||||
logger.debug(`${direction}: ${migration.title}`, {leaf: 'Cache Migration'});
|
||||
});
|
||||
set.migrate('up', null, (err) => {
|
||||
if (err) {
|
||||
const migError = new ErrorWithCause('Failed to complete cache migrations', {cause: err});
|
||||
logger.error(migError);
|
||||
reject(err);
|
||||
} else {
|
||||
logger.debug('Migrations completed', {leaf: 'Cache Migration'});
|
||||
resolve();
|
||||
}
|
||||
// @ts-ignore
|
||||
}, context);
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -39,3 +39,5 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const VERSION = '0.10.12';
|
||||
|
||||
@@ -16,6 +16,8 @@ import {JsonOperatorConfigDocument, YamlOperatorConfigDocument} from "./Config/O
|
||||
import {ConsoleTransportOptions} from "winston/lib/winston/transports";
|
||||
import {DailyRotateFileTransportOptions} from "winston-daily-rotate-file";
|
||||
import {DuplexTransportOptions} from "winston-duplex/dist/DuplexTransport";
|
||||
import {CommentCheckJson, SubmissionCheckJson} from "../Check";
|
||||
import {SafeDictionary} from "ts-essentials";
|
||||
|
||||
/**
|
||||
* An ISO 8601 Duration
|
||||
@@ -836,6 +838,16 @@ export interface ManagerOptions {
|
||||
* Default behavior is to exclude all mods and automoderator from checks
|
||||
* */
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
|
||||
/**
|
||||
* Set the default post-check behavior for all checks. If this property is specified it will override any defaults passed from the bot's config
|
||||
*
|
||||
* Default behavior is:
|
||||
*
|
||||
* * postFail => next
|
||||
* * postTrigger => nextRun
|
||||
* */
|
||||
postCheckBehaviorDefaults?: PostBehavior
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1018,7 +1030,9 @@ export interface StrongSubredditState extends SubredditState {
|
||||
name?: RegExp
|
||||
}
|
||||
|
||||
export type TypedActivityStates = SubmissionState[] | CommentState[];
|
||||
export type TypedActivityState = SubmissionState | CommentState;
|
||||
|
||||
export type TypedActivityStates = TypedActivityState[];
|
||||
|
||||
export interface DomainInfo {
|
||||
display: string,
|
||||
@@ -1250,6 +1264,14 @@ export type NotificationProvider = 'discord';
|
||||
|
||||
export type NotificationEventType = 'runStateChanged' | 'pollingError' | 'eventActioned' | 'configUpdated'
|
||||
|
||||
export interface NotificationEventPayload {
|
||||
type: NotificationEventType,
|
||||
title: string
|
||||
body?: string
|
||||
causedBy?: string
|
||||
logLevel?: string
|
||||
}
|
||||
|
||||
export interface NotificationProviderConfig {
|
||||
name: string
|
||||
type: NotificationProvider
|
||||
@@ -1445,6 +1467,13 @@ export interface FilterCriteriaDefaults {
|
||||
authorIsBehavior?: FilterCriteriaDefaultBehavior
|
||||
}
|
||||
|
||||
export interface SubredditOverrides {
|
||||
name: string
|
||||
flowControlDefaults?: {
|
||||
maxGotoDepth?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration for an **individual reddit account** ContextMod will run as a bot.
|
||||
*
|
||||
@@ -1480,6 +1509,12 @@ export interface BotInstanceJsonConfig {
|
||||
* */
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
|
||||
postCheckBehaviorDefaults?: PostBehavior
|
||||
|
||||
flowControlDefaults?: {
|
||||
maxGotoDepth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings related to bot behavior for subreddits it is managing
|
||||
* */
|
||||
@@ -1540,6 +1575,8 @@ export interface BotInstanceJsonConfig {
|
||||
* @examples [300]
|
||||
* */
|
||||
heartbeatInterval?: number,
|
||||
|
||||
overrides?: SubredditOverrides[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1576,22 +1613,23 @@ export interface BotInstanceJsonConfig {
|
||||
* Useful when running many subreddits and rules are potentially cpu/memory/traffic heavy -- allows spreading out load
|
||||
* */
|
||||
stagger?: number,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings related to default configurations for queue behavior for subreddits
|
||||
* */
|
||||
queue?: {
|
||||
/**
|
||||
* Set the number of maximum concurrent workers any subreddit can use.
|
||||
*
|
||||
* Subreddits may define their own number of max workers in their config but the application will never allow any subreddit's max workers to be larger than the operator
|
||||
*
|
||||
* NOTE: Do not increase this unless you are certain you know what you are doing! The default is suitable for the majority of use cases.
|
||||
*
|
||||
* @default 1
|
||||
* @examples [1]
|
||||
* */
|
||||
maxWorkers?: number,
|
||||
/**
|
||||
* Set the number of maximum concurrent workers any subreddit can use.
|
||||
*
|
||||
* Subreddits may define their own number of max workers in their config but the application will never allow any subreddit's max workers to be larger than the operator
|
||||
*
|
||||
* NOTE: Do not increase this unless you are certain you know what you are doing! The default is suitable for the majority of use cases.
|
||||
*
|
||||
* @default 1
|
||||
* @examples [1]
|
||||
* */
|
||||
maxWorkers?: number,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1690,6 +1728,17 @@ export interface OperatorJsonConfig {
|
||||
|
||||
bots?: BotInstanceJsonConfig[]
|
||||
|
||||
/**
|
||||
* Added to the User-Agent information sent to reddit
|
||||
*
|
||||
* This string will be added BETWEEN version and your bot name.
|
||||
*
|
||||
* EX: `myBranch` => `web:contextMod:v1.0.0-myBranch:BOT-/u/MyBotUser`
|
||||
*
|
||||
* * ENV => `USER_AGENT`
|
||||
* */
|
||||
userAgent?: string
|
||||
|
||||
/**
|
||||
* Settings for the web interface
|
||||
* */
|
||||
@@ -1850,6 +1899,7 @@ export interface BotInstanceConfig extends BotInstanceJsonConfig {
|
||||
dryRun?: boolean,
|
||||
wikiConfig: string,
|
||||
heartbeatInterval: number,
|
||||
overrides?: SubredditOverrides[]
|
||||
},
|
||||
polling: {
|
||||
shared: PollOn[],
|
||||
@@ -1865,6 +1915,7 @@ export interface BotInstanceConfig extends BotInstanceJsonConfig {
|
||||
softLimit: number,
|
||||
hardLimit: number,
|
||||
}
|
||||
userAgent?: string
|
||||
}
|
||||
|
||||
export interface OperatorConfig extends OperatorJsonConfig {
|
||||
@@ -1935,6 +1986,7 @@ export interface LogInfo {
|
||||
instance?: string
|
||||
labels?: string[]
|
||||
bot?: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
export interface ActionResult extends ActionProcessResult {
|
||||
@@ -1942,6 +1994,8 @@ export interface ActionResult extends ActionProcessResult {
|
||||
name: string,
|
||||
run: boolean,
|
||||
runReason?: string,
|
||||
itemIs?: FilterResult<TypedActivityState>
|
||||
authorIs?: FilterResult<AuthorCriteria>
|
||||
}
|
||||
|
||||
export interface ActionProcessResult {
|
||||
@@ -1951,18 +2005,49 @@ export interface ActionProcessResult {
|
||||
touchedEntities?: (Submission | Comment | RedditUser | string)[]
|
||||
}
|
||||
|
||||
export interface ActionedEvent {
|
||||
activity: {
|
||||
peek: string
|
||||
link: string
|
||||
}
|
||||
export interface EventActivity {
|
||||
peek: string
|
||||
link: string
|
||||
type: ActivityType
|
||||
id: string
|
||||
subreddit: string
|
||||
author: string
|
||||
}
|
||||
|
||||
export interface ActionedEvent {
|
||||
activity: EventActivity
|
||||
parentSubmission?: EventActivity
|
||||
timestamp: number
|
||||
check: string
|
||||
ruleSummary: string,
|
||||
subreddit: string,
|
||||
triggered: boolean,
|
||||
runResults: RunResult[]
|
||||
}
|
||||
|
||||
export interface CheckResult {
|
||||
triggered: boolean
|
||||
ruleResults: RuleResult[]
|
||||
itemIs?: FilterResult<TypedActivityState>
|
||||
authorIs?: FilterResult<AuthorCriteria>
|
||||
fromCache?: boolean
|
||||
}
|
||||
|
||||
export interface CheckSummary extends CheckResult {
|
||||
name: string
|
||||
run: string
|
||||
postBehavior: string
|
||||
error?: string
|
||||
actionResults: ActionResult[]
|
||||
condition: 'AND' | 'OR'
|
||||
}
|
||||
|
||||
export interface RunResult {
|
||||
name: string
|
||||
triggered: boolean
|
||||
reason?: string
|
||||
error?: string
|
||||
itemIs?: FilterResult<TypedActivityState>
|
||||
authorIs?: FilterResult<AuthorCriteria>
|
||||
checkResults: CheckSummary[]
|
||||
}
|
||||
|
||||
export interface UserResultCache {
|
||||
@@ -2048,13 +2133,13 @@ export interface ManagerStats {
|
||||
export interface HistoricalStatUpdateData {
|
||||
eventsCheckedTotal?: number
|
||||
eventsActionedTotal?: number
|
||||
checksRun: string[] | string
|
||||
checksTriggered: string[] | string
|
||||
checksFromCache: string[] | string
|
||||
actionsRun: string[] | string
|
||||
rulesRun: string[] | string
|
||||
rulesCachedTotal: number
|
||||
rulesTriggered: string[] | string
|
||||
checksRun?: string[] | string
|
||||
checksTriggered?: string[] | string
|
||||
checksFromCache?: string[] | string
|
||||
actionsRun?: string[] | string
|
||||
rulesRun?: string[] | string
|
||||
rulesCachedTotal?: number
|
||||
rulesTriggered?: string[] | string
|
||||
}
|
||||
|
||||
export type SearchFacetType = 'title' | 'url' | 'duplicates' | 'crossposts' | 'external';
|
||||
@@ -2083,8 +2168,7 @@ export interface StringComparisonOptions {
|
||||
|
||||
export interface FilterCriteriaPropertyResult<T> {
|
||||
property: keyof T
|
||||
expected: (string | boolean | number)[]
|
||||
found?: string | boolean | number | null
|
||||
found?: string | boolean | number | null | FilterResult<any>
|
||||
passed?: null | boolean
|
||||
reason?: string
|
||||
behavior: FilterBehavior
|
||||
@@ -2157,3 +2241,39 @@ export interface TextMatchOptions {
|
||||
**/
|
||||
caseSensitive?: boolean
|
||||
}
|
||||
|
||||
export type ActivityCheckJson = SubmissionCheckJson | CommentCheckJson;
|
||||
|
||||
export type GotoPath = `goto:${string}`;
|
||||
/**
|
||||
* The possible behaviors that can occur after a check has run
|
||||
*
|
||||
* * next => continue to next Check/Run
|
||||
* * stop => stop CM lifecycle for this activity (immediately end)
|
||||
* * nextRun => skip any remaining Checks in this Run and start the next Run
|
||||
* * goto:[path] => specify a run[.check] to jump to
|
||||
*
|
||||
* */
|
||||
export type PostBehaviorTypes = 'next' | 'stop' | 'nextRun' | string;
|
||||
|
||||
export interface PostBehavior {
|
||||
/**
|
||||
* Do this behavior if a Check is triggered
|
||||
*
|
||||
* @default nextRun
|
||||
* @example ["nextRun"]
|
||||
* */
|
||||
postTrigger?: PostBehaviorTypes
|
||||
/**
|
||||
* Do this behavior if a Check is NOT triggered
|
||||
*
|
||||
* @default next
|
||||
* @example ["next"]
|
||||
* */
|
||||
postFail?: PostBehaviorTypes
|
||||
}
|
||||
|
||||
export type ActivityType = 'submission' | 'comment';
|
||||
|
||||
export type ItemCritPropHelper = SafeDictionary<FilterCriteriaPropertyResult<(CommentState & SubmissionState)>, keyof (CommentState & SubmissionState)>;
|
||||
export type RequiredItemCrit = Required<(CommentState & SubmissionState)>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Logger} from "winston";
|
||||
import {
|
||||
buildCacheOptionsFromProvider, buildCachePrefix,
|
||||
createAjvFactory, fileOrDirectoryIsWriteable,
|
||||
mergeArr,
|
||||
mergeArr, mergeFilters,
|
||||
normalizeName,
|
||||
overwriteMerge,
|
||||
parseBool, parseFromJsonOrYamlToObject, randomId,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
RedditCredentials,
|
||||
BotCredentialsJsonConfig,
|
||||
BotCredentialsConfig,
|
||||
FilterCriteriaDefaults, TypedActivityStates, OperatorFileConfig
|
||||
FilterCriteriaDefaults, TypedActivityStates, OperatorFileConfig, PostBehavior
|
||||
} from "./Common/interfaces";
|
||||
import {isRuleSetJSON, RuleSetJson, RuleSetObjectJson} from "./Rule/RuleSet";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
@@ -59,6 +59,7 @@ import {ConfigDocumentInterface} from "./Common/Config/AbstractConfigDocument";
|
||||
import {Document as YamlDocument} from "yaml";
|
||||
import {SimpleError} from "./Utils/Errors";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {RunStructuredJson} from "./Run";
|
||||
|
||||
export interface ConfigBuilderOptions {
|
||||
logger: Logger,
|
||||
@@ -130,55 +131,66 @@ export class ConfigBuilder {
|
||||
return validConfig as JSONConfig;
|
||||
}
|
||||
|
||||
parseToStructured(config: JSONConfig, filterCriteriaDefaultsFromBot?: FilterCriteriaDefaults): CheckStructuredJson[] {
|
||||
parseToStructured(config: JSONConfig, filterCriteriaDefaultsFromBot?: FilterCriteriaDefaults, postCheckBehaviorDefaultsFromBot: PostBehavior = {}): RunStructuredJson[] {
|
||||
let namedRules: Map<string, RuleObjectJson> = new Map();
|
||||
let namedActions: Map<string, ActionObjectJson> = new Map();
|
||||
const {checks = [], filterCriteriaDefaults} = config;
|
||||
for (const c of checks) {
|
||||
const {rules = []} = c;
|
||||
namedRules = extractNamedRules(rules, namedRules);
|
||||
namedActions = extractNamedActions(c.actions, namedActions);
|
||||
const {checks = [], runs = [], filterCriteriaDefaults, postCheckBehaviorDefaults} = config;
|
||||
|
||||
if(checks.length > 0 && runs.length > 0) {
|
||||
// cannot have both checks and runs at top-level
|
||||
throw new Error(`Subreddit configuration cannot contain both 'checks' and 'runs' at top-level.`);
|
||||
}
|
||||
|
||||
const filterDefs = filterCriteriaDefaults ?? filterCriteriaDefaultsFromBot;
|
||||
const {
|
||||
authorIsBehavior = 'merge',
|
||||
itemIsBehavior = 'merge',
|
||||
authorIs: authorIsDefault = {},
|
||||
itemIs: itemIsDefault = []
|
||||
} = filterDefs || {};
|
||||
|
||||
const structuredChecks: CheckStructuredJson[] = [];
|
||||
for (const c of checks) {
|
||||
const {rules = [], authorIs = {}, itemIs = []} = c;
|
||||
const strongRules = insertNamedRules(rules, namedRules);
|
||||
const strongActions = insertNamedActions(c.actions, namedActions);
|
||||
|
||||
let derivedAuthorIs: AuthorOptions = authorIsDefault;
|
||||
if (authorIsBehavior === 'merge') {
|
||||
derivedAuthorIs = merge.all([authorIs, authorIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
|
||||
} else if (Object.keys(authorIs).length > 0) {
|
||||
derivedAuthorIs = authorIs;
|
||||
}
|
||||
|
||||
let derivedItemIs: TypedActivityStates = itemIsDefault;
|
||||
if (itemIsBehavior === 'merge') {
|
||||
derivedItemIs = [...itemIs, ...itemIsDefault];
|
||||
} else if (itemIs.length > 0) {
|
||||
derivedItemIs = itemIs;
|
||||
}
|
||||
|
||||
const strongCheck = {
|
||||
...c,
|
||||
authorIs: derivedAuthorIs,
|
||||
itemIs: derivedItemIs,
|
||||
rules: strongRules,
|
||||
actions: strongActions
|
||||
} as CheckStructuredJson;
|
||||
structuredChecks.push(strongCheck);
|
||||
const realRuns = runs;
|
||||
if(checks.length > 0) {
|
||||
realRuns.push({name: 'Run1', checks: checks});
|
||||
}
|
||||
|
||||
return structuredChecks;
|
||||
for(const r of realRuns) {
|
||||
for (const c of r.checks) {
|
||||
const {rules = []} = c;
|
||||
namedRules = extractNamedRules(rules, namedRules);
|
||||
namedActions = extractNamedActions(c.actions, namedActions);
|
||||
}
|
||||
}
|
||||
|
||||
const structuredRuns: RunStructuredJson[] = [];
|
||||
|
||||
for(const r of realRuns) {
|
||||
|
||||
const {filterCriteriaDefaults: filterCriteriaDefaultsFromRun, postFail, postTrigger, authorIs, itemIs } = r;
|
||||
|
||||
const [derivedRunAuthorIs, derivedRunItemIs] = mergeFilters(r, filterCriteriaDefaults ?? filterCriteriaDefaultsFromBot);
|
||||
|
||||
const structuredChecks: CheckStructuredJson[] = [];
|
||||
for (const c of r.checks) {
|
||||
const {rules = [], authorIs = {}, itemIs = []} = c;
|
||||
const strongRules = insertNamedRules(rules, namedRules);
|
||||
const strongActions = insertNamedActions(c.actions, namedActions);
|
||||
|
||||
const [derivedAuthorIs, derivedItemIs] = mergeFilters(c, filterCriteriaDefaultsFromRun ?? (filterCriteriaDefaults ?? filterCriteriaDefaultsFromBot));
|
||||
|
||||
const postCheckBehaviors = Object.assign({}, postCheckBehaviorDefaultsFromBot, removeUndefinedKeys({postFail, postTrigger}));
|
||||
|
||||
const strongCheck = {
|
||||
...c,
|
||||
authorIs: derivedAuthorIs,
|
||||
itemIs: derivedItemIs,
|
||||
rules: strongRules,
|
||||
actions: strongActions,
|
||||
...postCheckBehaviors
|
||||
} as CheckStructuredJson;
|
||||
structuredChecks.push(strongCheck);
|
||||
}
|
||||
structuredRuns.push({
|
||||
...r,
|
||||
checks: structuredChecks,
|
||||
authorIs: derivedRunAuthorIs,
|
||||
itemIs: derivedRunItemIs
|
||||
});
|
||||
}
|
||||
|
||||
return structuredRuns;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +710,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
|
||||
stream = {},
|
||||
} = {},
|
||||
caching: opCache,
|
||||
userAgent,
|
||||
web: {
|
||||
port = 8085,
|
||||
maxLogs = 200,
|
||||
@@ -804,6 +817,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
|
||||
}
|
||||
},
|
||||
caching: cache,
|
||||
userAgent,
|
||||
web: {
|
||||
port,
|
||||
caching: {
|
||||
@@ -843,11 +857,13 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
|
||||
actionedEventsMax: opActionedEventsMax,
|
||||
actionedEventsDefault: opActionedEventsDefault = 25,
|
||||
provider: defaultProvider,
|
||||
} = {}
|
||||
} = {},
|
||||
userAgent,
|
||||
} = opConfig;
|
||||
const {
|
||||
name: botName,
|
||||
filterCriteriaDefaults = filterCriteriaDefault,
|
||||
postCheckBehaviorDefaults,
|
||||
polling: {
|
||||
sharedMod,
|
||||
shared = [],
|
||||
@@ -864,8 +880,10 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
|
||||
hardLimit = 50
|
||||
} = {},
|
||||
snoowrap = snoowrapOp,
|
||||
flowControlDefaults,
|
||||
credentials = {},
|
||||
subreddits: {
|
||||
overrides = [],
|
||||
names = [],
|
||||
exclude = [],
|
||||
wikiConfig = 'botconfig/contextbot',
|
||||
@@ -974,16 +992,20 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
|
||||
return {
|
||||
name: botName,
|
||||
snoowrap: snoowrap || {},
|
||||
flowControlDefaults,
|
||||
filterCriteriaDefaults,
|
||||
postCheckBehaviorDefaults,
|
||||
subreddits: {
|
||||
names,
|
||||
exclude,
|
||||
wikiConfig,
|
||||
heartbeatInterval,
|
||||
dryRun,
|
||||
overrides,
|
||||
},
|
||||
credentials: botCreds,
|
||||
caching: botCache,
|
||||
userAgent,
|
||||
polling: {
|
||||
shared: [...new Set(realShared)] as PollOn[],
|
||||
stagger,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {CheckJson, CommentCheckJson, SubmissionCheckJson} from "./Check";
|
||||
import {ManagerOptions} from "./Common/interfaces";
|
||||
import {ActivityCheckJson, ManagerOptions} from "./Common/interfaces";
|
||||
import {RunJson} from "./Run";
|
||||
|
||||
export interface JSONConfig extends ManagerOptions {
|
||||
/**
|
||||
@@ -12,5 +13,11 @@ export interface JSONConfig extends ManagerOptions {
|
||||
* When a check "passes", and actions are performed, then all subsequent checks are skipped.
|
||||
* @minItems 1
|
||||
* */
|
||||
checks: Array<SubmissionCheckJson|CommentCheckJson>
|
||||
checks?: ActivityCheckJson[]
|
||||
|
||||
/**
|
||||
* A list of sets of Checks to run
|
||||
* @minItems 1
|
||||
* */
|
||||
runs?: RunJson[]
|
||||
}
|
||||
|
||||
@@ -222,9 +222,11 @@ export class AttributionRule extends Rule {
|
||||
|
||||
activities = await as.filter(activities, async (activity) => {
|
||||
if (asSubmission(activity) && submissionState !== undefined) {
|
||||
return await this.resources.testItemCriteria(activity, [submissionState]);
|
||||
const {passed} = await this.resources.testItemCriteria(activity, submissionState, this.logger);
|
||||
return passed;
|
||||
} else if (commentState !== undefined) {
|
||||
return await this.resources.testItemCriteria(activity, [commentState]);
|
||||
const {passed} = await this.resources.testItemCriteria(activity, commentState, this.logger);
|
||||
return passed;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
isSubmission,
|
||||
isValidImageURL,
|
||||
objectToStringSummary,
|
||||
parseGenericValueOrPercentComparison,
|
||||
parseGenericValueOrPercentComparison, parseRedditEntity,
|
||||
parseStringToRegex,
|
||||
parseSubredditName,
|
||||
parseUsableLinkIdentifier,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import {
|
||||
ActivityWindow,
|
||||
ActivityWindowCriteria,
|
||||
ActivityWindowType, CommentState,
|
||||
ActivityWindowType, CommentState, CompareValueOrPercent,
|
||||
//ImageData,
|
||||
ImageDetection,
|
||||
ReferenceSubmission, StrongImageDetection, StrongSubredditState, SubmissionState,
|
||||
@@ -303,6 +303,11 @@ export class RecentActivityRule extends Rule {
|
||||
}
|
||||
}
|
||||
|
||||
const allDistinctSubreddits = [...viableActivity.reduce((acc, curr) => {
|
||||
acc.add(curr.subreddit_name_prefixed);
|
||||
return acc;
|
||||
}, new Set())].map(x => parseRedditEntity(x as string));
|
||||
|
||||
const summaries = [];
|
||||
let totalTriggeredOn;
|
||||
for (const triggerSet of this.thresholds) {
|
||||
@@ -315,6 +320,7 @@ export class RecentActivityRule extends Rule {
|
||||
karma: karmaThreshold,
|
||||
commentState,
|
||||
submissionState,
|
||||
subredditThreshold,
|
||||
} = triggerSet;
|
||||
|
||||
// convert subreddits array into entirely StrongSubredditState
|
||||
@@ -326,9 +332,11 @@ export class RecentActivityRule extends Rule {
|
||||
|
||||
let validActivity: (Comment | Submission)[] = await as.filter(viableActivity, async (activity) => {
|
||||
if (asSubmission(activity) && submissionState !== undefined) {
|
||||
return await this.resources.testItemCriteria(activity, [submissionState]);
|
||||
const {passed} = await this.resources.testItemCriteria(activity, submissionState, this.logger);
|
||||
return passed;
|
||||
} else if (commentState !== undefined) {
|
||||
return await this.resources.testItemCriteria(activity, [commentState]);
|
||||
const {passed} = await this.resources.testItemCriteria(activity, commentState, this.logger);
|
||||
return passed;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -345,36 +353,44 @@ export class RecentActivityRule extends Rule {
|
||||
}
|
||||
|
||||
const {operator, value, isPercent} = parseGenericValueOrPercentComparison(threshold);
|
||||
let sum = {
|
||||
let sum: any = {
|
||||
subsWithActivity: presentSubs,
|
||||
combinedKarma,
|
||||
karmaThreshold,
|
||||
subreddits: subStates.map(x => x.stateDescription),
|
||||
subredditCriteria: subStates.map(x => x.stateDescription),
|
||||
subreddits: allDistinctSubreddits.map(x => x.name),
|
||||
count: currCount,
|
||||
threshold,
|
||||
subredditThreshold,
|
||||
triggered: false,
|
||||
testValue: currCount.toString()
|
||||
};
|
||||
if (isPercent) {
|
||||
sum.testValue = `${formatNumber((currCount / viableActivity.length) * 100)}%`;
|
||||
if (comparisonTextOp(currCount / viableActivity.length, operator, value / 100)) {
|
||||
sum.triggered = true;
|
||||
totalTriggeredOn = sum;
|
||||
}
|
||||
} else if (comparisonTextOp(currCount, operator, value)) {
|
||||
sum.triggered = true;
|
||||
totalTriggeredOn = sum;
|
||||
sum.thresholdTriggered = comparisonTextOp(currCount / viableActivity.length, operator, value / 100);
|
||||
} else {
|
||||
sum.thresholdTriggered = comparisonTextOp(currCount, operator, value);
|
||||
}
|
||||
// if we would trigger on threshold need to also test for karma
|
||||
if (totalTriggeredOn !== undefined && karmaThreshold !== undefined) {
|
||||
if (sum.thresholdTriggered && karmaThreshold !== undefined) {
|
||||
const {operator: opKarma, value: valueKarma} = parseGenericValueOrPercentComparison(karmaThreshold);
|
||||
if (!comparisonTextOp(combinedKarma, opKarma, valueKarma)) {
|
||||
sum.triggered = false;
|
||||
totalTriggeredOn = undefined;
|
||||
sum.karmaThresholdTriggered = comparisonTextOp(combinedKarma, opKarma, valueKarma);
|
||||
}
|
||||
if(sum.thresholdTriggered && subredditThreshold !== undefined) {
|
||||
const {operator, value, isPercent} = parseGenericValueOrPercentComparison(subredditThreshold);
|
||||
if (isPercent) {
|
||||
sum.subredditThresholdTriggered = comparisonTextOp(sum.subsWithActivity / sum.subreddits, operator, value / 100);
|
||||
} else {
|
||||
sum.subredditThresholdTriggered = comparisonTextOp(sum.subsWithActivity, operator, value);
|
||||
}
|
||||
}
|
||||
|
||||
summaries.push(sum);
|
||||
const { thresholdTriggered, karmaThresholdTriggered, subredditThresholdTriggered } = sum;
|
||||
sum.triggered = thresholdTriggered && ((karmaThresholdTriggered === undefined || karmaThresholdTriggered) && (subredditThresholdTriggered === undefined || subredditThresholdTriggered));
|
||||
if(sum.triggered) {
|
||||
totalTriggeredOn = sum;
|
||||
}
|
||||
// if either trigger condition is hit end the iteration early
|
||||
if (totalTriggeredOn !== undefined) {
|
||||
break;
|
||||
@@ -410,12 +426,45 @@ export class RecentActivityRule extends Rule {
|
||||
triggered,
|
||||
combinedKarma,
|
||||
karmaThreshold,
|
||||
subredditThreshold,
|
||||
} = summary;
|
||||
const relevantSubs = subsWithActivity.length === 0 ? subreddits : subsWithActivity;
|
||||
let totalSummary = `${testValue} activities over ${relevantSubs.length} subreddits${karmaThreshold !== undefined ? ` with ${combinedKarma} combined karma` : ''} ${triggered ? 'met' : 'did not meet'} threshold of ${threshold}${karmaThreshold !== undefined ? ` and ${karmaThreshold} combined karma` : ''}`;
|
||||
if (triggered && subsWithActivity.length > 0) {
|
||||
totalSummary = `${totalSummary} -- subreddits: ${subsWithActivity.join(', ')}`;
|
||||
let totalSummaryParts: string[] = [`${testValue} activities found in ${subsWithActivity.length} of the specified subreddits (out of ${subreddits.length} total)`];
|
||||
let statSummary = '';
|
||||
let thresholdSummary = '';
|
||||
if(karmaThreshold !== undefined || subredditThreshold !== undefined) {
|
||||
let statParts = [];
|
||||
let thresholdParts = [];
|
||||
if(karmaThreshold !== undefined) {
|
||||
statParts.push(`${combinedKarma} combined karma`);
|
||||
thresholdParts.push(`${karmaThreshold} combined karma`);
|
||||
}
|
||||
if(subredditThreshold !== undefined) {
|
||||
statParts.push(`${subsWithActivity.length} distinct subreddits`);
|
||||
thresholdParts.push(`${subredditThreshold} distinct subreddits`);
|
||||
}
|
||||
statSummary = statParts.join(' and ');
|
||||
thresholdSummary = thresholdParts.join(' and ');
|
||||
}
|
||||
|
||||
if(statSummary !== '') {
|
||||
totalSummaryParts.push(` with ${statSummary}`)
|
||||
}
|
||||
|
||||
totalSummaryParts.push(` ${triggered ? 'MET' : 'DID NOT MEET'} threshold of ${threshold} activities`);
|
||||
|
||||
if(thresholdSummary !== '') {
|
||||
totalSummaryParts.push(` and ${thresholdSummary}`);
|
||||
}
|
||||
|
||||
if (triggered && subsWithActivity.length > 0) {
|
||||
totalSummaryParts.push(` -- subreddits: ${subsWithActivity.join(', ')}`);
|
||||
}
|
||||
|
||||
// EX
|
||||
// 2 activities over 1 subreddits with 4 combined karma and 1 distinct subreddits did not meet threshold of > 1 activities and > 2 distinct subreddits -- subreddits: mySubreddit
|
||||
const totalSummary = totalSummaryParts.join('');
|
||||
|
||||
return {
|
||||
result: totalSummary,
|
||||
data: {
|
||||
@@ -493,6 +542,22 @@ export interface ActivityThreshold {
|
||||
* @examples [["mealtimevideos","askscience", "/onlyfans*\/i", {"over18": true}]]
|
||||
* */
|
||||
subreddits?: (string | SubredditState)[]
|
||||
|
||||
/**
|
||||
* A string containing a comparison operator and a value to compare the **number of subreddits that have valid activities** against
|
||||
*
|
||||
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
|
||||
*
|
||||
* * EX `> 3` => greater than 3 Subreddits found with valid activities
|
||||
* * EX `<= 75%` => number of Subreddits with valid activities are equal to or less than 75% of all Subreddits found
|
||||
*
|
||||
* **Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then "all Subreddits found" is only pertains to Subreddits that had the Link of the Submission, rather than all Subreddits from this window.
|
||||
*
|
||||
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$
|
||||
* @default ">= 1"
|
||||
* @examples [">= 1"]
|
||||
* */
|
||||
subredditThreshold?: string
|
||||
}
|
||||
|
||||
interface RecentActivityConfig extends ActivityWindow, ReferenceSubmission {
|
||||
|
||||
@@ -2,9 +2,9 @@ import Snoowrap, {Comment} from "snoowrap";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import {Logger} from "winston";
|
||||
import {findResultByPremise, mergeArr} from "../util";
|
||||
import {checkAuthorFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {ChecksActivityState, TypedActivityStates} from "../Common/interfaces";
|
||||
import Author, {AuthorOptions} from "../Author/Author";
|
||||
import {checkAuthorFilter, checkItemFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {ChecksActivityState, FilterResult, TypedActivityState, TypedActivityStates} from "../Common/interfaces";
|
||||
import Author, {AuthorCriteria, AuthorOptions} from "../Author/Author";
|
||||
|
||||
export interface RuleOptions {
|
||||
name?: string;
|
||||
@@ -31,6 +31,9 @@ export interface RuleResult extends ResultContext {
|
||||
kind: string
|
||||
name: string
|
||||
triggered: (boolean | null)
|
||||
fromCache?: boolean
|
||||
itemIs?: FilterResult<TypedActivityState>
|
||||
authorIs?: FilterResult<AuthorCriteria>
|
||||
}
|
||||
|
||||
export type FormattedRuleResult = RuleResult & {
|
||||
@@ -94,9 +97,9 @@ export abstract class Rule implements IRule, Triggerable {
|
||||
const existingResult = findResultByPremise(this.getPremise(), existingResults);
|
||||
if (existingResult) {
|
||||
this.logger.debug(`Returning existing result of ${existingResult.triggered ? '✔️' : '❌'}`);
|
||||
return Promise.resolve([existingResult.triggered, {...existingResult, name: this.name}]);
|
||||
return Promise.resolve([existingResult.triggered, {...existingResult, name: this.name, fromCache: true}]);
|
||||
}
|
||||
const itemPass = await this.resources.testItemCriteria(item, this.itemIs);
|
||||
const [itemPass, itemFilterType, itemFilterResults] = await checkItemFilter(item, this.itemIs, this.resources, this.logger);
|
||||
if (!itemPass) {
|
||||
this.logger.verbose(`(Skipped) Item did not pass 'itemIs' test`);
|
||||
return Promise.resolve([null, this.getResult(null, {result: `Item did not pass 'itemIs' test`})]);
|
||||
|
||||
317
src/Run/index.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import {Check, CheckStructuredJson} from "../Check";
|
||||
import {
|
||||
ActionResult,
|
||||
ActivityCheckJson, CheckResult, CheckSummary,
|
||||
FilterCriteriaDefaults, FilterResult,
|
||||
PostBehavior,
|
||||
PostBehaviorTypes, RunResult,
|
||||
TypedActivityStates
|
||||
} from "../Common/interfaces";
|
||||
import {SubmissionCheck} from "../Check/SubmissionCheck";
|
||||
import {CommentCheck} from "../Check/CommentCheck";
|
||||
import {Logger} from "winston";
|
||||
import {determineNewResults, FAIL, isSubmission, mergeArr, normalizeName} from "../util";
|
||||
import {checkAuthorFilter, checkItemFilter, SubredditResources} from "../Subreddit/SubredditResources";
|
||||
import {ExtendedSnoowrap} from "../Utils/SnoowrapClients";
|
||||
import {Author, AuthorCriteria, AuthorOptions} from "../Author/Author";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import {Comment} from "snoowrap";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {RuleResult} from "../Rule";
|
||||
import {ErrorWithCause, stackWithCauses} from "pony-cause";
|
||||
import EventEmitter from "events";
|
||||
import {CheckProcessingError, RunProcessingError} from "../Utils/Errors";
|
||||
|
||||
export class Run {
|
||||
name: string;
|
||||
submissionChecks: SubmissionCheck[] = [];
|
||||
commentChecks: CommentCheck[] = [];
|
||||
postFail?: PostBehaviorTypes;
|
||||
postTrigger?: PostBehaviorTypes;
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
logger: Logger;
|
||||
client: ExtendedSnoowrap;
|
||||
subredditName: string;
|
||||
resources: SubredditResources;
|
||||
dryRun?: boolean;
|
||||
itemIs: TypedActivityStates;
|
||||
authorIs: AuthorOptions;
|
||||
enabled: boolean;
|
||||
emitter: EventEmitter;
|
||||
|
||||
|
||||
constructor(options: RunOptions) {
|
||||
const {
|
||||
name,
|
||||
checks = [],
|
||||
emitter,
|
||||
postFail,
|
||||
postTrigger,
|
||||
filterCriteriaDefaults,
|
||||
logger,
|
||||
resources,
|
||||
client,
|
||||
subredditName,
|
||||
dryRun,
|
||||
authorIs: {
|
||||
include = [],
|
||||
excludeCondition,
|
||||
exclude = [],
|
||||
} = {},
|
||||
itemIs = [],
|
||||
enable = true,
|
||||
|
||||
} = options;
|
||||
this.name = name;
|
||||
this.logger = logger.child({labels: [`RUN ${name}`]}, mergeArr);
|
||||
this.resources = resources;
|
||||
this.client = client;
|
||||
this.subredditName = subredditName;
|
||||
this.postFail = postFail;
|
||||
this.postTrigger = postTrigger;
|
||||
this.filterCriteriaDefaults = filterCriteriaDefaults;
|
||||
this.dryRun = dryRun;
|
||||
this.enabled = enable;
|
||||
this.itemIs = itemIs;
|
||||
this.authorIs = {
|
||||
excludeCondition,
|
||||
exclude: exclude.map(x => new Author(x)),
|
||||
include: include.map(x => new Author(x)),
|
||||
}
|
||||
this.emitter = emitter;
|
||||
|
||||
for(const c of checks) {
|
||||
const checkConfig = {
|
||||
...c,
|
||||
emitter,
|
||||
dryRun: this.dryRun || c.dryRun,
|
||||
logger: this.logger,
|
||||
subredditName: this.subredditName,
|
||||
resources: this.resources,
|
||||
client: this.client,
|
||||
};
|
||||
if (c.kind === 'comment') {
|
||||
this.commentChecks.push(new CommentCheck(checkConfig));
|
||||
} else if (c.kind === 'submission') {
|
||||
this.submissionChecks.push(new SubmissionCheck(checkConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handle(activity: (Submission | Comment), initAllRuleResults: RuleResult[], existingRunResults: RunResult[] = [], options?: runCheckOptions): Promise<[RunResult, string]> {
|
||||
|
||||
let allRuleResults = initAllRuleResults;
|
||||
let continueRunIteration = true;
|
||||
let postBehavior = 'next';
|
||||
const runResult: RunResult = {
|
||||
name: this.name,
|
||||
triggered: false,
|
||||
checkResults: [],
|
||||
}
|
||||
const {
|
||||
maxGotoDepth = 1,
|
||||
gotoContext: optGotoContext = '',
|
||||
} = options || {};
|
||||
|
||||
if(!this.enabled) {
|
||||
runResult.error = 'Not enabled';
|
||||
return [runResult, postBehavior];
|
||||
}
|
||||
|
||||
if (isSubmission(activity)) {
|
||||
if (this.submissionChecks.length === 0) {
|
||||
const msg = 'Skipping b/c Run did not contain any submission Checks';
|
||||
this.logger.debug(msg);
|
||||
return [{...runResult, reason: msg}, postBehavior];
|
||||
}
|
||||
} else if (this.commentChecks.length === 0) {
|
||||
const msg = 'Skipping b/c Run did not contain any comment Checks';
|
||||
this.logger.debug(msg);
|
||||
return [{...runResult, reason: msg}, postBehavior];
|
||||
}
|
||||
|
||||
let gotoContext = optGotoContext;
|
||||
const checks = isSubmission(activity) ? this.submissionChecks : this.commentChecks;
|
||||
let continueCheckIteration = true;
|
||||
let checkIndex = 0;
|
||||
|
||||
// for now disallow the same goto from being run twice
|
||||
// maybe in the future this can be user-configurable
|
||||
const hitGotos: string[] = [];
|
||||
|
||||
try {
|
||||
|
||||
const [itemPass, itemFilterType, itemFilterResults] = await checkItemFilter(activity, this.itemIs, this.resources, this.logger)
|
||||
if (!itemPass) {
|
||||
this.logger.verbose(`${FAIL} => Item did not pass 'itemIs' test`);
|
||||
return [{
|
||||
...runResult,
|
||||
triggered: false,
|
||||
itemIs: itemFilterResults
|
||||
}, postBehavior];
|
||||
} else if (this.itemIs.length > 0) {
|
||||
runResult.itemIs = itemFilterResults;
|
||||
}
|
||||
|
||||
const [authFilterPass, authFilterType, authorFilterResult] = await checkAuthorFilter(activity, this.authorIs, this.resources, this.logger);
|
||||
if (!authFilterPass) {
|
||||
return [{
|
||||
...runResult,
|
||||
authorIs: authorFilterResult
|
||||
}, postBehavior];
|
||||
} else if (authFilterType !== undefined) {
|
||||
runResult.authorIs = authorFilterResult;
|
||||
}
|
||||
|
||||
while (continueCheckIteration && (checkIndex < checks.length || gotoContext !== '')) {
|
||||
let check: Check;
|
||||
if (gotoContext !== '') {
|
||||
const [runName, checkName] = gotoContext.split('.');
|
||||
hitGotos.push(checkName);
|
||||
if(hitGotos.filter(x => x === gotoContext).length > maxGotoDepth) {
|
||||
throw new Error(`The check specified in goto "${gotoContext}" has been triggered ${hitGotos.filter(x => x === gotoContext).length} times which is more than the max allowed for any single goto (${maxGotoDepth}).
|
||||
This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself! The max triggered depth can be configured by the operator.`);
|
||||
}
|
||||
const gotoIndex = checks.findIndex(x => normalizeName(x.name) === normalizeName(checkName));
|
||||
if (gotoIndex !== -1) {
|
||||
if (gotoIndex > checkIndex) {
|
||||
this.logger.debug(`Fast forwarding Check iteration to ${checks[gotoIndex].name}`, {leaf: 'GOTO'});
|
||||
} else if (gotoIndex < checkIndex) {
|
||||
this.logger.debug(`Rewinding Check iteration to ${checks[gotoIndex].name}`, {leaf: 'GOTO'});
|
||||
} else if(checkIndex !== 0) {
|
||||
this.logger.debug(`Did not iterate to next Check due to GOTO specifying same Check (you probably don't want to do this!)`, {leaf: 'GOTO'});
|
||||
}
|
||||
check = checks[gotoIndex];
|
||||
checkIndex = gotoIndex;
|
||||
gotoContext = '';
|
||||
} else {
|
||||
throw new Error(`GOTO specified a Check that could not be found in ${isSubmission(activity) ? 'Submission' : 'Comment'} checks: ${checkName}`);
|
||||
}
|
||||
} else {
|
||||
check = checks[checkIndex];
|
||||
}
|
||||
|
||||
if(existingRunResults.some(x => x.checkResults?.map(y => y.name).includes(check.name))) {
|
||||
throw new Error(`The check "${check.name}" has already been run once. This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself!`);
|
||||
}
|
||||
|
||||
const checkSummary = await check.handle(activity, allRuleResults, options);
|
||||
postBehavior = checkSummary.postBehavior;
|
||||
|
||||
allRuleResults = allRuleResults.concat(determineNewResults(allRuleResults, checkSummary.ruleResults));
|
||||
|
||||
runResult.checkResults.push(checkSummary);
|
||||
|
||||
switch (checkSummary.postBehavior.toLowerCase()) {
|
||||
case 'next':
|
||||
checkIndex++;
|
||||
gotoContext = '';
|
||||
break;
|
||||
case 'nextrun':
|
||||
continueCheckIteration = false;
|
||||
gotoContext = '';
|
||||
break;
|
||||
case 'stop':
|
||||
continueCheckIteration = false;
|
||||
continueRunIteration = false;
|
||||
gotoContext = '';
|
||||
break;
|
||||
default:
|
||||
if (checkSummary.postBehavior.includes('goto:')) {
|
||||
gotoContext = checkSummary.postBehavior.split(':')[1];
|
||||
if (!gotoContext.includes('.')) {
|
||||
// no period means we are going directly to a run
|
||||
continueCheckIteration = false;
|
||||
} else {
|
||||
const [runN, checkN] = gotoContext.split('.');
|
||||
if (runN !== '') {
|
||||
// if run name is specified then also break check iteration
|
||||
// OTHERWISE this is a special "in run" check path IE .check1 where we just want to continue iterating checks
|
||||
continueCheckIteration = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runResult.triggered = runResult.checkResults.some(x => x.triggered);
|
||||
return [runResult, postBehavior]
|
||||
} catch (err: any) {
|
||||
if(err instanceof CheckProcessingError && err.result !== undefined) {
|
||||
runResult.checkResults.push(err.result);
|
||||
}
|
||||
if(runResult.error === undefined) {
|
||||
runResult.error = `Run failed due to uncaught exception: ${err.message}`;
|
||||
}
|
||||
runResult.triggered = runResult.checkResults.some(x => x.triggered);
|
||||
|
||||
throw new RunProcessingError(`[RUN ${this.name}] An uncaught exception occurred while processing Run`, {cause: err}, runResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRun extends PostBehavior {
|
||||
/**
|
||||
* Friendly name for this Run EX "flairsRun"
|
||||
*
|
||||
* Can only contain letters, numbers, underscore, spaces, and dashes
|
||||
*
|
||||
* @pattern ^[a-zA-Z]([\w -]*[\w])?$
|
||||
* @examples ["myNewRun"]
|
||||
* */
|
||||
name?: string
|
||||
/**
|
||||
* Set the default filter criteria for all checks. If this property is specified it will override any defaults passed from the bot's config
|
||||
*
|
||||
* Default behavior is to exclude all mods and automoderator from checks
|
||||
* */
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
|
||||
/**
|
||||
* Use this option to override the `dryRun` setting for all Actions of all Checks in this Run
|
||||
*
|
||||
* @examples [false, true]
|
||||
* */
|
||||
dryRun?: boolean;
|
||||
|
||||
/**
|
||||
* A list of criteria to test the state of the `Activity` against before running the check.
|
||||
*
|
||||
* If any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.
|
||||
*
|
||||
* * @examples [[{"over_18": true, "removed': false}]]
|
||||
* */
|
||||
itemIs?: TypedActivityStates
|
||||
|
||||
/**
|
||||
* If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail.
|
||||
* */
|
||||
authorIs?: AuthorOptions
|
||||
|
||||
/**
|
||||
* Should this Run be executed by the bot?
|
||||
*
|
||||
* @default true
|
||||
* @examples [true]
|
||||
* */
|
||||
enable?: boolean,
|
||||
}
|
||||
|
||||
export interface RunOptions extends IRun {
|
||||
// submissionChecks?: SubmissionCheck[]
|
||||
// commentChecks?: CommentCheck[]
|
||||
checks: CheckStructuredJson[]
|
||||
name: string
|
||||
logger: Logger
|
||||
resources: SubredditResources
|
||||
client: ExtendedSnoowrap
|
||||
subredditName: string;
|
||||
emitter: EventEmitter;
|
||||
}
|
||||
|
||||
export interface RunJson extends IRun {
|
||||
checks: ActivityCheckJson[]
|
||||
}
|
||||
|
||||
export interface RunStructuredJson extends RunJson {
|
||||
checks: CheckStructuredJson[]
|
||||
}
|
||||
@@ -438,21 +438,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"subredditThreshold": {
|
||||
"default": ">= 1",
|
||||
"description": "A string containing a comparison operator and a value to compare the **number of subreddits that have valid activities** against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 3` => greater than 3 Subreddits found with valid activities\n* EX `<= 75%` => number of Subreddits with valid activities are equal to or less than 75% of all Subreddits found\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Subreddits found\" is only pertains to Subreddits that had the Link of the Submission, rather than all Subreddits from this window.",
|
||||
"examples": [
|
||||
">= 1"
|
||||
],
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
"type": "string"
|
||||
},
|
||||
"subreddits": {
|
||||
"description": "Activities will be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
|
||||
"examples": [
|
||||
@@ -192,21 +201,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -428,21 +434,18 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -684,21 +687,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -784,21 +784,18 @@
|
||||
"description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -1088,21 +1085,18 @@
|
||||
"description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -1274,6 +1268,16 @@
|
||||
"description": "If notifications are configured and this is `true` then an `eventActioned` event will be sent when this check is triggered.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"postFail": {
|
||||
"default": "next",
|
||||
"description": "Do this behavior if a Check is NOT triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"postTrigger": {
|
||||
"default": "nextRun",
|
||||
"description": "Do this behavior if a Check is triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"rules": {
|
||||
"description": "A list of Rules to run.\n\nIf `Rule` objects are triggered based on `condition` then `actions` will be performed.\n\nCan be `Rule`, `RuleSet`, or the `name` of any **named** `Rule` in your subreddit's configuration.\n\n**If `rules` is an empty array or not present then `actions` are performed immediately.**",
|
||||
"items": {
|
||||
@@ -1491,20 +1495,17 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemIsBehavior": {
|
||||
"description": "Determine how itemIs defaults behave when itemIs is present on the check\n\n* merge => adds defaults to check's itemIs\n* replace => check itemIs will replace defaults (no defaults used)",
|
||||
@@ -1565,21 +1566,18 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -1745,21 +1743,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1894,21 +1889,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -1993,21 +1985,18 @@
|
||||
"description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -2227,6 +2216,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PostBehavior": {
|
||||
"properties": {
|
||||
"postFail": {
|
||||
"default": "next",
|
||||
"description": "Do this behavior if a Check is NOT triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"postTrigger": {
|
||||
"default": "nextRun",
|
||||
"description": "Do this behavior if a Check is triggered",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RecentActivityRuleJSONConfig": {
|
||||
"description": "Checks a user's history for any Activity (Submission/Comment) in the subreddits specified in thresholds\n\nAvailable data for [Action templating](https://github.com/FoxxMD/context-mod#action-templating):\n\n```\nsummary => comma-deliminated list of subreddits that hit the threshold and their count EX subredditA(1), subredditB(4),...\nsubCount => Total number of subreddits that hit the threshold\ntotalCount => Total number of all activity occurrences in subreddits\n```",
|
||||
"properties": {
|
||||
@@ -2254,21 +2258,18 @@
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -2529,21 +2530,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -2610,21 +2608,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -2730,21 +2725,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"keepRemoved": {
|
||||
"default": false,
|
||||
@@ -2877,21 +2869,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -3092,21 +3081,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -3186,6 +3172,98 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RunJson": {
|
||||
"properties": {
|
||||
"authorIs": {
|
||||
"$ref": "#/definitions/AuthorOptions",
|
||||
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail.",
|
||||
"examples": [
|
||||
{
|
||||
"include": [
|
||||
{
|
||||
"flairText": [
|
||||
"Contributor",
|
||||
"Veteran"
|
||||
]
|
||||
},
|
||||
{
|
||||
"isMod": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"checks": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionCheckJson"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CommentCheckJson"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dryRun": {
|
||||
"description": "Use this option to override the `dryRun` setting for all Actions of all Checks in this Run",
|
||||
"examples": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable": {
|
||||
"default": true,
|
||||
"description": "Should this Run be executed by the bot?",
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"filterCriteriaDefaults": {
|
||||
"$ref": "#/definitions/FilterCriteriaDefaults",
|
||||
"description": "Set the default filter criteria for all checks. If this property is specified it will override any defaults passed from the bot's config\n\nDefault behavior is to exclude all mods and automoderator from checks"
|
||||
},
|
||||
"itemIs": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Friendly name for this Run EX \"flairsRun\"\n\nCan only contain letters, numbers, underscore, spaces, and dashes",
|
||||
"examples": [
|
||||
"myNewRun"
|
||||
],
|
||||
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"postFail": {
|
||||
"default": "next",
|
||||
"description": "Do this behavior if a Check is NOT triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"postTrigger": {
|
||||
"default": "nextRun",
|
||||
"description": "Do this behavior if a Check is triggered",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SearchAndReplaceRegExp": {
|
||||
"properties": {
|
||||
"replace": {
|
||||
@@ -3431,6 +3509,16 @@
|
||||
"description": "If notifications are configured and this is `true` then an `eventActioned` event will be sent when this check is triggered.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"postFail": {
|
||||
"default": "next",
|
||||
"description": "Do this behavior if a Check is NOT triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"postTrigger": {
|
||||
"default": "nextRun",
|
||||
"description": "Do this behavior if a Check is triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"rules": {
|
||||
"description": "A list of Rules to run.\n\nIf `Rule` objects are triggered based on `condition` then `actions` will be performed.\n\nCan be `Rule`, `RuleSet`, or the `name` of any **named** `Rule` in your subreddit's configuration.\n\n**If `rules` is an empty array or not present then `actions` are performed immediately.**",
|
||||
"items": {
|
||||
@@ -3700,21 +3788,18 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -3798,21 +3883,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The type of action that will be performed",
|
||||
@@ -3980,6 +4062,10 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"postCheckBehaviorDefaults": {
|
||||
"$ref": "#/definitions/PostBehavior",
|
||||
"description": "Set the default post-check behavior for all checks. If this property is specified it will override any defaults passed from the bot's config\n\nDefault behavior is:\n\n* postFail => next\n* postTrigger => nextRun"
|
||||
},
|
||||
"queue": {
|
||||
"properties": {
|
||||
"maxWorkers": {
|
||||
@@ -3993,11 +4079,16 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"runs": {
|
||||
"description": "A list of sets of Checks to run",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RunJson"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checks"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,14 @@
|
||||
"$ref": "#/definitions/FilterCriteriaDefaults",
|
||||
"description": "Define the default behavior for all filter criteria on all checks in all subreddits\n\nDefaults to exclude mods and automoderator from checks"
|
||||
},
|
||||
"flowControlDefaults": {
|
||||
"properties": {
|
||||
"maxGotoDepth": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -319,6 +327,9 @@
|
||||
],
|
||||
"description": "Settings related to default polling configurations for subreddits"
|
||||
},
|
||||
"postCheckBehaviorDefaults": {
|
||||
"$ref": "#/definitions/PostBehavior"
|
||||
},
|
||||
"queue": {
|
||||
"description": "Settings related to default configurations for queue behavior for subreddits",
|
||||
"properties": {
|
||||
@@ -382,6 +393,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"overrides": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SubredditOverrides"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"wikiConfig": {
|
||||
"default": "botconfig/contextbot",
|
||||
"description": "The default relative url to the ContextMod wiki page EX `https://reddit.com/r/subreddit/wiki/<path>`\n\n* ENV => `WIKI_CONFIG`\n* ARG => `--wikiConfig <path>`",
|
||||
@@ -580,20 +597,17 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemIsBehavior": {
|
||||
"description": "Determine how itemIs defaults behave when itemIs is present on the check\n\n* merge => adds defaults to check's itemIs\n* replace => check itemIs will replace defaults (no defaults used)",
|
||||
@@ -1033,6 +1047,21 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PostBehavior": {
|
||||
"properties": {
|
||||
"postFail": {
|
||||
"default": "next",
|
||||
"description": "Do this behavior if a Check is NOT triggered",
|
||||
"type": "string"
|
||||
},
|
||||
"postTrigger": {
|
||||
"default": "nextRun",
|
||||
"description": "Do this behavior if a Check is triggered",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RedditCredentials": {
|
||||
"description": "Credentials required for the bot to interact with Reddit's API\n\nThese credentials will provided to both the API and Web interface unless otherwise specified with the `web.credentials` property\n\nRefer to the [required credentials table](https://github.com/FoxxMD/context-mod/blob/master/docs/operatorConfiguration.md#minimum-required-configuration) to see what is necessary to run the bot.",
|
||||
"examples": [
|
||||
@@ -1203,6 +1232,25 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SubredditOverrides": {
|
||||
"properties": {
|
||||
"flowControlDefaults": {
|
||||
"properties": {
|
||||
"maxGotoDepth": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThirdPartyCredentialsJsonConfig": {
|
||||
"additionalProperties": {},
|
||||
"properties": {
|
||||
@@ -1384,6 +1432,10 @@
|
||||
"$ref": "#/definitions/SnoowrapOptions",
|
||||
"description": "Set global snoowrap options as well as default snoowrap config for all bots that don't specify their own"
|
||||
},
|
||||
"userAgent": {
|
||||
"description": "Added to the User-Agent information sent to reddit\n\nThis string will be added BETWEEN version and your bot name.\n\nEX: `myBranch` => `web:contextMod:v1.0.0-myBranch:BOT-/u/MyBotUser`\n\n* ENV => `USER_AGENT`",
|
||||
"type": "string"
|
||||
},
|
||||
"web": {
|
||||
"description": "Settings for the web interface",
|
||||
"properties": {
|
||||
|
||||
@@ -57,6 +57,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"subredditThreshold": {
|
||||
"default": ">= 1",
|
||||
"description": "A string containing a comparison operator and a value to compare the **number of subreddits that have valid activities** against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 3` => greater than 3 Subreddits found with valid activities\n* EX `<= 75%` => number of Subreddits with valid activities are equal to or less than 75% of all Subreddits found\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Subreddits found\" is only pertains to Subreddits that had the Link of the Submission, rather than all Subreddits from this window.",
|
||||
"examples": [
|
||||
">= 1"
|
||||
],
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
"type": "string"
|
||||
},
|
||||
"subreddits": {
|
||||
"description": "Activities will be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
|
||||
"examples": [
|
||||
@@ -366,21 +375,18 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -622,21 +628,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -921,21 +924,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1127,21 +1127,18 @@
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1402,21 +1399,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1523,21 +1517,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"keepRemoved": {
|
||||
"default": false,
|
||||
@@ -1799,21 +1790,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"subredditThreshold": {
|
||||
"default": ">= 1",
|
||||
"description": "A string containing a comparison operator and a value to compare the **number of subreddits that have valid activities** against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 3` => greater than 3 Subreddits found with valid activities\n* EX `<= 75%` => number of Subreddits with valid activities are equal to or less than 75% of all Subreddits found\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Subreddits found\" is only pertains to Subreddits that had the Link of the Submission, rather than all Subreddits from this window.",
|
||||
"examples": [
|
||||
">= 1"
|
||||
],
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
"type": "string"
|
||||
},
|
||||
"subreddits": {
|
||||
"description": "Activities will be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
|
||||
"examples": [
|
||||
@@ -340,21 +349,18 @@
|
||||
"type": "string"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -596,21 +602,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -895,21 +898,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1101,21 +1101,18 @@
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1376,21 +1373,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
@@ -1497,21 +1491,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"keepRemoved": {
|
||||
"default": false,
|
||||
@@ -1773,21 +1764,18 @@
|
||||
"type": "array"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SubmissionState"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
{
|
||||
"$ref": "#/definitions/CommentState"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "The kind of rule to run",
|
||||
|
||||
@@ -3,13 +3,14 @@ import {Logger} from "winston";
|
||||
import {SubmissionCheck} from "../Check/SubmissionCheck";
|
||||
import {CommentCheck} from "../Check/CommentCheck";
|
||||
import {
|
||||
asSubmission,
|
||||
cacheStats,
|
||||
createHistoricalStatsDisplay,
|
||||
createRetryHandler,
|
||||
determineNewResults,
|
||||
findLastIndex,
|
||||
formatNumber, likelyJson5,
|
||||
mergeArr,
|
||||
formatNumber, isSubmission, likelyJson5,
|
||||
mergeArr, normalizeName,
|
||||
parseFromJsonOrYamlToObject,
|
||||
parseRedditEntity,
|
||||
pollingInfo,
|
||||
@@ -22,11 +23,11 @@ import {RuleResult} from "../Rule";
|
||||
import {ConfigBuilder, buildPollingOptions} from "../ConfigBuilder";
|
||||
import {
|
||||
ActionedEvent,
|
||||
ActionResult,
|
||||
ActionResult, CheckResult, CheckSummary,
|
||||
DEFAULT_POLLING_INTERVAL,
|
||||
DEFAULT_POLLING_LIMIT, FilterCriteriaDefaults, Invokee,
|
||||
ManagerOptions, ManagerStateChangeOption, ManagerStats, PAUSED,
|
||||
PollingOptionsStrong, PollOn, RUNNING, RunState, STOPPED, SYSTEM, USER
|
||||
DEFAULT_POLLING_LIMIT, FilterCriteriaDefaults, Invokee, LogInfo,
|
||||
ManagerOptions, ManagerStateChangeOption, ManagerStats, NotificationEventPayload, PAUSED,
|
||||
PollingOptionsStrong, PollOn, PostBehavior, PostBehaviorTypes, RUNNING, RunResult, RunState, STOPPED, SYSTEM, USER
|
||||
} from "../Common/interfaces";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import {activityIsRemoved, itemContentPeek} from "../Utils/SnoowrapUtils";
|
||||
@@ -44,12 +45,14 @@ import dayjs, {Dayjs as DayjsObj} from "dayjs";
|
||||
import Action from "../Action";
|
||||
import {queue, QueueObject} from 'async';
|
||||
import {JSONConfig} from "../JsonConfig";
|
||||
import {CheckStructuredJson} from "../Check";
|
||||
import {Check, CheckStructuredJson} from "../Check";
|
||||
import NotificationManager from "../Notification/NotificationManager";
|
||||
import {createHistoricalDefaults, historicalDefaults} from "../Common/defaults";
|
||||
import {ExtendedSnoowrap} from "../Utils/SnoowrapClients";
|
||||
import {CMError, isRateLimitError, isStatusError} from "../Utils/Errors";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {CMError, isRateLimitError, isStatusError, RunProcessingError} from "../Utils/Errors";
|
||||
import {ErrorWithCause, stackWithCauses} from "pony-cause";
|
||||
import {Run} from "../Run";
|
||||
import got from "got";
|
||||
|
||||
export interface RunningState {
|
||||
state: RunState,
|
||||
@@ -62,10 +65,11 @@ export interface runCheckOptions {
|
||||
dryRun?: boolean,
|
||||
refresh?: boolean,
|
||||
force?: boolean,
|
||||
gotoContext?: string
|
||||
maxGotoDepth?: number
|
||||
}
|
||||
|
||||
export interface CheckTask {
|
||||
checkType: ('Comment' | 'Submission'),
|
||||
activity: (Submission | Comment),
|
||||
options?: runCheckOptions
|
||||
}
|
||||
@@ -73,8 +77,9 @@ export interface CheckTask {
|
||||
export interface RuntimeManagerOptions extends ManagerOptions {
|
||||
sharedStreams?: PollOn[];
|
||||
wikiLocation?: string;
|
||||
botName: string;
|
||||
maxWorkers: number;
|
||||
botName?: string;
|
||||
maxWorkers?: number;
|
||||
maxGotoDepth?: number
|
||||
}
|
||||
|
||||
interface QueuedIdentifier {
|
||||
@@ -87,16 +92,23 @@ export class Manager extends EventEmitter {
|
||||
subreddit: Subreddit;
|
||||
client: ExtendedSnoowrap;
|
||||
logger: Logger;
|
||||
logs: LogInfo[] = [];
|
||||
botName: string;
|
||||
pollOptions: PollingOptionsStrong[] = [];
|
||||
submissionChecks!: SubmissionCheck[];
|
||||
commentChecks!: CommentCheck[];
|
||||
get submissionChecks() {
|
||||
return this.runs.map(x => x.submissionChecks).flat();
|
||||
}
|
||||
get commentChecks() {
|
||||
return this.runs.map(x => x.commentChecks).flat();
|
||||
}
|
||||
runs: Run[] = []
|
||||
resources!: SubredditResources;
|
||||
wikiLocation: string;
|
||||
lastWikiRevision?: DayjsObj
|
||||
lastWikiCheck: DayjsObj = dayjs();
|
||||
wikiFormat: ('yaml' | 'json') = 'yaml';
|
||||
filterCriteriaDefaults?: FilterCriteriaDefaults
|
||||
postCheckBehaviorDefaults?: PostBehavior
|
||||
//wikiUpdateRunning: boolean = false;
|
||||
|
||||
streams: Map<string, SPoll<Snoowrap.Submission | Snoowrap.Comment>> = new Map();
|
||||
@@ -118,6 +130,7 @@ export class Manager extends EventEmitter {
|
||||
queuedItemsMeta: QueuedIdentifier[] = [];
|
||||
globalMaxWorkers: number;
|
||||
subMaxWorkers?: number;
|
||||
maxGotoDepth: number;
|
||||
|
||||
displayLabel: string;
|
||||
currentLabels: string[] = [];
|
||||
@@ -154,6 +167,8 @@ export class Manager extends EventEmitter {
|
||||
rulesUniqueRollingAvg: number = 0;
|
||||
actionedEvents: ActionedEvent[] = [];
|
||||
|
||||
processEmitter: EventEmitter = new EventEmitter();
|
||||
|
||||
getStats = async (): Promise<ManagerStats> => {
|
||||
const data: any = {
|
||||
eventsAvg: formatNumber(this.eventsRollingAvg),
|
||||
@@ -194,10 +209,19 @@ export class Manager extends EventEmitter {
|
||||
return this.displayLabel;
|
||||
}
|
||||
|
||||
constructor(sub: Subreddit, client: ExtendedSnoowrap, logger: Logger, cacheManager: BotResourcesManager, opts: RuntimeManagerOptions = {botName: 'ContextMod', maxWorkers: 1}) {
|
||||
constructor(sub: Subreddit, client: ExtendedSnoowrap, logger: Logger, cacheManager: BotResourcesManager, opts: RuntimeManagerOptions) {
|
||||
super();
|
||||
|
||||
const {dryRun, sharedStreams = [], wikiLocation = 'botconfig/contextbot', botName, maxWorkers, filterCriteriaDefaults} = opts;
|
||||
const {
|
||||
dryRun,
|
||||
sharedStreams = [],
|
||||
wikiLocation = 'botconfig/contextbot',
|
||||
botName = 'ContextMod',
|
||||
maxWorkers = 1,
|
||||
maxGotoDepth = 1,
|
||||
filterCriteriaDefaults,
|
||||
postCheckBehaviorDefaults
|
||||
} = opts || {};
|
||||
this.displayLabel = opts.nickname || `${sub.display_name_prefixed}`;
|
||||
const getLabels = this.getCurrentLabels;
|
||||
const getDisplay = this.getDisplay;
|
||||
@@ -211,14 +235,21 @@ export class Manager extends EventEmitter {
|
||||
return getDisplay()
|
||||
}
|
||||
}, mergeArr);
|
||||
this.logger.stream().on('log', (log: LogInfo) => {
|
||||
if(log.subreddit !== undefined && log.subreddit === this.getDisplay()) {
|
||||
this.logs = [log, ...this.logs].slice(0, 301);
|
||||
}
|
||||
});
|
||||
this.globalDryRun = dryRun;
|
||||
this.wikiLocation = wikiLocation;
|
||||
this.filterCriteriaDefaults = filterCriteriaDefaults;
|
||||
this.postCheckBehaviorDefaults = postCheckBehaviorDefaults;
|
||||
this.sharedStreams = sharedStreams;
|
||||
this.pollingRetryHandler = createRetryHandler({maxRequestRetry: 3, maxOtherRetry: 2}, this.logger);
|
||||
this.subreddit = sub;
|
||||
this.client = client;
|
||||
this.botName = botName;
|
||||
this.maxGotoDepth = maxGotoDepth;
|
||||
this.globalMaxWorkers = maxWorkers;
|
||||
this.notificationManager = new NotificationManager(this.logger, this.subreddit, this.displayLabel, botName);
|
||||
this.cacheManager = cacheManager;
|
||||
@@ -227,6 +258,8 @@ export class Manager extends EventEmitter {
|
||||
this.queue.pause();
|
||||
this.firehose = this.generateFirehose();
|
||||
|
||||
this.logger.info(`Max GOTO Depth: ${this.maxGotoDepth}`);
|
||||
|
||||
this.eventsSampleInterval = setInterval((function(self) {
|
||||
return function() {
|
||||
const et = self.resources !== undefined ? self.resources.stats.historical.allTime.eventsCheckedTotal : 0;
|
||||
@@ -268,6 +301,13 @@ export class Manager extends EventEmitter {
|
||||
//self.logger.debug(`Unique Rules Run Rolling Avg: ${formatNumber(self.rulesUniqueRollingAvg)}/s`);
|
||||
}
|
||||
})(this), 10000);
|
||||
|
||||
this.processEmitter.on('notify', (payload: NotificationEventPayload) => {
|
||||
this.notificationManager.handle(payload.type, payload.title, payload.body, payload.causedBy, payload.logLevel);
|
||||
});
|
||||
|
||||
// relay check/run errors to bot for retry metrics
|
||||
this.processEmitter.on('error', err => this.emit('error', err));
|
||||
}
|
||||
|
||||
public async getModPermissions(): Promise<string[]> {
|
||||
@@ -347,7 +387,7 @@ export class Manager extends EventEmitter {
|
||||
try {
|
||||
const itemMeta = this.queuedItemsMeta[queuedItemIndex];
|
||||
this.queuedItemsMeta.splice(queuedItemIndex, 1, {...itemMeta, state: 'processing'});
|
||||
await this.runChecks(task.checkType, task.activity, {...task.options, refresh: itemMeta.shouldRefresh});
|
||||
await this.handleActivity(task.activity, {...task.options, refresh: itemMeta.shouldRefresh});
|
||||
} finally {
|
||||
// always remove item meta regardless of success or failure since we are done with it meow
|
||||
this.queuedItemsMeta.splice(queuedItemIndex, 1);
|
||||
@@ -366,6 +406,14 @@ export class Manager extends EventEmitter {
|
||||
return q;
|
||||
}
|
||||
|
||||
public getCommentChecks() {
|
||||
return this.runs.map(x => x.commentChecks);
|
||||
}
|
||||
|
||||
public getSubmissionChecks() {
|
||||
return this.runs.map(x => x.commentChecks);
|
||||
}
|
||||
|
||||
protected async parseConfigurationFromObject(configObj: object, suppressChangeEvent: boolean = false) {
|
||||
try {
|
||||
const configBuilder = new ConfigBuilder({logger: this.logger});
|
||||
@@ -422,35 +470,51 @@ export class Manager extends EventEmitter {
|
||||
this.resources.setLogger(this.logger);
|
||||
|
||||
this.logger.info('Subreddit-specific options updated');
|
||||
this.logger.info('Building Checks...');
|
||||
this.logger.info('Building Runs and Checks...');
|
||||
|
||||
const commentChecks: Array<CommentCheck> = [];
|
||||
const subChecks: Array<SubmissionCheck> = [];
|
||||
const structuredChecks = configBuilder.parseToStructured(validJson, this.filterCriteriaDefaults);
|
||||
const structuredRuns = configBuilder.parseToStructured(validJson, this.filterCriteriaDefaults, this.postCheckBehaviorDefaults);
|
||||
|
||||
let runs: Run[] = [];
|
||||
|
||||
// TODO check that bot has permissions for subreddit for all specified actions
|
||||
// can find permissions in this.subreddit.mod_permissions
|
||||
|
||||
for (const jCheck of structuredChecks) {
|
||||
const checkConfig = {
|
||||
...jCheck,
|
||||
dryRun: this.dryRun || jCheck.dryRun,
|
||||
let index = 1;
|
||||
for (const r of structuredRuns) {
|
||||
const {name = `Run${index}`, ...rest} = r;
|
||||
const run = new Run({
|
||||
name,
|
||||
...rest,
|
||||
logger: this.logger,
|
||||
subredditName: this.subreddit.display_name,
|
||||
resources: this.resources,
|
||||
subredditName: this.subreddit.display_name,
|
||||
client: this.client,
|
||||
};
|
||||
if (jCheck.kind === 'comment') {
|
||||
commentChecks.push(new CommentCheck(checkConfig));
|
||||
} else if (jCheck.kind === 'submission') {
|
||||
subChecks.push(new SubmissionCheck(checkConfig));
|
||||
}
|
||||
emitter: this.processEmitter,
|
||||
});
|
||||
runs.push(run);
|
||||
index++;
|
||||
}
|
||||
|
||||
// make sure run names are unique
|
||||
const rNames: string[] = [];
|
||||
for(const r of runs) {
|
||||
if(rNames.includes(normalizeName(r.name))) {
|
||||
throw new Error(`Rule names must be unique. Duplicate name detected: ${r.name}`);
|
||||
}
|
||||
rNames.push(normalizeName(r.name));
|
||||
}
|
||||
|
||||
this.runs = runs;
|
||||
const runSummary = `Found ${runs.length} Runs with ${this.submissionChecks.length + this.commentChecks.length} Checks`;
|
||||
|
||||
if(this.runs.length === 0) {
|
||||
this.logger.warn(runSummary);
|
||||
} else {
|
||||
this.logger.info(runSummary);
|
||||
}
|
||||
|
||||
this.submissionChecks = subChecks;
|
||||
this.commentChecks = commentChecks;
|
||||
const checkSummary = `Found Checks -- Submission: ${this.submissionChecks.length} | Comment: ${this.commentChecks.length}`;
|
||||
if (subChecks.length === 0 && commentChecks.length === 0) {
|
||||
if (this.submissionChecks.length === 0 && this.commentChecks.length === 0) {
|
||||
this.logger.warn(checkSummary);
|
||||
} else {
|
||||
this.logger.info(checkSummary);
|
||||
@@ -595,8 +659,8 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async runChecks(checkType: ('Comment' | 'Submission'), activity: (Submission | Comment), options?: runCheckOptions): Promise<void> {
|
||||
const checks = checkType === 'Comment' ? this.commentChecks : this.submissionChecks;
|
||||
async handleActivity(activity: (Submission | Comment), options?: runCheckOptions): Promise<void> {
|
||||
const checkType = isSubmission(activity) ? 'Submission' : 'Comment';
|
||||
let item = activity;
|
||||
const itemId = await item.id;
|
||||
|
||||
@@ -612,44 +676,36 @@ export class Manager extends EventEmitter {
|
||||
}
|
||||
|
||||
let allRuleResults: RuleResult[] = [];
|
||||
const runResults: RunResult[] = [];
|
||||
const itemIdentifier = `${checkType === 'Submission' ? 'SUB' : 'COM'} ${itemId}`;
|
||||
this.currentLabels = [itemIdentifier];
|
||||
let ePeek = '';
|
||||
try {
|
||||
const [peek, _] = await itemContentPeek(item);
|
||||
ePeek = peek;
|
||||
const [peek, { content: peekContent }] = await itemContentPeek(item);
|
||||
ePeek = peekContent;
|
||||
this.logger.info(`<EVENT> ${peek}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error occurred while generate item peek for ${checkType} Activity ${itemId}`, err);
|
||||
this.logger.error(`Error occurred while generating item peek for ${checkType} Activity ${itemId}`, err);
|
||||
}
|
||||
|
||||
let checksRun = 0;
|
||||
let actionsRun = 0;
|
||||
let totalRulesRun = 0;
|
||||
let runActions: ActionResult[] = [];
|
||||
let actionedEvent: ActionedEvent = {
|
||||
triggered: false,
|
||||
subreddit: this.subreddit.display_name_prefixed,
|
||||
activity: {
|
||||
peek: ePeek,
|
||||
link: item.permalink
|
||||
link: item.permalink,
|
||||
type: checkType === 'Submission' ? 'submission' : 'comment',
|
||||
id: itemId,
|
||||
author: item.author.name,
|
||||
subreddit: item.subreddit_name_prefixed
|
||||
},
|
||||
author: item.author.name,
|
||||
timestamp: Date.now(),
|
||||
check: '',
|
||||
ruleSummary: '',
|
||||
ruleResults: [],
|
||||
actionResults: [],
|
||||
runResults: []
|
||||
}
|
||||
let triggered = false;
|
||||
let triggeredCheckName;
|
||||
const checksRunNames = [];
|
||||
const cachedCheckNames = [];
|
||||
const startingApiLimit = this.client.ratelimitRemaining;
|
||||
|
||||
const {
|
||||
checkNames = [],
|
||||
delayUntil,
|
||||
dryRun,
|
||||
refresh = false,
|
||||
} = options || {};
|
||||
|
||||
@@ -676,7 +732,7 @@ export class Manager extends EventEmitter {
|
||||
item = await activity.refresh();
|
||||
}
|
||||
|
||||
if (item instanceof Submission) {
|
||||
if (asSubmission(item)) {
|
||||
if (await item.removed_by_category === 'deleted') {
|
||||
this.logger.warn('Submission was deleted, cannot process.');
|
||||
return;
|
||||
@@ -686,100 +742,117 @@ export class Manager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const check of checks) {
|
||||
if (checkNames.length > 0 && !checkNames.map(x => x.toLowerCase()).some(x => x === check.name.toLowerCase())) {
|
||||
this.logger.warn(`Check ${check.name} not in array of requested checks to run, skipping...`);
|
||||
continue;
|
||||
}
|
||||
if(!check.enabled) {
|
||||
this.logger.info(`Check ${check.name} not run because it is not enabled, skipping...`);
|
||||
continue;
|
||||
}
|
||||
checksRunNames.push(check.name);
|
||||
checksRun++;
|
||||
triggered = false;
|
||||
let isFromCache = false;
|
||||
let currentResults: RuleResult[] = [];
|
||||
try {
|
||||
const [checkTriggered, checkResults, fromCache = false] = await check.runRules(item, allRuleResults);
|
||||
isFromCache = fromCache;
|
||||
if(!fromCache) {
|
||||
await check.setCacheResult(item, {result: checkTriggered, ruleResults: checkResults});
|
||||
// for now disallow the same goto from being run twice
|
||||
// maybe in the future this can be user-configurable
|
||||
const hitGotos: string[] = [];
|
||||
|
||||
let continueRunIteration = true;
|
||||
let runIndex = 0;
|
||||
let gotoContext: string = '';
|
||||
while(continueRunIteration && (runIndex < this.runs.length || gotoContext !== '')) {
|
||||
let currRun: Run;
|
||||
if(gotoContext !== '') {
|
||||
hitGotos.push(gotoContext);
|
||||
if(hitGotos.filter(x => x === gotoContext).length > this.maxGotoDepth) {
|
||||
throw new Error(`The goto "${gotoContext}" has been triggered ${hitGotos.filter(x => x === gotoContext).length} times which is more than the max allowed for any single goto (${this.maxGotoDepth}).
|
||||
This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself! The max triggered depth can be configured by the operator.`);
|
||||
}
|
||||
const [runName] = gotoContext.split('.');
|
||||
const gotoIndex = this.runs.findIndex(x => normalizeName(x.name) === normalizeName(runName));
|
||||
if(gotoIndex !== -1) {
|
||||
if(gotoIndex > runIndex) {
|
||||
this.logger.debug(`Fast forwarding Run iteration to ${this.runs[gotoIndex].name}`, {leaf: 'GOTO'});
|
||||
} else if(gotoIndex < runIndex) {
|
||||
this.logger.debug(`Rewinding Run iteration to ${this.runs[gotoIndex].name}`, {leaf: 'GOTO'});
|
||||
} else {
|
||||
this.logger.debug(`Did not iterate to next Run due to GOTO specifying same run`, {leaf: 'GOTO'});
|
||||
}
|
||||
currRun = this.runs[gotoIndex];
|
||||
runIndex = gotoIndex;
|
||||
if(!gotoContext.includes('.')) {
|
||||
// goto completed, no check
|
||||
gotoContext = '';
|
||||
}
|
||||
} else {
|
||||
cachedCheckNames.push(check.name);
|
||||
throw new Error(`GOTO specified a Run that could not be found: ${runName}`);
|
||||
}
|
||||
currentResults = checkResults;
|
||||
totalRulesRun += checkResults.length;
|
||||
allRuleResults = allRuleResults.concat(determineNewResults(allRuleResults, checkResults));
|
||||
triggered = checkTriggered;
|
||||
if(triggered && fromCache && !check.cacheUserResult.runActions) {
|
||||
this.logger.info('Check was triggered but cache result options specified NOT to run actions...counting as check NOT triggered');
|
||||
triggered = false;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.logged !== true) {
|
||||
this.logger.warn(`Running rules for Check ${check.name} failed due to uncaught exception`, e);
|
||||
}
|
||||
this.emit('error', e);
|
||||
} else {
|
||||
currRun = this.runs[runIndex];
|
||||
}
|
||||
|
||||
if (triggered) {
|
||||
triggeredCheckName = check.name;
|
||||
actionedEvent.check = check.name;
|
||||
actionedEvent.ruleResults = currentResults;
|
||||
if(isFromCache) {
|
||||
actionedEvent.ruleSummary = `Check result was found in cache: ${triggeredIndicator(true)}`;
|
||||
} else {
|
||||
actionedEvent.ruleSummary = resultsSummary(currentResults, check.condition);
|
||||
}
|
||||
runActions = await check.runActions(item, currentResults.filter(x => x.triggered), dryRun);
|
||||
// we only can about report and comment actions since those can produce items for newComm and modqueue
|
||||
const recentCandidates = runActions.filter(x => ['report','comment'].includes(x.kind.toLocaleLowerCase())).map(x => x.touchedEntities === undefined ? [] : x.touchedEntities).flat();
|
||||
for(const recent of recentCandidates) {
|
||||
await this.resources.setRecentSelf(recent as (Submission|Comment));
|
||||
}
|
||||
actionsRun = runActions.length;
|
||||
const [runResult, postBehavior] = await currRun.handle(item,allRuleResults, runResults.filter(x => x.name === currRun.name), {...options, gotoContext, maxGotoDepth: this.maxGotoDepth});
|
||||
runResults.push(runResult);
|
||||
|
||||
if(check.notifyOnTrigger) {
|
||||
const ar = runActions.filter(x => x.success).map(x => x.name).join(', ');
|
||||
this.notificationManager.handle('eventActioned', 'Check Triggered', `Check "${check.name}" was triggered on Event: \n\n ${ePeek} \n\n with the following actions run: ${ar}`);
|
||||
}
|
||||
break;
|
||||
allRuleResults = allRuleResults.concat(determineNewResults(allRuleResults, (runResult.checkResults ?? []).map(x => x.ruleResults).flat()));
|
||||
|
||||
switch (postBehavior.toLowerCase()) {
|
||||
case 'next':
|
||||
case 'nextrun':
|
||||
continueRunIteration = true;
|
||||
gotoContext = '';
|
||||
break;
|
||||
case 'stop':
|
||||
continueRunIteration = false;
|
||||
gotoContext = '';
|
||||
break;
|
||||
default:
|
||||
if (postBehavior.includes('goto:')) {
|
||||
gotoContext = postBehavior.split(':')[1];
|
||||
}
|
||||
}
|
||||
runIndex++;
|
||||
}
|
||||
|
||||
if (!triggered) {
|
||||
this.logger.info('No checks triggered');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
if (!(err instanceof LoggedError) && err.logged !== true) {
|
||||
this.logger.error('An unhandled error occurred while running checks', err);
|
||||
if(err instanceof RunProcessingError && err.result !== undefined) {
|
||||
runResults.push(err.result);
|
||||
}
|
||||
const processError = new ErrorWithCause('Activity processing terminated early due to unexpected error', {cause: err});
|
||||
this.logger.error(processError);
|
||||
this.emit('error', err);
|
||||
} finally {
|
||||
actionedEvent.triggered = runResults.some(x => x.triggered);
|
||||
if(!actionedEvent.triggered) {
|
||||
this.logger.verbose('No checks triggered');
|
||||
}
|
||||
try {
|
||||
actionedEvent.actionResults = runActions;
|
||||
if(triggered) {
|
||||
//actionedEvent.actionResults = runActions;
|
||||
actionedEvent.runResults = runResults;
|
||||
if(actionedEvent.triggered) {
|
||||
// only get parent submission info if we are actually going to use this event
|
||||
if(checkType === 'Comment') {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const subProxy = await this.client.getSubmission(await item.link_id);
|
||||
const sub = await this.resources.getActivity(subProxy);
|
||||
const [peek, { content: peekContent, author, permalink }] = await itemContentPeek(sub);
|
||||
actionedEvent.parentSubmission = {
|
||||
peek: peekContent,
|
||||
author,
|
||||
subreddit: item.subreddit_name_prefixed,
|
||||
id: (item as Comment).link_id,
|
||||
type: 'comment',
|
||||
link: permalink
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error occurred while generating item peek for ${checkType} Activity ${itemId}`, err);
|
||||
}
|
||||
}
|
||||
await this.resources.addActionedEvent(actionedEvent);
|
||||
}
|
||||
|
||||
const checksRun = actionedEvent.runResults.map(x => x.checkResults).flat().length;
|
||||
let actionsRun = actionedEvent.runResults.map(x => x.checkResults?.map(y => y.actionResults)).flat().length;
|
||||
let totalRulesRun = actionedEvent.runResults.map(x => x.checkResults?.map(y => y.ruleResults)).flat().length;
|
||||
|
||||
this.logger.verbose(`Run Stats: Checks ${checksRun} | Rules => Total: ${totalRulesRun} Unique: ${allRuleResults.length} Cached: ${totalRulesRun - allRuleResults.length} Rolling Avg: ~${formatNumber(this.rulesUniqueRollingAvg)}/s | Actions ${actionsRun}`);
|
||||
this.logger.verbose(`Reddit API Stats: Initial ${startingApiLimit} | Current ${this.client.ratelimitRemaining} | Used ~${startingApiLimit - this.client.ratelimitRemaining} | Events ~${formatNumber(this.eventsRollingAvg)}/s`);
|
||||
this.currentLabels = [];
|
||||
} catch (err: any) {
|
||||
this.logger.error('Error occurred while cleaning up Activity check and generating stats', err);
|
||||
this.logger.error(new ErrorWithCause('Error occurred while cleaning up Activity check and generating stats', {cause: err}));
|
||||
} finally {
|
||||
this.resources.updateHistoricalStats({
|
||||
eventsCheckedTotal: 1,
|
||||
eventsActionedTotal: triggered ? 1 : 0,
|
||||
checksTriggered: triggeredCheckName !== undefined ? [triggeredCheckName] : [],
|
||||
checksRun: checksRunNames,
|
||||
checksFromCache: cachedCheckNames,
|
||||
actionsRun: runActions.map(x => x.name),
|
||||
rulesRun: allRuleResults.map(x => x.name),
|
||||
rulesTriggered: allRuleResults.filter(x => x.triggered).map(x => x.name),
|
||||
rulesCachedTotal: totalRulesRun - allRuleResults.length,
|
||||
eventsActionedTotal: actionedEvent.triggered ? 1 : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -902,7 +975,7 @@ export class Manager extends EventEmitter {
|
||||
checkType = 'Comment';
|
||||
}
|
||||
if (checkType !== undefined) {
|
||||
this.firehose.push({checkType, activity: item, options: {delayUntil}})
|
||||
this.firehose.push({activity: item, options: {delayUntil}})
|
||||
}
|
||||
};
|
||||
|
||||
|
||||