Compare commits

..

83 Commits

Author SHA1 Message Date
FoxxMD
f7cebc013b Merge branch 'edge' 2022-03-08 09:48:06 -05:00
FoxxMD
f4103206db docs: some clarifications in flow control wording 2022-03-07 13:17:17 -05:00
FoxxMD
c9b1bfed40 fix(docs): mermaid issue was being inside closed detail block
For some reason. Removing blocks fixed rendering error
2022-03-07 13:11:45 -05:00
FoxxMD
7f764b4d99 fix(docs): Replace colon causing issues with mermaid diagram rendering 2022-03-07 13:06:12 -05:00
FoxxMD
fb7ddbba70 docs: Add overview for runs and flow control #73 2022-03-07 13:02:17 -05:00
FoxxMD
85b1d13718 docs: Refactor docs to use runs syntax
* Add Runs to main docs readme and concepts
* Add high level diagram in main docs readme to show CM lifecycle
* Refactor subreddit/rule examples to use runs syntax
2022-03-07 10:28:48 -05:00
FoxxMD
7f2191a11a fix: Fix bug where submission identifier was accessed via 'id' instead of 'name' property
* Was causing uncaught promise rejection in userflairaction because it should have been accessing name instead of id
* Wrap all as/is utility functions where value may be from cache (plain object) or proxy (from snoowrap) with try-catch to prevent any more uncaught promise rejections -- would rather swallow silently (for now) than crash the entire application
2022-03-06 19:30:25 -05:00
FoxxMD
9b79bdbdd5 feat(ui): Make submission context in actioned event more succinct 2022-03-04 21:08:43 -05:00
FoxxMD
6b9852cc14 feat(recent activity): Implement ability to compare distinct subreddits found
Closes #77
2022-03-03 13:16:39 -05:00
FoxxMD
fbf627c971 feat(actioned event): Add more context for ui and search
* Add id and activity type to event activity data
* Include parent submission activity data if activity is a comment
* Refactor event page ui to simplify headers and move content into collapsible
  * Add context to content by including submission context for comments
2022-03-02 15:10:27 -05:00
FoxxMD
b2077132cf fix(ui): Don't strip urls from body when previewing comment in events 2022-03-02 13:31:18 -05:00
FoxxMD
f622c3ee03 fix(errors): try-catch on transform for good measure
Would rather just get top-level error than crash the app and log a bad transformation (which tells us nothing about the original error)
2022-03-02 11:22:59 -05:00
FoxxMD
ab83f3ed0c fix(errors): Another better check for respsone headers 2022-03-02 11:16:48 -05:00
FoxxMD
a021b503a0 fix(errors): Better check for response headers on error transform 2022-03-02 11:13:32 -05:00
FoxxMD
d28714aacc fix(usernote): Fix user note instantiation and link handling
* Fix argument order for new note object
* Better handling for notes without a reference link
2022-03-02 09:53:48 -05:00
FoxxMD
7632a66250 fix(user flair): Fix dry check 2022-03-01 13:45:27 -05:00
FoxxMD
bb6936d657 fix(check): Correctly get run actions for notification payload 2022-02-28 14:42:44 -05:00
FoxxMD
d4062b679a refactor(filter): Better handling for submissionState item criteria
* Allow FilterResult as a property result
* Remove pre-item testing cache optimization for submissionState to simplify flow
  * Helped reduce key count but not worth the cost of overly complex code for returning filter results
* Remove expected prop from results and instead use criteria in filter results to generate this for logs/events
* Refactor log/event generation to handle FilterResult in filter property result
2022-02-28 13:04:21 -05:00
FoxxMD
313ee0a9a3 fix(usernotes): Better handle missing moderators (no more errors thrown)
Fixes edge case where a usernote was created by a moderator that no longer mods the sub

* Store mod index so we can recreate note even if moderator is missing
* Refactor moderator hydration on usernote from raw data to just warn if moderator cannot be found
2022-02-28 12:58:24 -05:00
FoxxMD
7afc384d17 fix(ui): Fix filter numbering in events page 2022-02-28 12:56:26 -05:00
FoxxMD
fea1f240dd Merge branch 'edge' into flowControl 2022-02-28 10:01:52 -05:00
FoxxMD
1dba0e857f fix(logging): Better data shape checking for error transformer 2022-02-28 10:01:30 -05:00
FoxxMD
0966aa689f Merge branch 'edge' into flowControl 2022-02-22 15:55:00 -05:00
FoxxMD
138e237fbc fix(comment): Fix the target of the lock intention
Should be locking the created comment instead of the activity being checked
2022-02-22 13:29:58 -05:00
FoxxMD
6b38ec1669 Merge branch 'edge' into flowControl 2022-02-22 11:21:14 -05:00
FoxxMD
ae8e11feb4 Merge branch 'edge' 2022-02-22 11:11:46 -05:00
FoxxMD
5cd415e300 Bump version 2022-02-22 11:11:29 -05:00
FoxxMD
7cdaa4bf25 fix(migrations): Remove unnecessary log warning for all logs on live stats 2022-02-22 11:10:51 -05:00
FoxxMD
280ddf583b fix(migrations): Fix missing TTL for cache migration 2022-02-22 10:50:44 -05:00
FoxxMD
4969cafc97 fix(ui): Add missing dayjs plugins for timestamp formatting 2022-02-22 10:43:17 -05:00
FoxxMD
5f6e63542b feat(flow): Implement max goto depth configuration 2022-02-22 10:42:52 -05:00
FoxxMD
bca9c96468 fix(ui): Fix run summary typo 2022-02-22 09:44:49 -05:00
FoxxMD
7569c06a36 Merge branch 'edge' into flowControl
# Conflicts:
#	src/Bot/index.ts
#	src/Subreddit/Manager.ts
#	src/Web/Client/index.ts
#	src/Web/Server/routes/authenticated/user/index.ts
#	src/Web/assets/public/app.css
#	src/util.ts
2022-02-22 09:27:36 -05:00
FoxxMD
88bafbc1ac fix(ui): Fix not clearing intervals on client disconnect 2022-02-21 16:47:17 -05:00
FoxxMD
a5acd6ec83 feat: Refactor client/secret api interaction to improve fetching data and enable live stats
* Only return logs for "default viewed" subreddit/bot when fetching instance status, when specified from QS
  * Greatly reduces amount of data fetched and response time
* Return logs with formatted property for non-streaming response
* Implement server live stats endpoint to return subreddit/all stats based on QS
* Use client websocket connection to return stats for currently viewed subreddit
2022-02-21 16:14:41 -05:00
FoxxMD
d93c8bdef2 Merge branch 'docUpdates' into edge 2022-02-21 12:00:37 -05:00
FoxxMD
8a32bd6485 Merge branch 'edge' into logRefactor
# Conflicts:
#	src/Web/Client/index.ts
2022-02-18 15:45:28 -05:00
FoxxMD
425cbc4826 feat: Improve user agent reporting and version display in ui 2022-02-18 15:16:37 -05:00
FoxxMD
3a2d3f5047 refactor(logging): Use logging from CMInstance instead of client
Reduces logging complexity and has better single responsibility
2022-02-18 13:37:07 -05:00
FoxxMD
ae20b85400 refactor(client): Refactor server instance into own class
* Move from plain data with interface to class and refactor heartbeat logic into class
* Makes logging easier and cleans up client code
2022-02-18 13:09:33 -05:00
FoxxMD
e07b8cc291 Merge branch 'edge' 2022-02-18 11:58:28 -05:00
FoxxMD
e993c5d376 refactor(logging): Move log collection into bot/manager for better single responsibility
* Move "sorting" log objects into lists for retrieval from server and into bot/managers for each log object type
* Refactor log filtering and aggregration under status/log endpoints to use logs from each entity rather than pulling from server

Reduces complexity in historical log data structures at the expense of slightly more runtime data crunching. The trade-off is well worth it and paves the way for easier retrieval of single/subsets of logs
2022-02-18 11:58:13 -05:00
FoxxMD
80fabeac54 fix(usernote): Fix adding new note to user note cache AFTER clearing cache
* Fixes an issue where the cached notes for a user only contain the last added note instead of all notes + new
* Also reduced api calls by caching moderator adding new note instead of calling each time
2022-02-18 09:54:18 -05:00
FoxxMD
c001be9abf feat(ui): Add reddit status indicator with link 2022-02-17 16:14:36 -05:00
FoxxMD
639a542fb2 fix(ui): Fix default values for scopes and permissions when not available 2022-02-17 13:53:06 -05:00
FoxxMD
9299258de0 feat(ui): Add moderator permissions list to subreddit overview 2022-02-17 13:37:42 -05:00
FoxxMD
59f8ac6dd4 feat(ui): Add oauth scopes list to bot overview
Visible when user is an operator
2022-02-17 13:29:37 -05:00
FoxxMD
f16155bb1f fix(flair): Fix snoowrap function used for assigning flair template id 2022-02-17 13:17:17 -05:00
FoxxMD
e2d2f73bb3 feat: Add log warning when user has no access 2022-02-15 11:00:09 -05:00
FoxxMD
9ca5d6c8c2 fix: Fix config builder to supply more defaults for a minimal configuration
* Provide a default redirect uri
* Don't add default bot instance if no credentials were specified
2022-02-14 12:07:54 -05:00
FoxxMD
4f9d1c1ca1 docs: Some clarifications for install/run directions 2022-02-14 10:54:25 -05:00
FoxxMD
d8f673bd26 fix(remove): Only warn if item looks removed, on remove action
If the item is not actually removed (it's hard to tell from reddit api) we don't want to prematurely end remove action. Just warn and try to remove anyway
2022-02-14 09:31:50 -05:00
FoxxMD
7e2068d82a fix(author): Ensure automoderator is always detected as a moderator for author isMod test 2022-02-14 09:30:54 -05:00
FoxxMD
176611dbf3 docs: Add web interface and config onboarding 2022-02-11 23:40:28 -05:00
FoxxMD
372bae0e03 feat(ui): Implement displaying author/item filters for actioned events 2022-02-10 15:15:21 -05:00
FoxxMD
6f35ec3705 fix: Set process result error property with easier-to-read error message and include "not enabled" 2022-02-10 15:14:55 -05:00
FoxxMD
a542d80c1d feat(migration): Update cache migration for new actioned event structure 2022-02-10 13:54:49 -05:00
FoxxMD
9dcf256aa1 feat(filter): Add filters to all result data 2022-02-10 13:43:33 -05:00
FoxxMD
da206f41ad feat(filter): Refactor itemIs to use FilterCriteriaResult
Normalizes item is filter so it behaves and produces the same type of results as author filter
2022-02-10 13:37:04 -05:00
FoxxMD
550beb9baf feat: Add restricts on flow control to prevent endless loops
* Restrict checks/gotos to being run once
* Throw flow control errors instead of catching to ensure we exit flow early
2022-02-10 11:21:12 -05:00
FoxxMD
3d99406f33 Merge branch 'persistActions' into edge 2022-02-09 17:09:24 -05:00
FoxxMD
7f9adcef36 refactor: Refactor manager/run/check handling for better single responsibility 2022-02-09 16:47:57 -05:00
FoxxMD
ab355977ba fix(approve): Fix approval check target 2022-02-09 16:41:39 -05:00
FoxxMD
f24eb52697 Merge branch 'edge' into flowControl 2022-02-09 13:15:01 -05:00
FoxxMD
60dbc42148 Merge remote-tracking branch 'origin/flowControl' into flowControl
# Conflicts:
#	src/Check/index.ts
#	src/Subreddit/Manager.ts
#	src/util.ts
2022-02-09 13:12:52 -05:00
FoxxMD
8667fcdef3 fix(stats): Correctly initialize all time historical stats from cache when stat is empty 2022-02-09 13:10:05 -05:00
FoxxMD
ec20445772 refactor(ui): Use checkmark symbol that matches x symbol (no emojis) 2022-02-09 13:09:39 -05:00
FoxxMD
8d9fb29848 feat(ui): Implement run context for actioned events
* Refactor events view to show checks within runs
* Build cohesive runs server-side before rendering so user can see all checks in a run together
* Add collapse/expand behavior for activity/run/check with ability to toggle based on triggered state
* Default to collapsing all non-triggered states
* Build check summary on-the-fly instead of storing in event result data
2022-02-09 13:08:48 -05:00
FoxxMD
f7a7e817f9 feat(cache): Implement cache migrations
* Store migration state in cache instance
* Migrate on default cache init or private cache init
* Implement first migration to deal with run structure in actioned events
2022-02-08 17:00:18 -05:00
FoxxMD
e09cab6872 refactor(check): Refactor structure for storing check/action result for runs
* Use array of check result objects that contain rule/action results
* Include post behavior on check summary
2022-02-08 14:35:26 -05:00
FoxxMD
f1797f29fd Merge branch 'persistActions' into flowControl
# Conflicts:
#	src/util.ts
2022-02-08 13:39:36 -05:00
FoxxMD
4eae07f831 feat(flow): Implement basic flow control structures #73
* Add Run and postCheckBehavior config structures to schema and interfaces
* Implement parsing from config and initial flow logic for running on activities in manager
2022-02-08 13:39:02 -05:00
FoxxMD
0293928a99 feat(cache): Implement cache key manipulation based on key pattern
* Implement glob pattern or regex as argument
* Implement scan search for redis for efficiency otherwise iterate keys using generic function
* Implement cache reset based on passed item from action -- reset item crit for activities, author crit for users, and overwrite any cached activity
2022-02-08 13:01:09 -05:00
FoxxMD
b56d6dbe7c fix(actions): Only include successfully run actions in notification summary 2022-02-07 22:21:22 -05:00
FoxxMD
42d269e28d feat(actions): Mutate activities during actions for immediate use and ensure cache is synced 2022-02-07 16:21:43 -05:00
FoxxMD
8f60a1da53 feat(regex): Add option to stop rule early if current activity does not match
In order to prevent history from being pulled (and using api) if user indicates current activity must also match
2022-02-07 15:15:50 -05:00
FoxxMD
f511be7c33 fix(usernote): Throw error with cause when usernote fails instead of logging quietly
* Makes error cause easier to see in stack and fixes error now logging during action failure
* Use error with cause for logging action error for clearer stack
2022-02-07 12:41:10 -05:00
FoxxMD
ebb426e696 feat(filter): Add isRedditMediaDomain submission state criteria 2022-02-07 10:36:56 -05:00
FoxxMD
63696b746e feat(flow): Implement basic flow control structures #73
* Add Run and postCheckBehavior config structures to schema and interfaces
* Implement parsing from config and initial flow logic for running on activities in manager
2022-02-04 15:06:21 -05:00
FoxxMD
fc51928054 Merge branch 'edge' 2022-02-02 16:59:56 -05:00
FoxxMD
c07276a3be fix(logging): Fix typo in error transform 2022-02-01 13:13:27 -05:00
FoxxMD
4a2297f5cd docs: Add github sponsor link 2022-02-01 12:01:34 -05:00
FoxxMD
f8967d55c4 feat(repeat): Use newer text comparison technique to improve repeat detection
* Use same technique as repost rule which has high accuracy and let false-positives
* Implement ability to see similarity score, case sensitivity, and text transformations
2022-01-31 14:08:21 -05:00
135 changed files with 6955 additions and 3447 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [FoxxMD]
custom: ["bitcoincash:qqmpsh365r8n9jhp4p8ks7f7qdr7203cws4kmkmr8q"]

View File

@@ -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**
![Subreddit View](docs/screenshots/subredditStatus.jpg)
![Subreddit View](docs/images/subredditStatus.jpg)
### Bot Setup/Authentication
@@ -117,11 +117,11 @@ A bot oauth helper allows operators to define oauth credentials/permissions and
Operator view/invite link generation:
![Oauth View](docs/screenshots/oauth.jpg)
![Oauth View](docs/images/oauth.jpg)
Moderator view/invite and authorization:
![Invite View](docs/screenshots/oauth-invite.jpg)
![Invite View](docs/images/oauth-invite.jpg)
### 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
![Configuration View](docs/screenshots/editor.jpg)
![Configuration View](docs/images/editor.jpg)
## License

View File

@@ -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>
![Flow Diagram](/docs/images/diagram-highlevel.jpg)
</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).

View File

@@ -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)

View File

@@ -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

View 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]
```

View 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

View File

@@ -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}}"
}
]
},
]
},
}
]
}

View File

@@ -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}}

View File

@@ -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"
}
]
},
],
}
]
}

View File

@@ -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

View File

@@ -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}}"
}
]
}
]
}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}"
}
]
}
]
}

View File

@@ -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"
}
]
}
]
}

View File

@@ -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

View File

@@ -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"
}
]
}
]

View File

@@ -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

View File

@@ -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"
}
]
}
]

View File

@@ -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

View File

@@ -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"
}
]
}
]
}

View File

@@ -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

View File

@@ -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}}"
}
]
}
]

View File

@@ -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}}

View File

@@ -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"
}
]
}
]

View File

@@ -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

View File

@@ -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"
}
]
}
]
}

View File

@@ -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

View File

@@ -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}}"
}
]
}
]
}

View File

@@ -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}}

View File

@@ -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}}"
}
]
}
]
}

View File

@@ -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}}

View File

@@ -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"
}
]
}
]
],
}

View File

@@ -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

View File

@@ -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}}"
}
]
}
]
}
]
],
}

View File

@@ -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}}

View File

@@ -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}}"
}
]
}
]
}

View File

@@ -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}}

View File

@@ -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
}
]
}
]
}
]
],
}

View File

@@ -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

View File

@@ -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
}
]
}
]
],
}

View File

@@ -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

View File

@@ -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"
}
]
}
]
],
}

View File

@@ -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

View File

@@ -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
}
]
}
]
}

View File

@@ -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

View File

@@ -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,
}
]
}
]
],
}

View File

@@ -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

View File

@@ -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
}
]
}
]

View File

@@ -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

View File

@@ -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}}%"
}
]
}
]
}

View File

@@ -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}}%

View File

@@ -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}}%"
}
]
}
]
}

View File

@@ -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}}%

View File

@@ -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

View File

@@ -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)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https://github.com/FoxxMD/context-mod)

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/images/config/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

BIN
docs/images/logs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

BIN
docs/images/runInput.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

View File

@@ -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>

30
docs/webInterface.md Normal file
View File

@@ -0,0 +1,30 @@
## Editing/Updating Your Config
* Open the editor for your subreddit
* 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/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/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**
* Use `report` actions to get reports in your modqueue from the bot that describe what it detected and what it would do about it
* Once the bot is behaving as desired (no false positives or weird behavior) destructive actions can be enabled or turned off of dryrun
## Web Dashboard Tips
* 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/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/images/actionsEvents.png)
* This includes activities run with dry run

194
package-lock.json generated
View File

@@ -34,6 +34,7 @@
"express-socket.io-session": "^1.3.5",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.4.6",
"globrex": "^0.1.2",
"got": "^11.8.2",
"he": "^1.2.0",
"http-proxy": "^1.18.1",
@@ -43,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",
@@ -80,6 +82,7 @@
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/express-socket.io-session": "^1.3.6",
"@types/globrex": "^0.1.1",
"@types/he": "^1.1.1",
"@types/http-proxy": "^1.17.7",
"@types/js-yaml": "^4.0.1",
@@ -360,6 +363,15 @@
"@types/redis": "^2.8.0"
}
},
"node_modules/@types/cache-manager-redis-store/node_modules/@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -456,6 +468,12 @@
"@types/socket.io": "2.1.13"
}
},
"node_modules/@types/globrex": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@types/globrex/-/globrex-0.1.1.tgz",
"integrity": "sha512-bce8X5Yb8l8ou2VDaEG8CYY1p6NynmswkaasO1pdAzFASKJ43sjf9MQdVH6VmKNG2bPEEmvI5onJJSH+1qOMOA==",
"dev": true
},
"node_modules/@types/he": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.1.2.tgz",
@@ -607,15 +625,6 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -1422,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",
@@ -1554,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",
@@ -2147,6 +2172,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
},
"node_modules/google-auth-library": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.11.0.tgz",
@@ -2851,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",
@@ -2905,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",
@@ -3819,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",
@@ -4354,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",
@@ -4931,6 +5023,17 @@
"requires": {
"@types/cache-manager": "*",
"@types/redis": "^2.8.0"
},
"dependencies": {
"@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"requires": {
"@types/node": "*"
}
}
}
},
"@types/cacheable-request": {
@@ -5029,6 +5132,12 @@
"@types/socket.io": "2.1.13"
}
},
"@types/globrex": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@types/globrex/-/globrex-0.1.1.tgz",
"integrity": "sha512-bce8X5Yb8l8ou2VDaEG8CYY1p6NynmswkaasO1pdAzFASKJ43sjf9MQdVH6VmKNG2bPEEmvI5onJJSH+1qOMOA==",
"dev": true
},
"@types/he": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.1.2.tgz",
@@ -5180,15 +5289,6 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -5846,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",
@@ -5938,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",
@@ -6401,6 +6511,11 @@
"path-is-absolute": "^1.0.0"
}
},
"globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
},
"google-auth-library": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.11.0.tgz",
@@ -6969,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",
@@ -7005,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",
@@ -7670,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",
@@ -8079,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",

View File

@@ -49,6 +49,7 @@
"express-socket.io-session": "^1.3.5",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.4.6",
"globrex": "^0.1.2",
"got": "^11.8.2",
"he": "^1.2.0",
"http-proxy": "^1.18.1",
@@ -58,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",
@@ -95,6 +97,7 @@
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/express-socket.io-session": "^1.3.6",
"@types/globrex": "^0.1.1",
"@types/he": "^1.1.1",
"@types/http-proxy": "^1.17.7",
"@types/js-yaml": "^4.0.1",

View File

@@ -36,7 +36,7 @@ export class ApproveAction extends Action {
}
// @ts-ignore
if (item.approved) {
if (targetItem.approved) {
const msg = `${target === 'self' ? 'Item' : 'Comment\'s parent Submission'} is already approved`;
this.logger.warn(msg);
return {
@@ -54,6 +54,16 @@ export class ApproveAction extends Action {
}
// @ts-ignore
touchedEntities.push(await targetItem.approve());
if(target === 'self') {
// @ts-ignore
item.approved = true;
await this.resources.resetCacheForItem(item);
} else if(await this.resources.hasActivity(targetItem)) {
// @ts-ignore
targetItem.approved = true;
await this.resources.resetCacheForItem(targetItem);
}
}
}

View File

@@ -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,
};
}

View File

@@ -26,6 +26,9 @@ export class LockAction extends Action {
//snoowrap typing issue, thinks comments can't be locked
// @ts-ignore
await item.lock();
// @ts-ignore
item.locked = true;
await this.resources.resetCacheForItem(item);
touchedEntities.push(item);
}
return {

View File

@@ -4,6 +4,8 @@ import Snoowrap, {Comment, Submission} from "snoowrap";
import {RuleResult} from "../Rule";
import {activityIsRemoved} from "../Utils/SnoowrapUtils";
import {ActionProcessResult} from "../Common/interfaces";
import dayjs from "dayjs";
import {isSubmission} from "../util";
export class RemoveAction extends Action {
spam: boolean;
@@ -26,11 +28,7 @@ export class RemoveAction extends Action {
// issue with snoowrap typings, doesn't think prop exists on Submission
// @ts-ignore
if (activityIsRemoved(item)) {
return {
dryRun,
success: false,
result: 'Item is already removed',
}
this.logger.warn('It looks like this Item is already removed!');
}
if (this.spam) {
this.logger.verbose('Marking as spam on removal');
@@ -38,6 +36,13 @@ export class RemoveAction extends Action {
if (!dryRun) {
// @ts-ignore
await item.remove({spam: this.spam});
item.banned_at_utc = dayjs().unix();
item.spam = this.spam;
if(!isSubmission(item)) {
// @ts-ignore
item.removed = true;
}
await this.resources.resetCacheForItem(item);
touchedEntities.push(item);
}

View File

@@ -35,6 +35,7 @@ export class ReportAction extends Action {
await item.report({reason: truncatedContent});
// due to reddit not updating this in response (maybe)?? just increment stale activity
item.num_reports++;
await this.resources.resetCacheForItem(item);
touchedEntities.push(item);
}

View File

@@ -33,16 +33,26 @@ export class FlairAction extends Action {
if(this.css !== '') {
flairParts.push(`CSS: ${this.css}`);
}
if(this.flair_template_id !== '') {
flairParts.push(`Template: ${this.flair_template_id}`);
}
const flairSummary = flairParts.length === 0 ? 'No flair (unflaired)' : flairParts.join(' | ');
this.logger.verbose(flairSummary);
if (item instanceof Submission) {
if(!this.dryRun) {
if (this.flair_template_id) {
await item.selectFlair({flair_template_id: this.flair_template_id}).then(() => {});
// typings are wrong for this function, flair_template_id should be accepted
// assignFlair uses /api/flair (mod endpoint)
// selectFlair uses /api/selectflair (self endpoint for user to choose their own flair for submission)
// @ts-ignore
await item.assignFlair({flair_template_id: this.flair_template_id}).then(() => {});
item.link_flair_template_id = this.flair_template_id;
} else {
await item.assignFlair({text: this.text, cssClass: this.css}).then(() => {});
item.link_flair_css_class = this.css;
item.link_flair_text = this.text;
}
await this.resources.resetCacheForItem(item);
}
} else {
this.logger.warn('Cannot flair Comment');

View File

@@ -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
@@ -50,6 +50,7 @@ export class UserFlairAction extends Action {
flairTemplateId: this.flair_template_id,
username: item.author.name,
});
item.author_flair_template_id = this.flair_template_id
} catch (err: any) {
this.logger.error('Either the flair template ID is incorrect or you do not have permission to access it.');
throw err;
@@ -57,6 +58,9 @@ export class UserFlairAction extends Action {
} else if (this.text === undefined && this.css === undefined) {
// @ts-ignore
await item.subreddit.deleteUserFlair(item.author.name);
item.author_flair_css_class = null;
item.author_flair_text = null;
item.author_flair_template_id = null;
} else {
// @ts-ignore
await item.author.assignFlair({
@@ -64,6 +68,12 @@ export class UserFlairAction extends Action {
cssClass: this.css,
text: this.text,
});
item.author_flair_text = this.text ?? null;
item.author_flair_css_class = this.css ?? null;
}
await this.resources.resetCacheForItem(item);
if(typeof item.author !== 'string') {
await this.resources.resetCacheForItem(item.author);
}
}

View File

@@ -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,

View File

@@ -1,12 +1,13 @@
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";
import LoggedError from "../Utils/LoggedError";
import {ExtendedSnoowrap} from '../Utils/SnoowrapClients';
import {ErrorWithCause} from "pony-cause";
export abstract class Action {
name?: string;
@@ -68,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;
@@ -86,7 +94,8 @@ export abstract class Action {
return {...actRes, ...results};
} catch (err: any) {
if(!(err instanceof LoggedError)) {
this.logger.error(`Encountered error while running`, err);
const actionError = new ErrorWithCause('Action did not run successfully due to unexpected error', {cause: err});
this.logger.error(actionError);
}
actRes.success = false;
actRes.result = err.message;

View File

@@ -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));

View File

@@ -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.
*

View 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
}

View 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});
}
}

View 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);
});
})
}

View File

@@ -39,3 +39,5 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
]
}
}
export const VERSION = '0.10.12';

View File

@@ -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
}
/**
@@ -949,6 +961,10 @@ export interface SubmissionState extends ActivityState {
link_flair_text?: string | string[]
link_flair_css_class?: string | string[]
flairTemplate?: string | string[]
/**
* Is the submission a reddit-hosted image or video?
* */
isRedditMediaDomain?: boolean
}
// properties calculated/derived by CM -- not provided as plain values by reddit
@@ -1014,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,
@@ -1246,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
@@ -1441,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.
*
@@ -1476,6 +1509,12 @@ export interface BotInstanceJsonConfig {
* */
filterCriteriaDefaults?: FilterCriteriaDefaults
postCheckBehaviorDefaults?: PostBehavior
flowControlDefaults?: {
maxGotoDepth?: number
}
/**
* Settings related to bot behavior for subreddits it is managing
* */
@@ -1536,6 +1575,8 @@ export interface BotInstanceJsonConfig {
* @examples [300]
* */
heartbeatInterval?: number,
overrides?: SubredditOverrides[]
}
/**
@@ -1572,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,
}
/**
@@ -1686,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
* */
@@ -1846,6 +1899,7 @@ export interface BotInstanceConfig extends BotInstanceJsonConfig {
dryRun?: boolean,
wikiConfig: string,
heartbeatInterval: number,
overrides?: SubredditOverrides[]
},
polling: {
shared: PollOn[],
@@ -1861,6 +1915,7 @@ export interface BotInstanceConfig extends BotInstanceJsonConfig {
softLimit: number,
hardLimit: number,
}
userAgent?: string
}
export interface OperatorConfig extends OperatorJsonConfig {
@@ -1931,6 +1986,7 @@ export interface LogInfo {
instance?: string
labels?: string[]
bot?: string
user?: string
}
export interface ActionResult extends ActionProcessResult {
@@ -1938,6 +1994,8 @@ export interface ActionResult extends ActionProcessResult {
name: string,
run: boolean,
runReason?: string,
itemIs?: FilterResult<TypedActivityState>
authorIs?: FilterResult<AuthorCriteria>
}
export interface ActionProcessResult {
@@ -1947,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 {
@@ -2044,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';
@@ -2079,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
@@ -2100,3 +2188,92 @@ export interface FilterResult<T> {
join: JoinOperands
passed: boolean
}
export interface TextTransformOptions {
/**
* A set of search-and-replace operations to perform on text values before performing a match. Transformations are performed in the order they are defined.
*
* * If `transformationsActivity` IS NOT defined then these transformations will be performed on BOTH the activity text (submission title or comment) AND the repost candidate text
* * If `transformationsActivity` IS defined then these transformations are only performed on repost candidate text
* */
transformations?: SearchAndReplaceRegExp[]
/**
* Specify a separate set of transformations for the activity text (submission title or comment)
*
* To perform no transformations when `transformations` is defined set this to an empty array (`[]`)
* */
transformationsActivity?: SearchAndReplaceRegExp[]
}
export interface TextMatchOptions {
/**
* The percentage, as a whole number, of a repost title/comment that must match the title/comment being checked in order to consider both a match
*
* Note: Setting to 0 will make every candidate considered a match -- useful if you want to match if the URL has been reposted anywhere
*
* Defaults to `85` (85%)
*
* @default 85
* @example [85]
* */
matchScore?: number
/**
* The minimum number of words in the activity being checked for which this rule will run on
*
* If the word count is below the minimum the rule fails
*
* Defaults to 2
*
* @default 2
* @example [2]
* */
minWordCount?: number
/**
* Should text matching be case sensitive?
*
* Defaults to false
*
* @default false
* @example [false]
**/
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)>;

View File

@@ -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;
}
}
@@ -666,9 +678,13 @@ export const parseOperatorConfigFromSources = async (args: any): Promise<[Operat
defaultBotInstance.caching = configFromFile.caching;
}
let botInstances = [];
let botInstances: BotInstanceJsonConfig[] = [];
if (botInstancesFromFile.length === 0) {
botInstances = [defaultBotInstance];
// only add default bot if user supplied any credentials
// otherwise its most likely just default, empty settings
if(defaultBotInstance.credentials !== undefined) {
botInstances = [defaultBotInstance];
}
} else {
botInstances = botInstancesFromFile.map(x => merge.all([defaultBotInstance, x], {arrayMerge: overwriteMerge}));
}
@@ -694,6 +710,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
stream = {},
} = {},
caching: opCache,
userAgent,
web: {
port = 8085,
maxLogs = 200,
@@ -772,6 +789,10 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
...fileRest
} = file;
const defaultWebCredentials = {
redirectUri: 'http://localhost:8085/callback'
};
const config: OperatorConfig = {
mode,
@@ -796,6 +817,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
}
},
caching: cache,
userAgent,
web: {
port,
caching: {
@@ -811,7 +833,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
},
maxLogs,
clients: clients === undefined ? [{host: 'localhost:8095', secret: apiSecret}] : clients,
credentials: webCredentials as RequiredWebRedditCredentials,
credentials: {...defaultWebCredentials, ...webCredentials} as RequiredWebRedditCredentials,
operators: operators || defaultOperators,
},
api: {
@@ -835,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 = [],
@@ -856,8 +880,10 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
hardLimit = 50
} = {},
snoowrap = snoowrapOp,
flowControlDefaults,
credentials = {},
subreddits: {
overrides = [],
names = [],
exclude = [],
wikiConfig = 'botconfig/contextbot',
@@ -966,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,

View File

@@ -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[]
}

View File

@@ -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;
});

View File

@@ -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 {

View File

@@ -95,6 +95,15 @@ export interface RegexCriteria {
* */
totalMatchThreshold?: string,
/**
* When `true` the Activity being checked MUST pass the `matchThreshold` before the Rule considers any history
*
* For use with `activityMatchThreshold`/`totalMatchThreshold` -- useful to conserve API calls
*
* @default false
* */
mustMatchCurrent?: boolean
window?: ActivityWindowType
}
@@ -140,6 +149,7 @@ export class RegexRule extends Rule {
matchThreshold = '> 0',
activityMatchThreshold = '> 0',
totalMatchThreshold = null,
mustMatchCurrent = false,
window,
} = criteria;
@@ -184,6 +194,8 @@ export class RegexRule extends Rule {
if (singleMatched) {
activitiesMatchedCount++;
}
const singleCriteriaPass = !mustMatchCurrent || (mustMatchCurrent && singleMatched);
if (activityMatchComparison !== undefined) {
activityThresholdMet = !activityMatchComparison.isPercent && comparisonTextOp(activitiesMatchedCount, activityMatchComparison.operator, activityMatchComparison.value);
}
@@ -192,7 +204,7 @@ export class RegexRule extends Rule {
}
let history: (Submission | Comment)[] = [];
if ((activityThresholdMet === false || totalThresholdMet === false) && window !== undefined) {
if ((activityThresholdMet === false || totalThresholdMet === false) && window !== undefined && singleCriteriaPass) {
// our checking activity didn't meet threshold requirements and criteria does define window
// leh go
@@ -263,7 +275,8 @@ export class RegexRule extends Rule {
matchThreshold,
activityMatchThreshold,
totalMatchThreshold,
window: humanWindow
window: humanWindow,
mustMatchCurrent,
},
matches,
matchCount,

View File

@@ -1,17 +1,28 @@
import {Rule, RuleJSONConfig, RuleOptions, RuleResult} from "./index";
import {Comment} from "snoowrap";
import {
activityWindowText, asSubmission,
comparisonTextOp, FAIL, getActivitySubredditName, isExternalUrlSubmission, isRedditMedia,
parseGenericValueComparison, parseSubredditName,
parseUsableLinkIdentifier as linkParser, PASS, subredditStateIsNameOnly, toStrongSubredditState
activityWindowText,
asSubmission,
comparisonTextOp,
FAIL,
getActivitySubredditName,
isExternalUrlSubmission,
isRedditMedia,
parseGenericValueComparison,
parseSubredditName,
parseUsableLinkIdentifier as linkParser,
PASS,
searchAndReplace,
stringSameness,
subredditStateIsNameOnly,
toStrongSubredditState
} from "../util";
import {
ActivityWindow,
ActivityWindowType,
ReferenceSubmission,
ReferenceSubmission, SearchAndReplaceRegExp,
StrongSubredditState,
SubredditState
SubredditState, TextMatchOptions, TextTransformOptions
} from "../Common/interfaces";
import Submission from "snoowrap/dist/objects/Submission";
import dayjs from "dayjs";
@@ -29,27 +40,6 @@ interface RepeatActivityReducer {
allSets: RepeatActivityData[]
}
const getActivityIdentifier = (activity: (Submission | Comment), length = 200) => {
let identifier: string;
if (asSubmission(activity)) {
if (activity.is_self) {
identifier = `${activity.title}${activity.selftext.slice(0, length)}`;
} else if(isRedditMedia(activity)) {
identifier = activity.title;
} else {
identifier = parseUsableLinkIdentifier(activity.url) as string;
}
} else {
identifier = activity.body.slice(0, length);
}
return identifier;
}
const fuzzyOptions = {
includeScore: true,
distance: 15
};
export class RepeatActivityRule extends Rule {
threshold: string;
window: ActivityWindowType;
@@ -62,6 +52,9 @@ export class RepeatActivityRule extends Rule {
activityFilterFunc: (x: Submission|Comment) => Promise<boolean> = async (x) => true;
keepRemoved: boolean;
minWordCount: number;
transformations: SearchAndReplaceRegExp[]
caseSensitive: boolean
matchScore: number
constructor(options: RepeatActivityOptions) {
super(options);
@@ -75,7 +68,13 @@ export class RepeatActivityRule extends Rule {
include = [],
exclude = [],
keepRemoved = false,
transformations = [],
caseSensitive = true,
matchScore = 85,
} = options;
this.matchScore = matchScore;
this.transformations = transformations;
this.caseSensitive = caseSensitive;
this.minWordCount = minWordCount;
this.keepRemoved = keepRemoved;
this.threshold = threshold;
@@ -136,6 +135,37 @@ export class RepeatActivityRule extends Rule {
}
}
getActivityIdentifier(activity: (Submission | Comment), length = 200, transform = true) {
let identifier: string;
if (asSubmission(activity)) {
if (activity.is_self) {
identifier = `${activity.title}${activity.selftext.slice(0, length)}`;
} else if(isRedditMedia(activity)) {
identifier = activity.title;
} else {
identifier = parseUsableLinkIdentifier(activity.url) as string;
}
} else {
identifier = activity.body.slice(0, length);
}
if(!transform) {
return identifier;
}
// apply any transforms
if (this.transformations.length > 0) {
identifier = searchAndReplace(identifier, this.transformations);
}
// perform after transformations so as not to mess up regex's depending on case
if(!this.caseSensitive) {
identifier = identifier.toLowerCase();
}
return identifier;
}
async process(item: Submission|Comment): Promise<[boolean, RuleResult]> {
let referenceUrl;
if(asSubmission(item) && this.useSubmissionAsReference) {
@@ -162,9 +192,10 @@ export class RepeatActivityRule extends Rule {
const acc = await accProm;
const {openSets = [], allSets = []} = acc;
let identifier = getActivityIdentifier(activity);
let identifier = this.getActivityIdentifier(activity);
const isUrl = isExternalUrlSubmission(activity);
let fu = new Fuse([identifier], !isUrl ? fuzzyOptions : {...fuzzyOptions, distance: 5});
//let fu = new Fuse([identifier], !isUrl ? fuzzyOptions : {...fuzzyOptions, distance: 5});
const validSub = await this.activityFilterFunc(activity);
let minMet = identifier.length >= this.minWordCount;
@@ -174,12 +205,15 @@ export class RepeatActivityRule extends Rule {
let currIdentifierInOpen = false;
const bufferedActivities = this.gapAllowance === undefined || this.gapAllowance === 0 ? [] : activities.slice(Math.max(0, index - this.gapAllowance), Math.max(0, index));
for (const o of openSets) {
const res = fu.search(o.identifier);
const match = res.length > 0;
if (match && validSub && minMet) {
const strMatchResults = stringSameness(o.identifier, identifier);
if (strMatchResults.highScoreWeighted >= this.matchScore && minMet) {
updatedOpenSets.push({...o, sets: [...o.sets, activity]});
currIdentifierInOpen = true;
} else if (bufferedActivities.some(x => fu.search(getActivityIdentifier(x)).length > 0) && validSub && minMet) {
} else if (bufferedActivities.some(x => {
let buffIdentifier = this.getActivityIdentifier(x);
const buffMatch = stringSameness(identifier, buffIdentifier);
return buffMatch.highScoreWeighted >= this.matchScore;
}) && validSub && minMet) {
updatedOpenSets.push(o);
} else if(!currIdentifierInOpen && !isUrl) {
updatedAllSets.push(o);
@@ -193,15 +227,18 @@ export class RepeatActivityRule extends Rule {
// could be that a spammer is using different URLs for each submission but similar submission titles so search by title as well
const sub = activity as Submission;
identifier = sub.title;
fu = new Fuse([identifier], !isUrl ? fuzzyOptions : {...fuzzyOptions, distance: 5});
//fu = new Fuse([identifier], !isUrl ? fuzzyOptions : {...fuzzyOptions, distance: 5});
minMet = identifier.length >= this.minWordCount;
for (const o of openSets) {
const res = fu.search(o.identifier);
const match = res.length > 0;
if (match && validSub && minMet) {
const strMatchResults = stringSameness(o.identifier, identifier);
if (strMatchResults.highScoreWeighted >= this.matchScore && minMet) {
updatedOpenSets.push({...o, sets: [...o.sets, activity]});
currIdentifierInOpen = true;
} else if (bufferedActivities.some(x => fu.search(getActivityIdentifier(x)).length > 0) && validSub && minMet && !updatedOpenSets.includes(o)) {
} else if (bufferedActivities.some(x => {
let buffIdentifier = this.getActivityIdentifier(x);
const buffMatch = stringSameness(identifier, buffIdentifier);
return buffMatch.highScoreWeighted >= this.matchScore;
}) && validSub && minMet && !updatedOpenSets.includes(o)) {
updatedOpenSets.push(o);
} else if(!updatedAllSets.includes(o)) {
updatedAllSets.push(o);
@@ -232,7 +269,7 @@ export class RepeatActivityRule extends Rule {
let applicableGroupedActivities = identifierGroupedActivities;
if (this.useSubmissionAsReference) {
applicableGroupedActivities = new Map();
let identifier = getActivityIdentifier(item);
let identifier = this.getActivityIdentifier(item);
let referenceSubmissions = identifierGroupedActivities.get(identifier);
if(referenceSubmissions === undefined && isExternalUrlSubmission(item)) {
// if external url sub then try by title
@@ -240,7 +277,7 @@ export class RepeatActivityRule extends Rule {
referenceSubmissions = identifierGroupedActivities.get(identifier);
if(referenceSubmissions === undefined) {
// didn't get by title so go back to url since that's the default
identifier = getActivityIdentifier(item);
identifier = this.getActivityIdentifier(item);
}
}
@@ -265,7 +302,7 @@ export class RepeatActivityRule extends Rule {
};
for (let set of value) {
const test = comparisonTextOp(set.length, operator, thresholdValue);
const md = set.map((x: (Comment | Submission)) => `[${asSubmission(x) ? x.title : getActivityIdentifier(x, 50)}](https://reddit.com${x.permalink}) in ${x.subreddit_name_prefixed} on ${dayjs(x.created_utc * 1000).utc().format()}`);
const md = set.map((x: (Comment | Submission)) => `[${asSubmission(x) ? x.title : this.getActivityIdentifier(x, 50)}](https://reddit.com${x.permalink}) in ${x.subreddit_name_prefixed} on ${dayjs(x.created_utc * 1000).utc().format()}`);
summaryData.sets.push(set);
summaryData.largestTrigger = Math.max(summaryData.largestTrigger, set.length);
@@ -325,7 +362,7 @@ interface SummaryData {
triggeringSetsMarkdown: string[]
}
interface RepeatActivityConfig extends ActivityWindow, ReferenceSubmission {
interface RepeatActivityConfig extends ActivityWindow, ReferenceSubmission, TextMatchOptions {
/**
* The number of repeat submissions that will trigger the rule
* @default ">= 5"
@@ -383,18 +420,9 @@ interface RepeatActivityConfig extends ActivityWindow, ReferenceSubmission {
keepRemoved?: boolean
/**
* For activities that are text-based this is the minimum number of words required for the activity to be considered for a repeat
*
* EX if `minimumWordCount=5` and a comment is `what about you` then it is ignored because `3 is less than 5`
*
* **For self-text submissions** -- title + body text
*
* **For comments* -- body text
*
* @default 1
* @example [1]
* A set of search-and-replace operations to perform on text values before performing a match. Transformations are performed in the order they are defined.
* */
minWordCount?: number,
transformations?: SearchAndReplaceRegExp[]
}
export interface RepeatActivityOptions extends RepeatActivityConfig, RuleOptions {

Some files were not shown because too many files have changed in this diff Show More