Compare commits

...

72 Commits

Author SHA1 Message Date
FoxxMD
d02d70ded3 Merge branch 'edge' 2022-10-17 15:33:15 -04:00
FoxxMD
acbb9a8626 feat(usernote): Improve usernote filtering functionality
Same as modActions...

* Add `referencesCurrentActivity` boolean to filter by notes associated with current activity
* Add `note` string property to filter by note content (string or regular expression)
* Replace `allowDuplicates` with `existingNoteCheck` on UserNoteAction to allow for more granular note control on action
2022-10-17 15:32:47 -04:00
FoxxMD
122d5fb2af docs: Fix malformed URLs
Fixes #114
2022-10-17 14:16:34 -04:00
FoxxMD
98a8568eb6 fix: Add missing else condition 2022-10-13 16:17:43 -04:00
FoxxMD
457f947603 fix: Further improvements for influxdb logging
* Suppress write failure warnings to reduce noise in log
  * Can be toggled using debug flag in config
2022-10-13 12:34:11 -04:00
FoxxMD
7fb69ae67a fix: Attempt to decrease frequency of influxdb timeout errors
* Use keep-alive http agent to reuse open connections
* Decrease max batch size and flush interval to payload sent is smaller
* Add debug logging for fail/success/retry events on flush
2022-10-13 11:39:45 -04:00
FoxxMD
2241d40e49 fix: Fix custom footer never loading
Custom footer needs to be loaded AFTER resources are set
2022-10-13 08:58:12 -04:00
FoxxMD
a3ca3f17ec refactor(recent): Log image parsing error with cause 2022-10-12 14:39:21 -04:00
FoxxMD
f527a17fa2 feat(modnote): Implement existing note check before adding note to replace allowDuplicates
* Use "modActions" authorIs filtering to check note prior to adding note using "existingNoteCheck" property
  * Refactors concept of "allowDuplicates" to allow any arbitrary modActions test to be used
* Provide convenience ModLogCriteria generation for "existingNoteCheck" based on boolean (emulates allowDuplicates functionality)
2022-10-12 13:00:47 -04:00
FoxxMD
e98364eae9 feat(filter): Improve modAction filtering functionality
* Implement filtering activityType by "false" in order to return actions/notes not added to a specific activity
* Implement "referencesCurrentActivity" property to allow filtering by actions/notes that are associated with the activity being processed
* Implement using "count" for "current" search to enable criteria condition based on presence or non-presence of current action/note passing
2022-10-12 12:58:49 -04:00
FoxxMD
8b125d7433 feat(ui): Improve visibility and resilience for live log stream
* Tie loading indicator to live stream status and display error if one occurs
  * Add manual restart action to end of error
* Restart stream automatically if reader ends, up to 3 retries
2022-10-11 12:19:55 -04:00
FoxxMD
6ee060c5ce fix(logs): Remove listeners from log stream event emitter before end of response to prevent write-after-end errors 2022-10-11 12:18:00 -04:00
FoxxMD
9b12d0b2b3 feat(bot): Improve log wording for manager loading phase 2022-10-11 10:51:00 -04:00
FoxxMD
b174c7928a feat(ui): Add favicon files 2022-10-11 10:49:57 -04:00
FoxxMD
74dfe9258a fix(filter): Fix mod action note filtering assignment 2022-10-10 15:00:39 -04:00
FoxxMD
1cf8855a24 feat(testing): Implement initial author filter tests 2022-10-10 12:06:08 -04:00
FoxxMD
adc69894fc fix(filter): Fix detecting empty filter when using 'replace' filter default behavior 2022-10-10 11:03:57 -04:00
FoxxMD
3435c683c8 feat(filter): Add author flair item criteria 2022-10-05 11:24:44 -04:00
FoxxMD
80f83bf84b Merge branch 'edge' 2022-10-05 08:57:04 -04:00
FoxxMD
f0032cd433 Bump version 2022-10-05 08:56:53 -04:00
FoxxMD
7933f77764 Merge branch 'edge' 2022-10-05 08:55:39 -04:00
FoxxMD
ade0b7948e Add patreon funding link 2022-10-04 09:40:03 -04:00
FoxxMD
542aa26c62 feat(history): Implement ratio comparison #112 2022-10-03 13:25:05 -04:00
FoxxMD
3bcc3d78e8 Merge branch 'edge' 2022-09-28 09:28:38 -04:00
FoxxMD
296f1c8dee Merge branch 'edge' 2022-09-14 15:30:27 -04:00
FoxxMD
e32ac60db5 Merge branch 'edge' 2022-09-14 15:29:13 -04:00
FoxxMD
859680dca8 Merge branch 'edge' 2022-09-01 09:03:27 -04:00
FoxxMD
ffa1e423b2 Merge branch 'edge' 2022-08-23 09:49:23 -04:00
FoxxMD
09cb08492c Merge branch 'edge' 2022-08-23 09:47:59 -04:00
FoxxMD
d9ab81ab8c Merge branch 'edge' 2022-07-27 09:19:30 -04:00
FoxxMD
98691bd19c Merge branch 'edge' 2022-07-15 09:27:22 -04:00
FoxxMD
8123c34463 Merge branch 'edge' 2022-06-21 16:13:54 -04:00
FoxxMD
3292d011fa Merge branch 'edge' 2022-06-21 10:03:14 -04:00
FoxxMD
661a0ae440 Merge branch 'edge' 2022-05-26 09:59:32 -04:00
FoxxMD
05f477b67d Merge branch 'edge' 2022-05-12 12:27:51 -04:00
Matt Foxx
1317a5916c Merge pull request #86 from wchristian/example_fix
trying to use names key in authorfilter causes config parse failure
2022-04-05 16:55:56 -04:00
Christian Walde
e9135ec1ef trying to use names key in authorfilter causes config parse failure 2022-04-05 13:49:41 +02:00
FoxxMD
e58a0f8f21 Merge branch 'edge' 2022-03-14 12:39:05 -04:00
FoxxMD
f7cebc013b Merge branch 'edge' 2022-03-08 09:48:06 -05:00
FoxxMD
ae8e11feb4 Merge branch 'edge' 2022-02-22 11:11:46 -05:00
FoxxMD
e07b8cc291 Merge branch 'edge' 2022-02-18 11:58:28 -05:00
FoxxMD
fc51928054 Merge branch 'edge' 2022-02-02 16:59:56 -05:00
FoxxMD
e2590e50f8 Merge branch 'edge' 2022-01-28 17:27:51 -05:00
FoxxMD
aaed0d3419 Merge branch 'edge' 2022-01-21 10:46:11 -05:00
FoxxMD
bc7eff8928 Merge branch 'edge' 2022-01-14 15:27:09 -05:00
FoxxMD
d6954533a0 Merge branch 'edge' 2022-01-10 12:32:14 -05:00
FoxxMD
ba53233640 Merge branch 'edge' 2022-01-07 09:31:14 -05:00
FoxxMD
1ac7ad4724 Merge branch 'edge' 2022-01-03 16:35:01 -05:00
FoxxMD
2a282a0d6f Merge branch 'edge' 2021-12-21 09:35:21 -05:00
FoxxMD
fd5a92758d Merge branch 'edge' 2021-11-28 19:43:20 -05:00
FoxxMD
39daa11f2d Merge branch 'edge' 2021-11-15 12:53:28 -05:00
FoxxMD
dac6541e28 Merge branch 'edge' 2021-11-01 16:12:43 -04:00
FoxxMD
97906281e6 Merge branch 'edge' 2021-11-01 14:55:10 -04:00
FoxxMD
487f13f704 Merge branch 'edge' 2021-10-12 11:56:51 -04:00
FoxxMD
631e21452c Merge branch 'edge' 2021-09-28 16:36:13 -04:00
FoxxMD
4f3685a1f5 Merge branch 'edge' 2021-09-21 15:18:38 -04:00
FoxxMD
d2d945db2c Merge branch 'edge' 2021-09-21 15:08:28 -04:00
FoxxMD
910f7f79ef Merge branch 'edge' 2021-09-20 10:54:32 -04:00
FoxxMD
a11b667d5e Merge branch 'edge' 2021-09-13 16:16:55 -04:00
FoxxMD
885e3fa765 Merge branch 'edge' 2021-08-26 16:04:01 -04:00
FoxxMD
465c3c9acf Merge branch 'edge' 2021-08-20 15:02:24 -04:00
FoxxMD
161251a943 Merge branch 'edge' 2021-08-05 14:40:06 -04:00
FoxxMD
ce4cb96d9a Merge branch 'edge' 2021-08-03 23:39:14 -04:00
FoxxMD
c317f95953 Merge branch 'edge' 2021-08-03 22:43:02 -04:00
FoxxMD
d0e0515990 Merge branch 'edge' 2021-08-02 15:44:57 -04:00
FoxxMD
cdddd8de48 Merge branch 'edge' 2021-07-30 18:17:38 -04:00
FoxxMD
f598215d88 Merge branch 'edge' 2021-07-30 14:46:51 -04:00
FoxxMD
0c7218571c Merge branch 'edge' 2021-07-29 13:25:16 -04:00
FoxxMD
acc7c49e0e Merge branch 'edge' 2021-07-29 11:27:42 -04:00
FoxxMD
01839512d5 Merge branch 'edge' 2021-07-29 11:14:33 -04:00
FoxxMD
4680640b0c Merge branch 'develop' 2021-07-28 16:58:36 -04:00
Matt Foxx
b813ebdd96 Create dockerhub.yml 2021-07-28 11:27:04 -04:00
52 changed files with 3184 additions and 498 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -754,7 +754,7 @@ actions:
- kind: usernote
type: spamwarn
content: 'Usernote message'
allowDuplicate: boolean # if false then the usernote will not be added if the same note appears for this activity
existingNoteCheck: boolean # if true (default) then the usernote will not be added if the same note appears for this activity
```
### Mod Note
@@ -779,6 +779,7 @@ actions:
type: SPAM_WATCH
content: 'a note only mods can see message' # optional
referenceActivity: boolean # if true the Note will be linked to the Activity being processed
existingNoteCheck: boolean # if true (default) then the note will not be added if the same note appears for this activity
```
# Filters

View File

@@ -10,5 +10,5 @@ Consult the [schema](https://json-schema.app/view/%23/%23%2Fdefinitions%2FCheckJ
### Examples
* Self Promotion as percentage of all Activities [YAML](/docs/subreddit/componentscomponents/attribution/redditSelfPromoAll.yaml) | [JSON](/docs/subreddit/componentscomponents/attribution/redditSelfPromoAll.json5) - Check if Author is submitting much more than they comment.
* Self Promotion as percentage of all Activities [YAML](/docs/subreddit/components/attribution/redditSelfPromoAll.yaml) | [JSON](/docs/subreddit/components/attribution/redditSelfPromoAll.json5) - Check if Author is submitting much more than they comment.
* Self Promotion as percentage of Submissions [YAML](/docs/subreddit/components/attribution/redditSelfPromoSubmissionsOnly.yaml) | [JSON](/docs/examplesm/attribution/redditSelfPromoSubmissionsOnly.json5) - Check if any of Author's aggregated submission origins are >10% of their submissions

View File

@@ -9,7 +9,7 @@ The **Author** rule triggers if any [AuthorCriteria](https://json-schema.app/vie
* author's subreddit flair text
* author's subreddit flair css
* author's subreddit mod status
* [Toolbox User Notes](/docs/subreddit/componentscomponents/userNotes)
* [Toolbox User Notes](/docs/subreddit/components/userNotes)
The Author **Rule** is best used in conjunction with other Rules to short-circuit a Check based on who the Author is. It is easier to use a Rule to do this then to write **author filters** for every Rule (and makes Rules more re-useable).
@@ -18,10 +18,10 @@ Consult the [schema](https://json-schema.app/view/%23%2Fdefinitions%2FAuthorRule
### Examples
* Basic examples
* Flair new user Submission [YAML](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.json5) - If the Author does not have the `vet` flair then flair the Submission with `New User`
* Flair vetted user Submission [YAML](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.json5) - If the Author does have the `vet` flair then flair the Submission with `Vetted`
* Flair new user Submission [YAML](/docs/subreddit/components/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/components/author/flairNewUserSubmission.json5) - If the Author does not have the `vet` flair then flair the Submission with `New User`
* Flair vetted user Submission [YAML](/docs/subreddit/components/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/components/author/flairNewUserSubmission.json5) - If the Author does have the `vet` flair then flair the Submission with `Vetted`
* Used with other Rules
* Ignore vetted user [YAML](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/componentscomponents/author/flairNewUserSubmission.json5) - Short-circuit the Check if the Author has the `vet` flair
* Ignore vetted user [YAML](/docs/subreddit/components/author/flairNewUserSubmission.yaml) | [JSON](/docs/subreddit/components/author/flairNewUserSubmission.json5) - Short-circuit the Check if the Author has the `vet` flair
## Filter
@@ -35,7 +35,7 @@ All **Rules** and **Checks** have an optional `authorIs` property that takes an
### Examples
* Skip recent activity check based on author [YAML](/docs/subreddit/componentscomponents/author/authorFilter.yaml) | [JSON](/docs/subreddit/componentscomponents/author/authorFilter.json5) - Skip a Recent Activity check for a set of subreddits if the Author of the Submission has any set of flairs.
* Skip recent activity check based on author [YAML](/docs/subreddit/components/author/authorFilter.yaml) | [JSON](/docs/subreddit/components/author/authorFilter.json5) - Skip a Recent Activity check for a set of subreddits if the Author of the Submission has any set of flairs.
## Flair users and submissions
@@ -45,4 +45,4 @@ Consult [User Flair schema](https://json-schema.app/view/%23%2Fdefinitions%2FUse
### Examples
* OnlyFans submissions [YAML](/docs/subreddit/componentscomponents/author/onlyfansFlair.yaml) | [JSON](/docs/subreddit/componentscomponents/author/onlyfansFlair.json5) - Check whether submitter has typical OF keywords in their profile and flair both author + submission accordingly.
* OnlyFans submissions [YAML](/docs/subreddit/components/author/onlyfansFlair.yaml) | [JSON](/docs/subreddit/components/author/onlyfansFlair.json5) - Check whether submitter has typical OF keywords in their profile and flair both author + submission accordingly.

View File

@@ -40,7 +40,7 @@
// 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": ["user1","user2"]
},
{
// for this to pass the Author of the Submission must not have the flair "Decent Memer"

View File

@@ -30,7 +30,7 @@ runs:
# 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:
name:
- user1
- user2
# for this to pass the Author of the Submission must not have the flair "Decent Memer"

View File

@@ -5,10 +5,46 @@ The **History** rule can check an Author's submission/comment statistics over a
* Submission total or percentage of All Activity
* Comment total or percentage of all Activity
* Comments made as OP (commented in their own Submission) total or percentage of all Comments
* Ratio of activities against another window of activities
Consult the [schema](https://json-schema.app/view/%23%2Fdefinitions%2FHistoryJSONConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fcontext-mod%2Fmaster%2Fsrc%2FSchema%2FApp.json) for a complete reference of the rule's properties.
## Ratio
Use the `ratio` property in Criteria to test the [number of activities](/docs/subreddit/activitiesWindow.md) found in the parent criteria against the number of activities from _another_ [activity window](/docs/subreddit/activitiesWindow.md) defined in the ratio.
Example:
```yaml
- kind: history
criteria:
# "parent" criteria, returns all activities, in the last 100 from user's history, that occurred in r/mealtimevideos
- window:
count: 100
filterOn:
post:
subreddits:
include:
- mealtimevideos
ratio:
# "ratio" criteria, returns all activities, in the last 100 from user's history, that occurred in r/redditdev
window:
count: 100
filterOn:
post:
subreddits:
include:
- redditdev
# test (number of parent criteria activities) / (number of ratio critieria activities)
threshold: '> 1.2'
```
`threshold` may be a number or percentage `(number * 100)`
* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities
* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities
### Examples
* Low Comment Engagement [YAML](/docs/subreddit/componentscomponents/history/lowEngagement.yaml) | [JSON](/docs/subreddit/componentscomponents/history/lowEngagement.json5) - Check if Author is submitting much more than they comment.
* OP Comment Engagement [YAML](/docs/subreddit/componentscomponents/history/opOnlyEngagement.yaml) | [JSON](/docs/subreddit/componentscomponents/history/opOnlyEngagement.json5) - Check if Author is mostly engaging only in their own content
* Low Comment Engagement [YAML](/docs/subreddit/components/history/lowEngagement.yaml) | [JSON](/docs/subreddit/components/history/lowEngagement.json5) - Check if Author is submitting much more than they comment.
* OP Comment Engagement [YAML](/docs/subreddit/components/history/opOnlyEngagement.yaml) | [JSON](/docs/subreddit/components/history/opOnlyEngagement.json5) - Check if Author is mostly engaging only in their own content

View File

@@ -27,5 +27,5 @@ Consult the [schema](https://json-schema.app/view/%23%2Fdefinitions%2FRecentActi
### Examples
* Free Karma Subreddits [YAML](/docs/subreddit/componentscomponents/recentActivity/freeKarma.yaml) | [JSON](/docs/subreddit/componentscomponents/recentActivity/freeKarma.json5) - Check if the Author has recently posted in any "free karma" subreddits
* Submission in Free Karma Subreddits [YAML](/docs/subreddit/componentscomponents/recentActivity/freeKarmaOnSubmission.yaml) | [JSON](/docs/subreddit/componentscomponents/recentActivity/freeKarmaOnSubmission.json5) - Check if the Author has posted the Submission this check is running on in any "free karma" subreddits recently
* Free Karma Subreddits [YAML](/docs/subreddit/components/recentActivity/freeKarma.yaml) | [JSON](/docs/subreddit/components/recentActivity/freeKarma.json5) - Check if the Author has recently posted in any "free karma" subreddits
* Submission in Free Karma Subreddits [YAML](/docs/subreddit/components/recentActivity/freeKarmaOnSubmission.yaml) | [JSON](/docs/subreddit/components/recentActivity/freeKarmaOnSubmission.json5) - Check if the Author has posted the Submission this check is running on in any "free karma" subreddits recently

View File

@@ -11,12 +11,12 @@ Which can then be used in conjunction with a [`window`](https://github.com/FoxxM
### Examples
* Trigger if regex matches against the current activity - [YAML](/docs/subreddit/componentscomponents/regex/matchAnyCurrentActivity.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchAnyCurrentActivity.json5)
* Trigger if regex matches 5 times against the current activity - [YAML](/docs/subreddit/componentscomponents/regex/matchThresholdCurrentActivity.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchThresholdCurrentActivity.json5)
* Trigger if regex matches against any part of a Submission - [YAML](/docs/subreddit/componentscomponents/regex/matchSubmissionParts.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchSubmissionParts.json5)
* Trigger if regex matches any of Author's last 10 activities - [YAML](/docs/subreddit/componentscomponents/regex/matchHistoryActivity.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchHistoryActivity.json5)
* Trigger if regex matches at least 3 of Author's last 10 activities - [YAML](/docs/subreddit/componentscomponents/regex/matchActivityThresholdHistory.json5) | [JSON](/docs/subreddit/componentscomponents/regex/matchActivityThresholdHistory.json5)
* Trigger if there are 5 regex matches in the Author's last 10 activities - [YAML](/docs/subreddit/componentscomponents/regex/matchTotalHistoryActivity.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchTotalHistoryActivity.json5)
* Trigger if there are 5 regex matches in the Author's last 10 comments - [YAML](/docs/subreddit/componentscomponents/regex/matchSubsetHistoryActivity.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/matchSubsetHistoryActivity.json5)
* Remove comments that are spamming discord links - [YAML](/docs/subreddit/componentscomponents/regex/removeDiscordSpam.yaml) | [JSON](/docs/subreddit/componentscomponents/regex/removeDiscordSpam.json5)
* Trigger if regex matches against the current activity - [YAML](/docs/subreddit/components/regex/matchAnyCurrentActivity.yaml) | [JSON](/docs/subreddit/components/regex/matchAnyCurrentActivity.json5)
* Trigger if regex matches 5 times against the current activity - [YAML](/docs/subreddit/components/regex/matchThresholdCurrentActivity.yaml) | [JSON](/docs/subreddit/components/regex/matchThresholdCurrentActivity.json5)
* Trigger if regex matches against any part of a Submission - [YAML](/docs/subreddit/components/regex/matchSubmissionParts.yaml) | [JSON](/docs/subreddit/components/regex/matchSubmissionParts.json5)
* Trigger if regex matches any of Author's last 10 activities - [YAML](/docs/subreddit/components/regex/matchHistoryActivity.yaml) | [JSON](/docs/subreddit/components/regex/matchHistoryActivity.json5)
* Trigger if regex matches at least 3 of Author's last 10 activities - [YAML](/docs/subreddit/components/regex/matchActivityThresholdHistory.json5) | [JSON](/docs/subreddit/components/regex/matchActivityThresholdHistory.json5)
* Trigger if there are 5 regex matches in the Author's last 10 activities - [YAML](/docs/subreddit/components/regex/matchTotalHistoryActivity.yaml) | [JSON](/docs/subreddit/components/regex/matchTotalHistoryActivity.json5)
* Trigger if there are 5 regex matches in the Author's last 10 comments - [YAML](/docs/subreddit/components/regex/matchSubsetHistoryActivity.yaml) | [JSON](/docs/subreddit/components/regex/matchSubsetHistoryActivity.json5)
* Remove comments that are spamming discord links - [YAML](/docs/subreddit/components/regex/removeDiscordSpam.yaml) | [JSON](/docs/subreddit/components/regex/removeDiscordSpam.json5)
* Differs from just using automod because this config can allow one-off/organic links from users who DO NOT spam discord links but will still remove the comment if the user is spamming them

View File

@@ -47,5 +47,5 @@ With only `gapAllowance: 2` this rule **would trigger** because the the 1 and 2
## Examples
* Crosspost Spamming [JSON](/docs/subreddit/componentscomponents/repeatActivity/crosspostSpamming.json5) | [YAML](/docs/subreddit/componentscomponents/repeatActivity/crosspostSpamming.yaml) - Check if an Author is spamming their Submissions across multiple subreddits
* Burst-posting [JSON](/docs/subreddit/componentscomponents/repeatActivity/burstPosting.json5) | [YAML](/docs/subreddit/componentscomponents/repeatActivity/burstPosting.yaml) - Check if Author is crossposting their Submissions in short bursts
* Crosspost Spamming [JSON](/docs/subreddit/components/repeatActivity/crosspostSpamming.json5) | [YAML](/docs/subreddit/components/repeatActivity/crosspostSpamming.yaml) - Check if an Author is spamming their Submissions across multiple subreddits
* Burst-posting [JSON](/docs/subreddit/components/repeatActivity/burstPosting.json5) | [YAML](/docs/subreddit/components/repeatActivity/burstPosting.yaml) - Check if Author is crossposting their Submissions in short bursts

View File

@@ -17,25 +17,25 @@ All actions for these configurations are non-destructive in that:
### Remove submissions from users who have used 'freekarma' subs to bypass karma checks
[YAML](/docs/subreddit/componentscomponents/subredditReady/freekarma.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/freekarma.json5)
[YAML](/docs/subreddit/components/subredditReady/freekarma.yaml) | [JSON](/docs/subreddit/components/subredditReady/freekarma.json5)
If the user has any activity (comment/submission) in known freekarma subreddits in the past (50 activities or 6 months) then remove the submission.
### Remove submissions from users who have crossposted the same submission 4 or more times
[YAML](/docs/subreddit/componentscomponents/subredditReady/crosspostSpam.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/crosspostSpam.yaml)
[YAML](/docs/subreddit/components/subredditReady/crosspostSpam.yaml) | [JSON](/docs/subreddit/components/subredditReady/crosspostSpam.yaml)
If the user has crossposted the same submission in the past (50 activities or 6 months) 4 or more times in a row then remove the submission.
### Remove submissions from users who have crossposted or used 'freekarma' subs
[YAML](/docs/subreddit/componentscomponents/subredditReady/freeKarmaOrCrosspostSpam.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/freeKarmaOrCrosspostSpam.json5)
[YAML](/docs/subreddit/componentsc/subredditReady/freeKarmaOrCrosspostSpam.yaml) | [JSON](/docs/subreddit/components/subredditReady/freeKarmaOrCrosspostSpam.json5)
Will remove submission if either of the above two behaviors is detected
### Remove link submissions where the user's history is comprised of 10% or more of the same link
[YAML](/docs/subreddit/componentscomponents/subredditReady/selfPromo.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/selfPromo.json5)
[YAML](/docs/subreddit/components/subredditReady/selfPromo.yaml) | [JSON](/docs/subreddit/components/subredditReady/selfPromo.json5)
If the link origin (youtube author, twitter author, etc. or regular domain for non-media links)
@@ -48,13 +48,13 @@ then remove the submission
### Remove comment if the user has posted the same comment 4 or more times in a row
[YAML](/docs/subreddit/componentscomponents/subredditReady/commentSpam.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/commentSpam.json5)
[YAML](/docs/subreddit/components/subredditReady/commentSpam.yaml) | [JSON](/docs/subreddit/components/subredditReady/commentSpam.json5)
If the user made the same comment (with some fuzzy matching) 4 or more times in a row in the past (50 activities or 6 months) then remove the comment.
### Remove comment if it is discord invite link spam
[YAML](/docs/subreddit/componentscomponents/subredditReady/discordSpam.yaml) | [JSON](/docs/subreddit/componentscomponents/subredditReady/discordSpam.json5)
[YAML](/docs/subreddit/components/subredditReady/discordSpam.yaml) | [JSON](/docs/subreddit/components/subredditReady/discordSpam.json5)
This rule goes a step further than automod can by being more discretionary about how it handles this type of spam.

View File

@@ -24,7 +24,7 @@ Consult the [schema](https://json-schema.app/view/%23%2Fdefinitions%2FUserNoteCr
### Examples
* Do not tag user with Good User note [JSON](/docs/subreddit/componentscomponents/userNotes/usernoteFilter.json5) | [YAML](/docs/subreddit/componentscomponents/userNotes/usernoteFilter.yaml)
* Do not tag user with Good User note [JSON](/docs/subreddit/components/userNotes/usernoteFilter.json5) | [YAML](/docs/subreddit/components/userNotes/usernoteFilter.yaml)
## Action
@@ -33,4 +33,4 @@ A User Note can also be added to the Author of a Submission or Comment with the
### Examples
* Add note on user doing self promotion [JSON](/docs/subreddit/componentscomponents/userNotes/usernoteSP.json5) | [YAML](/docs/subreddit/componentscomponents/userNotes/usernoteSP.yaml)
* Add note on user doing self promotion [JSON](/docs/subreddit/components/userNotes/usernoteSP.json5) | [YAML](/docs/subreddit/components/userNotes/usernoteSP.yaml)

28
package-lock.json generated
View File

@@ -13,8 +13,8 @@
"@awaitjs/express": "^0.8.0",
"@datasert/cronjs-matcher": "^1.2.0",
"@googleapis/youtube": "^2.0.0",
"@influxdata/influxdb-client": "^1.27.0",
"@influxdata/influxdb-client-apis": "^1.27.0",
"@influxdata/influxdb-client": "^1.31.0",
"@influxdata/influxdb-client-apis": "^1.31.0",
"@nlpjs/core": "^4.23.4",
"@nlpjs/lang-de": "^4.23.4",
"@nlpjs/lang-en": "^4.23.4",
@@ -683,14 +683,14 @@
}
},
"node_modules/@influxdata/influxdb-client": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.27.0.tgz",
"integrity": "sha512-hOBi+ApIurDd8jFWo+eYjMWWsDRp3wih/U/NOVRoHaTOE8ihSQthi9wfMD4YeVqt4pCN6ygIwo7lEKFXwNuwcA=="
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.31.0.tgz",
"integrity": "sha512-8DVT3ZB/VeCK5Nn+BxhgMrAMSTseQAEgV20AK+ZMO5Fcup9XWsA9L2zE+3eBFl0Y+lF3UeKiASkiKMQvws35GA=="
},
"node_modules/@influxdata/influxdb-client-apis": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.27.0.tgz",
"integrity": "sha512-a4gd7CwNRXSsSVt9tm8GzGxuPXngEmQucMdoTZ0YYeWSbKUXz3B/3u9/EqMGEbtq5MdbbB2OKA611hu205UiNg==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.31.0.tgz",
"integrity": "sha512-6ALGNLxtfffhICobOdj13Z6vj6gdQVOzVXPoPNd+w7V60zrbGhTqzXHV1KMZ/lzOb6YkRTRODbxz4W/b/7N5hg==",
"peerDependencies": {
"@influxdata/influxdb-client": "*"
}
@@ -10858,14 +10858,14 @@
}
},
"@influxdata/influxdb-client": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.27.0.tgz",
"integrity": "sha512-hOBi+ApIurDd8jFWo+eYjMWWsDRp3wih/U/NOVRoHaTOE8ihSQthi9wfMD4YeVqt4pCN6ygIwo7lEKFXwNuwcA=="
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.31.0.tgz",
"integrity": "sha512-8DVT3ZB/VeCK5Nn+BxhgMrAMSTseQAEgV20AK+ZMO5Fcup9XWsA9L2zE+3eBFl0Y+lF3UeKiASkiKMQvws35GA=="
},
"@influxdata/influxdb-client-apis": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.27.0.tgz",
"integrity": "sha512-a4gd7CwNRXSsSVt9tm8GzGxuPXngEmQucMdoTZ0YYeWSbKUXz3B/3u9/EqMGEbtq5MdbbB2OKA611hu205UiNg==",
"version": "1.31.0",
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.31.0.tgz",
"integrity": "sha512-6ALGNLxtfffhICobOdj13Z6vj6gdQVOzVXPoPNd+w7V60zrbGhTqzXHV1KMZ/lzOb6YkRTRODbxz4W/b/7N5hg==",
"requires": {}
},
"@istanbuljs/load-nyc-config": {

View File

@@ -33,8 +33,8 @@
"@awaitjs/express": "^0.8.0",
"@datasert/cronjs-matcher": "^1.2.0",
"@googleapis/youtube": "^2.0.0",
"@influxdata/influxdb-client": "^1.27.0",
"@influxdata/influxdb-client-apis": "^1.27.0",
"@influxdata/influxdb-client": "^1.31.0",
"@influxdata/influxdb-client-apis": "^1.31.0",
"@nlpjs/core": "^4.23.4",
"@nlpjs/lang-de": "^4.23.4",
"@nlpjs/lang-en": "^4.23.4",

View File

@@ -1,30 +1,32 @@
import {ActionJson, ActionConfig, ActionOptions} from "./index";
import Action from "./index";
import {Comment} from "snoowrap";
import {renderContent} from "../Utils/SnoowrapUtils";
import Submission from "snoowrap/dist/objects/Submission";
import {ActionProcessResult, RichContent} from "../Common/interfaces";
import {toModNoteLabel} from "../util";
import {buildFilterCriteriaSummary, normalizeModActionCriteria, toModNoteLabel} from "../util";
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
import {runCheckOptions} from "../Subreddit/Manager";
import {ActionTypes, ModUserNoteLabel} from "../Common/Infrastructure/Atomic";
import {ModNote} from "../Subreddit/ModNotes/ModNote";
import {
ActionTypes,
ModUserNoteLabel,
} from "../Common/Infrastructure/Atomic";
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
import {ModNoteCriteria} from "../Common/Infrastructure/Filters/FilterCriteria";
export class ModNoteAction extends Action {
content: string;
type?: string;
allowDuplicate: boolean;
existingNoteCheck?: ModNoteCriteria
referenceActivity: boolean
constructor(options: ModNoteActionOptions) {
super(options);
const {type, content = '', allowDuplicate = false, referenceActivity = true} = options;
const {type, content = '', existingNoteCheck = true, referenceActivity = true} = options;
this.type = type;
this.content = content;
this.allowDuplicate = allowDuplicate;
this.referenceActivity = referenceActivity;
this.existingNoteCheck = typeof existingNoteCheck === 'boolean' ? this.generateModLogCriteriaFromDuplicateConvenience(existingNoteCheck) : normalizeModActionCriteria(existingNoteCheck);
}
getKind(): ActionTypes {
@@ -35,7 +37,7 @@ export class ModNoteAction extends Action {
return {
content: this.content,
type: this.type,
allowDuplicate: this.allowDuplicate,
existingNoteCheck: this.existingNoteCheck,
referenceActivity: this.referenceActivity,
}
}
@@ -48,27 +50,30 @@ export class ModNoteAction extends Action {
const renderedContent = await this.renderContent(this.content, item, ruleResults, actionResults);
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
// TODO see what changes are made for bulk fetch of notes before implementing this
// https://www.reddit.com/r/redditdev/comments/t8w861/new_mod_notes_api/
// if (!this.allowDuplicate) {
// const notes = await this.resources.userNotes.getUserNotes(item.author);
// 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,
// success: false,
// result: `Will not add note because one already exists for this Activity (${existingNote.time.local().format()}) and allowDuplicate=false`
// };
// }
// }
let noteCheckPassed: boolean = true;
let noteCheckResult: undefined | string;
if(this.existingNoteCheck === undefined) {
// nothing to do!
noteCheckResult = 'existingNoteCheck=false so no existing note checks were performed.';
} else {
const noteCheckCriteriaResult = await this.resources.isAuthor(item, {
modActions: [this.existingNoteCheck]
});
noteCheckPassed = noteCheckCriteriaResult.passed;
const {details} = buildFilterCriteriaSummary(noteCheckCriteriaResult);
noteCheckResult = `${noteCheckPassed ? 'Existing note check condition succeeded' : 'Will not add note because existing note check condition failed'} -- ${details.join(' ')}`;
}
this.logger.info(noteCheckResult);
if (!noteCheckPassed) {
return {
dryRun,
success: false,
result: noteCheckResult
};
}
if (!dryRun) {
await this.resources.addModNote({
label: modLabel,
@@ -84,15 +89,36 @@ export class ModNoteAction extends Action {
result: `${modLabel !== undefined ? `(${modLabel})` : ''} ${renderedContent}`
}
}
generateModLogCriteriaFromDuplicateConvenience(val: boolean): ModNoteCriteria | undefined {
if(val) {
return {
noteType: this.type !== undefined ? [toModNoteLabel(this.type)] : undefined,
note: this.content !== '' ? [this.content] : undefined,
referencesCurrentActivity: this.referenceActivity ? true : undefined,
search: 'current',
count: '< 1'
}
}
return undefined;
}
}
export interface ModNoteActionConfig extends ActionConfig, RichContent {
/**
* Add Note even if a Note already exists for this Activity
* @examples [false]
* @default false
* Check if there is an existing Note matching some criteria before adding the Note.
*
* If this check passes then the Note is added. The value may be a boolean or ModNoteCriteria.
*
* Boolean convenience:
*
* * If `true` or undefined then CM generates a ModNoteCriteria that passes only if there is NO existing note matching note criteria
* * If `false` then no check is performed and Note is always added
*
* @examples [true]
* @default true
* */
allowDuplicate?: boolean,
existingNoteCheck?: boolean | ModNoteCriteria,
type?: ModUserNoteLabel
referenceActivity?: boolean
}

View File

@@ -1,7 +1,6 @@
import {ActionJson, ActionConfig, ActionOptions} from "./index";
import Action from "./index";
import {Comment} from "snoowrap";
import {renderContent} from "../Utils/SnoowrapUtils";
import {UserNoteJson} from "../Subreddit/UserNotes";
import Submission from "snoowrap/dist/objects/Submission";
import {ActionProcessResult, RuleResult} from "../Common/interfaces";
@@ -9,19 +8,34 @@ import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
import {runCheckOptions} from "../Subreddit/Manager";
import {ActionTypes, UserNoteType} from "../Common/Infrastructure/Atomic";
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
import {
FullUserNoteCriteria,
toFullUserNoteCriteria, UserNoteCriteria
} from "../Common/Infrastructure/Filters/FilterCriteria";
import {buildFilterCriteriaSummary} from "../util";
export class UserNoteAction extends Action {
content: string;
type: UserNoteType;
allowDuplicate: boolean;
existingNoteCheck?: UserNoteCriteria
constructor(options: UserNoteActionOptions) {
super(options);
const {type, content = '', allowDuplicate = false} = options;
const {type, content = '', existingNoteCheck = true, allowDuplicate} = options;
this.type = type;
this.content = content;
this.allowDuplicate = allowDuplicate;
if(typeof existingNoteCheck !== 'boolean') {
this.existingNoteCheck = existingNoteCheck;
} else {
let exNotecheck: boolean;
if(allowDuplicate !== undefined) {
exNotecheck = !allowDuplicate;
} else {
exNotecheck = existingNoteCheck;
}
this.existingNoteCheck = this.generateCriteriaFromDuplicateConvenience(exNotecheck);
}
}
getKind(): ActionTypes {
@@ -33,25 +47,30 @@ export class UserNoteAction extends Action {
const renderedContent = (await this.renderContent(this.content, item, ruleResults, actionResults) as string);
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
if (!this.allowDuplicate) {
const notes = await this.resources.userNotes.getUserNotes(item.author);
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,
success: false,
result: `Will not add note because one already exists for this Activity (${existingNote.time.local().format()}) and allowDuplicate=false`
};
}
let noteCheckPassed: boolean = true;
let noteCheckResult: undefined | string;
if(this.existingNoteCheck === undefined) {
// nothing to do!
noteCheckResult = 'existingNoteCheck=false so no existing note checks were performed.';
} else {
const noteCheckCriteriaResult = await this.resources.isAuthor(item, {
userNotes: [this.existingNoteCheck]
});
noteCheckPassed = noteCheckCriteriaResult.passed;
const {details} = buildFilterCriteriaSummary(noteCheckCriteriaResult);
noteCheckResult = `${noteCheckPassed ? 'Existing note check condition succeeded' : 'Will not add note because existing note check condition failed'} -- ${details.join(' ')}`;
}
this.logger.info(noteCheckResult);
if (!noteCheckPassed) {
return {
dryRun,
success: false,
result: noteCheckResult
};
}
if (!dryRun) {
await this.resources.userNotes.addUserNote(item, this.type, renderedContent, this.name !== undefined ? `(Action ${this.name})` : '');
} else if (!await this.resources.userNotes.warningExists(this.type)) {
@@ -64,11 +83,23 @@ export class UserNoteAction extends Action {
}
}
generateCriteriaFromDuplicateConvenience(val: boolean): UserNoteCriteria | undefined {
if(val) {
return {
type: this.type,
note: this.content !== '' && this.content !== undefined && this.content !== null ? [this.content] : undefined,
search: 'current',
count: '< 1'
};
}
return undefined;
}
protected getSpecificPremise(): object {
return {
content: this.content,
type: this.type,
allowDuplicate: this.allowDuplicate
existingNoteCheck: this.existingNoteCheck
}
}
}
@@ -76,10 +107,29 @@ export class UserNoteAction extends Action {
export interface UserNoteActionConfig extends ActionConfig,UserNoteJson {
/**
* Add Note even if a Note already exists for this Activity
*
* USE `existingNoteCheck` INSTEAD
*
* @examples [false]
* @default false
* @deprecated
* */
allowDuplicate?: boolean,
/**
* Check if there is an existing Note matching some criteria before adding the Note.
*
* If this check passes then the Note is added. The value may be a boolean or UserNoteCriteria.
*
* Boolean convenience:
*
* * If `true` or undefined then CM generates a UserNoteCriteria that passes only if there is NO existing note matching note criteria
* * If `false` then no check is performed and Note is always added
*
* @examples [true]
* @default true
* */
existingNoteCheck?: boolean | UserNoteCriteria,
}
export interface UserNoteActionOptions extends Omit<UserNoteActionConfig, 'authorIs' | 'itemIs'>, ActionOptions {

View File

@@ -529,7 +529,7 @@ class Bot implements BotInstanceFunctions {
for (const sub of subsToRun) {
if(!this.subManagers.some(x => x.subreddit.display_name === sub.display_name)) {
subManagersChanged = true;
this.logger.info(`Manager for ${sub.display_name_prefixed} not found in existing managers. Creating now...`);
this.logger.info(`Manager for ${sub.display_name_prefixed} not found in loaded managers. Loading now...`);
subsToInit.push(sub.display_name);
try {
this.subManagers.push(await this.createManager(sub));
@@ -743,6 +743,9 @@ class Bot implements BotInstanceFunctions {
eventsState: new EventsRunState({invokee, runType}),
managerState: new ManagerRunState({invokee, runType})
}));
this.logger.info(`Created new Manager (${managerEntity.id}) for ${subVal.display_name}`);
} else {
this.logger.info(`Found existing Manager (${managerEntity.id}) for ${subVal.display_name}`);
}
const manager = new Manager(sub, this.client, this.logger, this.cacheManager, {

View File

@@ -1,16 +1,40 @@
import {InfluxConfig} from "./interfaces";
import {InfluxDB, Point, WriteApi, setLogger} from "@influxdata/influxdb-client";
import {InfluxDB, Point, WriteApi, setLogger, DEFAULT_WriteOptions, ClientOptions, DEFAULT_RetryDelayStrategyOptions, Logger as InfluxLogger} from "@influxdata/influxdb-client";
import {HealthAPI} from "@influxdata/influxdb-client-apis";
import dayjs, {Dayjs} from "dayjs";
import {Logger} from "winston";
import {mergeArr} from "../../util";
import {CMError} from "../../Utils/Errors";
import {Agent} from 'http';
import {WriteOptions} from "@influxdata/influxdb-client/dist";
export interface InfluxClientConfig extends InfluxConfig {
client?: InfluxDB
ready?: boolean
}
/**
* Suppress non-error write failures
*
* These have not yet hit the max retry. On max retry failure Influx logs as ERROR.
* The non-error failures are super noisy in the log so suppress them UNLESS debug is turned on
*
* https://github.com/influxdata/influxdb-client-js/blob/master/packages/core/src/impl/WriteApiImpl.ts#L221
* */
const extendLogger = (logger: Logger, suppressWriteWarnings = true): InfluxLogger => {
return {
...logger,
error: (message: string, err?: any) => logger.error(message, err),
warn: (message: string, err?: any) => {
if(suppressWriteWarnings && !message.includes('Write to InfluxDB failed (attempt')) {
logger.warn(message, err);
} else if(!suppressWriteWarnings) {
logger.warn(message, err);
}
}
}
}
export class InfluxClient {
config: InfluxConfig;
client: InfluxDB;
@@ -34,13 +58,14 @@ export class InfluxClient {
this.config = rest;
this.ready = ready;
if(client !== undefined) {
if (client !== undefined) {
this.client = client;
} else {
this.client = InfluxClient.createClient(this.config);
setLogger(this.logger);
this.client = InfluxClient.createClient(this.config);
setLogger(extendLogger(this.logger, !(rest.debug ?? false)));
}
this.write = this.client.getWriteApi(config.credentials.org, config.credentials.bucket, 'ms');
this.write = this.client.getWriteApi(config.credentials.org, config.credentials.bucket, 'ms', InfluxClient.createWriteOptions(this.config, this.logger));
this.tags = tags;
this.write.useDefaultTags(tags);
this.health = new HealthAPI(this.client);
@@ -96,13 +121,62 @@ export class InfluxClient {
}
static createClient(config: InfluxConfig): InfluxDB {
return new InfluxDB({
url: config.credentials.url,
token: config.credentials.token,
const {
credentials,
useKeepAliveAgent = true,
} = config;
const clientOptions: ClientOptions = {
url: credentials.url,
token: credentials.token,
writeOptions: InfluxClient.createWriteOptions(config),
}
if (useKeepAliveAgent) {
// reusing connection
// https://github.com/influxdata/influxdb-client-js/issues/393#issuecomment-985272866
const agent = new Agent({
keepAlive: true,
keepAliveMsecs: 20 * 1000, // 20 seconds keep alive
})
process.on('exit', () => agent.destroy())
clientOptions.transportOptions = {agent};
}
return new InfluxDB(clientOptions);
}
static createWriteOptions(config: InfluxConfig, logger?: Logger): Partial<WriteOptions> {
const {
writeOptions: {
defaultTags: config.defaultTags
defaultTags: userDefinedDefaultTags = {},
...restUserWriteOptions
} = {
batchSize: 500,
maxRetries: 5,
// 30 seconds
flushInterval: 30000
},
defaultTags: legacyDefaultTags = {},
debug = false,
} = config;
const allUserDefinedTags = {...legacyDefaultTags, ...userDefinedDefaultTags};
const writeOptions: Partial<WriteOptions> = {
...DEFAULT_WriteOptions,
...restUserWriteOptions,
defaultTags: allUserDefinedTags
}
if (debug && logger !== undefined) {
writeOptions.writeSuccess = (lines: Array<string>) => {
logger.debug(`Flushed ${lines.length} lines to server`);
};
writeOptions.writeRetrySkipped = (entry: { lines: Array<string>; expires: number }) => {
logger.warn(`Skipped flushing ${entry.lines.length} lines due to full buffer`);
}
});
}
return writeOptions;
}
childClient(logger: Logger, tags: Record<string, string> = {}) {

View File

@@ -1,8 +1,11 @@
import {InfluxDB, WriteApi} from "@influxdata/influxdb-client/dist";
import {InfluxDB, WriteApi, WriteOptions} from "@influxdata/influxdb-client/dist";
export interface InfluxConfig {
credentials: InfluxCredentials
defaultTags?: Record<string, string>
writeOptions?: WriteOptions
useKeepAliveAgent?: boolean
debug?: boolean
}
export interface InfluxCredentials {

View File

@@ -41,7 +41,7 @@ export type RelativeDateTimeMatch = string;
* * EX `> 100` => greater than 100
* * EX `<= 75%` => less than or equal to 75%
*
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$
* @pattern ^\s*(>|>=|<|<=)\s*((?:\d+)(?:(?:(?:.|,)\d+)+)?)\s*(%?)(.*)$
* */
export type CompareValueOrPercent = string;
export type StringOperator = '>' | '>=' | '<' | '<=';

View File

@@ -35,8 +35,8 @@ export const asGenericComparison = (val: any): val is GenericComparison => {
return typeof val === 'object' && 'value' in val;
}
export const GENERIC_VALUE_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>-?\d?\.?\d+)(?<extra>\s+.*)*$/
export const GENERIC_VALUE_COMPARISON_URL = 'https://regexr.com/60dq4';
export const GENERIC_VALUE_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>-?(?:\d+)(?:(?:(?:.|,)\d+)+)?)(?<extra>\s+.*)*$/
export const GENERIC_VALUE_COMPARISON_URL = 'https://regexr.com/6vama';
export const parseGenericValueComparison = (val: string, options?: {
requireDuration?: boolean,
reg?: RegExp
@@ -107,8 +107,8 @@ export const parseGenericValueComparison = (val: string, options?: {
durationText,
}
}
const GENERIC_VALUE_PERCENT_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>\d+)\s*(?<percent>%)?(?<extra>.*)$/
const GENERIC_VALUE_PERCENT_COMPARISON_URL = 'https://regexr.com/60a16';
const GENERIC_VALUE_PERCENT_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>(?:\d+)(?:(?:(?:.|,)\d+)+)?)\s*(?<percent>%)?(?<extra>.*)$/
const GENERIC_VALUE_PERCENT_COMPARISON_URL = 'https://regexr.com/6valr';
export const parseGenericValueOrPercentComparison = (val: string, options?: {requireDuration: boolean}): GenericComparison => {
return parseGenericValueComparison(val, {...(options ?? {}), reg: GENERIC_VALUE_PERCENT_COMPARISON});
}

View File

@@ -6,9 +6,10 @@ import {
ModeratorNames, ModActionType,
ModUserNoteLabel, RelativeDateTimeMatch
} from "../Atomic";
import {ActivityType} from "../Reddit";
import {ActivityType, MaybeActivityType} from "../Reddit";
import {GenericComparison, parseGenericValueComparison} from "../Comparisons";
import {parseStringToRegexOrLiteralSearch} from "../../../util";
import { Submission, Comment } from "snoowrap";
/**
* Different attributes a `Subreddit` can be in. Only include a property if you want to check it.
@@ -118,17 +119,49 @@ export interface UserNoteCriteria extends UserSubredditHistoryCriteria {
* @examples ["spamwarn"]
* */
type: string;
/**
* The content of the Note to search For.
*
* * Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.
* * Can also be Regular Expression if wrapped in forward slashes IE '\/test.*\/i'
* */
note?: string | string[]
/*
* Does this note link to the currently processing Activity?
* */
referencesCurrentActivity?: boolean
}
export interface FullUserNoteCriteria extends Omit<UserNoteCriteria, 'note'> {
note?: RegExp[]
}
export const toFullUserNoteCriteria = (val: UserNoteCriteria): FullUserNoteCriteria => {
const {note} = val;
let notesVal = undefined;
if (note !== undefined) {
const notesArr = Array.isArray(note) ? note : [note];
notesVal = notesArr.map(x => parseStringToRegexOrLiteralSearch(x));
}
return {
...val,
note: notesVal
}
}
export interface ModActionCriteria extends UserSubredditHistoryCriteria {
type?: ModActionType | ModActionType[]
activityType?: ActivityType | ActivityType[]
activityType?: MaybeActivityType | MaybeActivityType[]
referencesCurrentActivity?: boolean
}
export interface FullModActionCriteria extends Omit<ModActionCriteria, 'count'> {
type?: ModActionType[]
count?: GenericComparison
activityType?: ActivityType[]
/*
* Does this action/note link to the currently processing Activity?
* */
activityType?: MaybeActivityType[]
}
export interface ModNoteCriteria extends ModActionCriteria {
@@ -138,6 +171,12 @@ export interface ModNoteCriteria extends ModActionCriteria {
export interface FullModNoteCriteria extends FullModActionCriteria, Omit<ModNoteCriteria, 'note' | 'count' | 'type' | 'activityType'> {
noteType?: ModUserNoteLabel[]
/**
* The content of the Note to search For.
*
* * Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.
* * Can also be Regular Expression if wrapped in forward slashes IE '\/test.*\/i'
* */
note?: RegExp[]
}
@@ -167,6 +206,7 @@ export const toFullModNoteCriteria = (val: ModNoteCriteria): FullModNoteCriteria
break;
case 'activityType':
case 'noteType':
case 'referencesCurrentActivity':
acc[k] = rawVal;
break;
case 'note':
@@ -219,6 +259,7 @@ export const toFullModLogCriteria = (val: ModLogCriteria): FullModLogCriteria =>
break;
case 'activityType':
case 'type':
case 'referencesCurrentActivity':
acc[k as keyof FullModLogCriteria] = rawVal;
break;
case 'action':
@@ -485,6 +526,33 @@ export interface ActivityState {
*
* */
source?: string | string[]
/**
* * If `true` then passes if ANY flair
* * If `false` then passes if NO flair
* * If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes.
* */
authorFlairText?: boolean | string | string[]
/**
* * If `true` then passes if ANY flair
* * If `false` then passes if NO flair
* * If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes.
* */
authorFlairTemplateId?: boolean | string | string[]
/**
* * If `true` then passes if ANY class
* * If `false` then passes if NO class
* * If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes.
* */
authorFlairCssClass?: boolean | string | string[]
/**
* * If `true` then passes if ANY color
* * If `false` then passes if NO color
* * If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes.
* */
authorFlairBackgroundColor?: boolean | string | string[]
}
/**
@@ -507,13 +575,22 @@ export interface SubmissionState extends ActivityState {
/**
* * If `true` then passes if flair has ANY text
* * If `false` then passes if flair has NO text
* * If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes.
* */
link_flair_text?: boolean | string | string[]
/**
* * If `true` then passes if flair has ANY css
* * If `false` then passes if flair has NO css
* * If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes.
* */
link_flair_css_class?: boolean | string | string[]
/**
* * If `true` then passes if ANY color
* * If `false` then passes if NO color
* * If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes.
* */
link_flair_background_color?: boolean | string | string[]
/**
* * If `true` then passes if there is ANY flair template id
* * If `false` then passes if there is NO flair template id
@@ -537,6 +614,16 @@ export interface SubmissionState extends ActivityState {
upvoteRatio?: number | CompareValue
}
export const cmToSnoowrapActivityMap: Record<string, keyof (Submission & Comment)> = {
authorFlairText: 'author_flair_text',
flairText: 'author_flair_text',
authorFlairTemplateId: 'author_flair_template_id',
authorFlairCssClass: 'author_flair_css_class',
authorFlairBackgroundColor: 'author_flair_background_color',
flairTemplate: 'link_flair_template_id',
flairCssClass: 'author_flair_css_class',
}
export const cmActivityProperties = ['submissionState', 'score', 'reports', 'removed', 'deleted', 'filtered', 'age', 'title'];
/**

View File

@@ -1,6 +1,7 @@
import {Comment, Submission} from "snoowrap/dist/objects";
export type ActivityType = 'submission' | 'comment';
export type MaybeActivityType = ActivityType | false;
export type FullNameTypes = ActivityType | 'user' | 'subreddit' | 'message';
export interface RedditThing {

View File

@@ -45,4 +45,4 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
export const defaultDataDir = path.resolve(__dirname, '../..');
export const defaultConfigFilenames = ['config.json', 'config.yaml'];
export const VERSION = '0.12.2';
export const VERSION = '0.13.2';

View File

@@ -81,6 +81,23 @@ export interface HistoryCriteria {
window: ActivityWindowConfig
ratio?: {
window: ActivityWindowConfig
/**
* A string containing a comparison operator and a value to compare number of parent criteria activities against number of "ratio" activities
*
* This comparison is always done as (number of parent criteria activities) / (number of ratio activities)
*
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
*
* * EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities
* * EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities
*
* @pattern ^\s*(>|>=|<|<=)\s*((?:\d+)(?:(?:(?:.|,)\d+)+)?)\s*(%?)(.*)$
* */
threshold: CompareValueOrPercent
}
/**
* The minimum number of **filtered** activities that must exist from the `window` results for this criteria to run
* @default 5
@@ -170,7 +187,7 @@ export class HistoryRule extends Rule {
for (const criteria of this.criteria) {
const {comment, window, submission, total, minActivityCount = 5} = criteria;
const {comment, window, submission, total, ratio, minActivityCount = 5} = criteria;
const {pre: activities, post: filteredActivities} = await this.resources.getAuthorActivitiesWithFilter(item.author, window);
@@ -251,6 +268,24 @@ export class HistoryRule extends Rule {
}
}
let foundRatio = undefined;
let ratioTrigger = undefined;
if(ratio !== undefined) {
const { window: ratioWindow, threshold: ratioThreshold } = ratio;
const {operator, value, isPercent, extra = ''} = parseGenericValueOrPercentComparison(ratioThreshold);
const ratioWindowConfig = windowConfigToWindowCriteria(ratioWindow);
const {post: ratioActivities} = await this.resources.getAuthorActivitiesWithFilter(item.author, ratioWindowConfig);
const ratioVal = filteredActivities.length / ratioActivities.length;
foundRatio = formatNumber(ratioVal);
if(isPercent) {
const per = value / 100;
ratioTrigger = comparisonTextOp(ratioVal, operator, per);
} else {
ratioTrigger = comparisonTextOp(ratioVal, operator, value);
}
}
const firstActivity = activities[0];
const lastActivity = activities[activities.length - 1];
@@ -263,11 +298,13 @@ export class HistoryRule extends Rule {
submissionTotal: fSubmissionTotal,
commentTotal: fCommentTotal,
opTotal: fOpTotal,
foundRatio,
filteredTotal: filteredActivities.length,
submissionTrigger,
commentTrigger,
totalTrigger,
triggered: (submissionTrigger === undefined || submissionTrigger === true) && (commentTrigger === undefined || commentTrigger === true) && (totalTrigger === undefined || totalTrigger === true),
ratioTrigger,
triggered: (submissionTrigger === undefined || submissionTrigger === true) && (commentTrigger === undefined || commentTrigger === true) && (totalTrigger === undefined || totalTrigger === true) && (ratioTrigger === undefined || ratioTrigger === true),
subredditBreakdown: getSubredditBreakdownByActivityType(!asOp ? filteredActivities : filteredActivities.filter(x => asSubmission(x) || x.is_submitter))
});
}
@@ -318,11 +355,13 @@ export class HistoryRule extends Rule {
submissionTotal,
commentTotal,
filteredTotal,
foundRatio,
opTotal,
criteria: {
comment,
submission,
total,
ratio,
window,
},
criteria,
@@ -330,6 +369,7 @@ export class HistoryRule extends Rule {
submissionTrigger,
commentTrigger,
totalTrigger,
ratioTrigger,
subredditBreakdown,
} = results;
@@ -338,6 +378,7 @@ export class HistoryRule extends Rule {
submissionTotal,
commentTotal,
filteredTotal,
foundRatio,
opTotal,
commentPercent: formatNumber((commentTotal/activityTotal)*100),
submissionPercent: formatNumber((submissionTotal/activityTotal)*100),
@@ -349,6 +390,7 @@ export class HistoryRule extends Rule {
submissionTrigger,
commentTrigger,
totalTrigger,
ratioTrigger,
subredditBreakdown
};
@@ -356,6 +398,7 @@ export class HistoryRule extends Rule {
let totalSummary;
let submissionSummary;
let commentSummary;
let ratioSummary;
if(total !== undefined) {
const {operator, value, isPercent, displayText} = parseGenericValueOrPercentComparison(total);
const suffix = !isPercent ? 'Items' : `(${formatNumber((filteredTotal/activityTotal)*100)}%) of ${activityTotal} Total`;
@@ -380,6 +423,13 @@ export class HistoryRule extends Rule {
data.commentSummary = commentSummary;
thresholdSummary.push(commentSummary);
}
if(ratio !== undefined) {
const {threshold} = ratio;
const {operator, value, isPercent, displayText, extra = ''} = parseGenericValueOrPercentComparison(threshold);
ratioSummary = `${includePassFailSymbols ? `${submissionTrigger ? PASS : FAIL} ` : ''}Activity Ratio of (${foundRatio}) ${ratioTrigger ? 'passed' : 'did not pass'} test of ${displayText}`;
data.ratioSummary = ratioSummary;
thresholdSummary.push(ratioSummary);
}
data.thresholdSummary = thresholdSummary.join(' and ');

View File

@@ -44,6 +44,7 @@ import {ActivityWindow, ActivityWindowConfig} from "../Common/Infrastructure/Act
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
import {ImageHashCacheData} from "../Common/Infrastructure/Atomic";
import {getSubredditBreakdownByActivityType} from "../Utils/SnoowrapUtils";
import {CMError} from "../Utils/Errors";
const parseLink = parseUsableLinkIdentifier();
@@ -315,7 +316,7 @@ export class RecentActivityRule extends Rule {
}
} catch (err: any) {
if(!err.message.includes('did not end with a valid image extension')) {
this.logger.warn(`Will not compare image from Submission ${x.id} due to error while parsing image URL => ${err.message}`);
this.logger.warn(new CMError(`Will not compare image from Submission ${x.id} due to error while parsing image URL`, {cause: err}));
}
}
}

View File

@@ -764,6 +764,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1717,18 +1785,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -1767,6 +1835,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -1813,14 +1884,6 @@
"ModNoteActionJson": {
"description": "Add a Toolbox User Note to the Author of this Activity",
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"examples": [
false
],
"type": "boolean"
},
"authorIs": {
"anyOf": [
{
@@ -1871,6 +1934,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/ModNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or ModNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a ModNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -1943,18 +2021,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -2013,6 +2091,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -2522,6 +2603,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -2586,6 +2735,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -2601,7 +2767,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -2618,7 +2784,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -2806,7 +2972,7 @@
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"description": "Add Note even if a Note already exists for this Activity\n\nUSE `existingNoteCheck` INSTEAD",
"examples": [
false
],
@@ -2862,6 +3028,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/UserNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or UserNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a UserNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -2929,6 +3110,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -28,6 +28,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1704,6 +1772,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -2904,6 +3040,40 @@
"name": {
"type": "string"
},
"ratio": {
"properties": {
"threshold": {
"description": "A string containing a comparison operator and a value to compare number of parent criteria activities against number of \"ratio\" activities\n\nThis comparison is always done as (number of parent criteria activities) / (number of ratio activities)\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities\n* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*((?:\\d+)(?:(?:(?:.|,)\\d+)+)?)\\s*(%?)(.*)$",
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
"string",
"number"
]
}
],
"description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`",
"examples": [
"90 days"
]
}
},
"required": [
"threshold",
"window"
],
"type": "object"
},
"submission": {
"description": "A string containing a comparison operator and a value to compare **filtered** (using `include` or `exclude`, if present) submissions against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 filtered submissions\n* EX `<= 75%` => filtered submissions are equal to or less than 75% of unfiltered Activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -3574,18 +3744,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3624,6 +3794,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -3670,14 +3843,6 @@
"ModNoteActionJson": {
"description": "Add a Toolbox User Note to the Author of this Activity",
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"examples": [
false
],
"type": "boolean"
},
"authorIs": {
"anyOf": [
{
@@ -3728,6 +3893,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/ModNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or ModNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a ModNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -3800,18 +3980,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3870,6 +4050,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -6218,6 +6401,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -6282,6 +6533,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -6297,7 +6565,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -6314,7 +6582,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -6586,7 +6854,7 @@
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"description": "Add Note even if a Note already exists for this Activity\n\nUSE `existingNoteCheck` INSTEAD",
"examples": [
false
],
@@ -6642,6 +6910,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/UserNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or UserNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a UserNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -6709,6 +6992,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -42,6 +42,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1527,6 +1595,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -2618,6 +2754,40 @@
"name": {
"type": "string"
},
"ratio": {
"properties": {
"threshold": {
"description": "A string containing a comparison operator and a value to compare number of parent criteria activities against number of \"ratio\" activities\n\nThis comparison is always done as (number of parent criteria activities) / (number of ratio activities)\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities\n* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*((?:\\d+)(?:(?:(?:.|,)\\d+)+)?)\\s*(%?)(.*)$",
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
"string",
"number"
]
}
],
"description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`",
"examples": [
"90 days"
]
}
},
"required": [
"threshold",
"window"
],
"type": "object"
},
"submission": {
"description": "A string containing a comparison operator and a value to compare **filtered** (using `include` or `exclude`, if present) submissions against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 filtered submissions\n* EX `<= 75%` => filtered submissions are equal to or less than 75% of unfiltered Activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -3288,18 +3458,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3338,6 +3508,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -3384,14 +3557,6 @@
"ModNoteActionJson": {
"description": "Add a Toolbox User Note to the Author of this Activity",
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"examples": [
false
],
"type": "boolean"
},
"authorIs": {
"anyOf": [
{
@@ -3442,6 +3607,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/ModNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or ModNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a ModNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -3514,18 +3694,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3584,6 +3764,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -5662,6 +5845,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -5726,6 +5977,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -5741,7 +6009,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -5758,7 +6026,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -6002,7 +6270,7 @@
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"description": "Add Note even if a Note already exists for this Activity\n\nUSE `existingNoteCheck` INSTEAD",
"examples": [
false
],
@@ -6058,6 +6326,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/UserNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or UserNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a UserNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -6125,6 +6408,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -534,6 +534,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -900,8 +968,18 @@
"credentials": {
"$ref": "#/definitions/InfluxCredentials"
},
"debug": {
"type": "boolean"
},
"defaultTags": {
"$ref": "#/definitions/Record<string,string>"
},
"useKeepAliveAgent": {
"type": "boolean"
},
"writeOptions": {
"$ref": "#/definitions/WriteOptions",
"description": "Options used by{@linkWriteApi}."
}
},
"required": [
@@ -1065,18 +1143,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -1115,6 +1193,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -1166,18 +1247,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -1236,6 +1317,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -1860,6 +1944,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1924,6 +2076,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -1939,7 +2108,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -1956,7 +2125,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -2124,6 +2293,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -2183,6 +2369,94 @@
}
},
"type": "object"
},
"WriteOptions": {
"description": "Options used by{@linkWriteApi}.",
"properties": {
"batchSize": {
"description": "max number of records/lines to send in a batch",
"type": "number"
},
"consistency": {
"description": "InfluxDB Enterprise write consistency as explained in https://docs.influxdata.com/enterprise_influxdb/v1.9/concepts/clustering/#write-consistency",
"enum": [
"all",
"any",
"one",
"quorum"
],
"type": "string"
},
"defaultTags": {
"$ref": "#/definitions/Record<string,string>",
"description": "default tags, unescaped"
},
"exponentialBase": {
"description": "base for the exponential retry delay",
"type": "number"
},
"flushInterval": {
"description": "delay between data flushes in milliseconds, at most `batch size` records are sent during flush",
"type": "number"
},
"gzipThreshold": {
"description": "When specified, write bodies larger than the threshold are gzipped",
"type": "number"
},
"headers": {
"additionalProperties": {
"type": "string"
},
"description": "HTTP headers that will be sent with every write request",
"type": "object"
},
"maxBatchBytes": {
"description": "max size of a batch in bytes",
"type": "number"
},
"maxBufferLines": {
"description": "the maximum size of retry-buffer (in lines)",
"type": "number"
},
"maxRetries": {
"description": "max count of retries after the first write fails",
"type": "number"
},
"maxRetryDelay": {
"description": "maximum delay when retrying write (milliseconds)",
"type": "number"
},
"maxRetryTime": {
"description": "max time (millis) that can be spent with retries",
"type": "number"
},
"minRetryDelay": {
"description": "minimum delay when retrying write (milliseconds)",
"type": "number"
},
"randomRetry": {
"description": "randomRetry indicates whether the next retry delay is deterministic (false) or random (true).\nThe deterministic delay starts with `minRetryDelay * exponentialBase` and it is multiplied\nby `exponentialBase` until it exceeds `maxRetryDelay`.\nWhen random is `true`, the next delay is computed as a random number between next retry attempt (upper)\nand the lower number in the deterministic sequence. `random(retryJitter)` is added to every returned value.",
"type": "boolean"
},
"retryJitter": {
"description": "add `random(retryJitter)` milliseconds delay when retrying HTTP calls",
"type": "number"
}
},
"required": [
"batchSize",
"exponentialBase",
"flushInterval",
"maxBatchBytes",
"maxBufferLines",
"maxRetries",
"maxRetryDelay",
"maxRetryTime",
"minRetryDelay",
"randomRetry",
"retryJitter"
],
"type": "object"
}
},
"description": "Configuration for application-level settings IE for running the bot instance\n\n* To load a JSON configuration **from the command line** use the `-c` cli argument EX: `node src/index.js -c /path/to/JSON/config.json`\n* To load a JSON configuration **using an environmental variable** use `OPERATOR_CONFIG` EX: `OPERATOR_CONFIG=/path/to/JSON/config.json`",

View File

@@ -63,6 +63,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -781,6 +849,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1467,6 +1603,40 @@
"name": {
"type": "string"
},
"ratio": {
"properties": {
"threshold": {
"description": "A string containing a comparison operator and a value to compare number of parent criteria activities against number of \"ratio\" activities\n\nThis comparison is always done as (number of parent criteria activities) / (number of ratio activities)\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities\n* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*((?:\\d+)(?:(?:(?:.|,)\\d+)+)?)\\s*(%?)(.*)$",
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
"string",
"number"
]
}
],
"description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`",
"examples": [
"90 days"
]
}
},
"required": [
"threshold",
"window"
],
"type": "object"
},
"submission": {
"description": "A string containing a comparison operator and a value to compare **filtered** (using `include` or `exclude`, if present) submissions against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 filtered submissions\n* EX `<= 75%` => filtered submissions are equal to or less than 75% of unfiltered Activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -1915,18 +2085,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -1965,6 +2135,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -2016,18 +2189,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -2086,6 +2259,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -3442,6 +3618,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -3506,6 +3750,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -3521,7 +3782,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -3538,7 +3799,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -3684,6 +3945,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -28,6 +28,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -746,6 +814,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1432,6 +1568,40 @@
"name": {
"type": "string"
},
"ratio": {
"properties": {
"threshold": {
"description": "A string containing a comparison operator and a value to compare number of parent criteria activities against number of \"ratio\" activities\n\nThis comparison is always done as (number of parent criteria activities) / (number of ratio activities)\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities\n* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*((?:\\d+)(?:(?:(?:.|,)\\d+)+)?)\\s*(%?)(.*)$",
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
"string",
"number"
]
}
],
"description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`",
"examples": [
"90 days"
]
}
},
"required": [
"threshold",
"window"
],
"type": "object"
},
"submission": {
"description": "A string containing a comparison operator and a value to compare **filtered** (using `include` or `exclude`, if present) submissions against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 filtered submissions\n* EX `<= 75%` => filtered submissions are equal to or less than 75% of unfiltered Activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -1880,18 +2050,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -1930,6 +2100,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -1981,18 +2154,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -2051,6 +2224,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -3407,6 +3583,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -3471,6 +3715,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -3486,7 +3747,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -3503,7 +3764,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -3649,6 +3910,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -39,6 +39,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -1524,6 +1592,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -2685,6 +2821,40 @@
"name": {
"type": "string"
},
"ratio": {
"properties": {
"threshold": {
"description": "A string containing a comparison operator and a value to compare number of parent criteria activities against number of \"ratio\" activities\n\nThis comparison is always done as (number of parent criteria activities) / (number of ratio activities)\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 1.2` => There are 1.2 activities from parent criteria for every 1 ratio activities\n* EX `<= 75%` => There are equal to or less than 0.75 activities from parent criteria for every 1 ratio activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*((?:\\d+)(?:(?:(?:.|,)\\d+)+)?)\\s*(%?)(.*)$",
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
"string",
"number"
]
}
],
"description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`",
"examples": [
"90 days"
]
}
},
"required": [
"threshold",
"window"
],
"type": "object"
},
"submission": {
"description": "A string containing a comparison operator and a value to compare **filtered** (using `include` or `exclude`, if present) submissions against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 filtered submissions\n* EX `<= 75%` => filtered submissions are equal to or less than 75% of unfiltered Activities",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -3355,18 +3525,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3405,6 +3575,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -3451,14 +3624,6 @@
"ModNoteActionJson": {
"description": "Add a Toolbox User Note to the Author of this Activity",
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"examples": [
false
],
"type": "boolean"
},
"authorIs": {
"anyOf": [
{
@@ -3509,6 +3674,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/ModNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or ModNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a ModNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -3581,18 +3761,18 @@
"items": {
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
},
"type": "array"
},
{
"enum": [
"comment",
false,
"submission"
],
"type": "string"
]
}
]
},
@@ -3651,6 +3831,9 @@
}
]
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
@@ -5859,6 +6042,74 @@
],
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
},
"authorFlairBackgroundColor": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairCssClass": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY class\n* If `false` then passes if NO class\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairTemplateId": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then template id is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"authorFlairText": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY flair\n* If `false` then passes if NO flair\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"createdOn": {
"anyOf": [
{
@@ -5923,6 +6174,23 @@
"is_self": {
"type": "boolean"
},
"link_flair_background_color": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": [
"string",
"boolean"
]
}
],
"description": "* If `true` then passes if ANY color\n* If `false` then passes if NO color\n* If string or list of strings then color is matched, case-insensitive, without #. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_css_class": {
"anyOf": [
{
@@ -5938,7 +6206,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css"
"description": "* If `true` then passes if flair has ANY css\n* If `false` then passes if flair has NO css\n* If string or list of strings then class is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"link_flair_text": {
"anyOf": [
@@ -5955,7 +6223,7 @@
]
}
],
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text"
"description": "* If `true` then passes if flair has ANY text\n* If `false` then passes if flair has NO text\n* If string or list of strings then text is matched, case-insensitive. String may also be a regular expression enclosed in forward slashes."
},
"locked": {
"type": "boolean"
@@ -6199,7 +6467,7 @@
"properties": {
"allowDuplicate": {
"default": false,
"description": "Add Note even if a Note already exists for this Activity",
"description": "Add Note even if a Note already exists for this Activity\n\nUSE `existingNoteCheck` INSTEAD",
"examples": [
false
],
@@ -6255,6 +6523,21 @@
],
"type": "boolean"
},
"existingNoteCheck": {
"anyOf": [
{
"$ref": "#/definitions/UserNoteCriteria"
},
{
"type": "boolean"
}
],
"default": true,
"description": "Check if there is an existing Note matching some criteria before adding the Note.\n\nIf this check passes then the Note is added. The value may be a boolean or UserNoteCriteria.\n\nBoolean convenience:\n\n* If `true` or undefined then CM generates a UserNoteCriteria that passes only if there is NO existing note matching note criteria\n* If `false` then no check is performed and Note is always added",
"examples": [
true
]
},
"itemIs": {
"anyOf": [
{
@@ -6322,6 +6605,23 @@
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<duration>in\\s+\\d+\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?))?\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "The content of the Note to search For.\n\n* Can be a single string or list of strings to search for. Each string will be searched for case-insensitive, as a subset of note content.\n* Can also be Regular Expression if wrapped in forward slashes IE '\\/test.*\\/i'"
},
"referencesCurrentActivity": {
"type": "boolean"
},
"search": {
"default": "current",
"description": "How to test the Toolbox Notes or Mod Actions for this Author:\n\n### current\n\nOnly the most recent note is checked for criteria\n\n### total\n\n`count` comparison of mod actions/notes must be found within all history\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n* EX: `count: > 3 in 1 week` => Must have more than 3 notes within the last week\n\n### consecutive\n\nThe `count` **number** of mod actions/notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",

View File

@@ -654,10 +654,6 @@ export class Manager extends EventEmitter implements RunningStates {
this.displayLabel = nickname || `${this.subreddit.display_name_prefixed}`;
if (footer !== undefined && this.resources !== undefined) {
this.resources.footer = footer;
}
this.subMaxWorkers = maxWorkers;
const realMax = this.getMaxWorkers(this.subMaxWorkers);
if(realMax !== this.queue.concurrency) {
@@ -697,6 +693,10 @@ export class Manager extends EventEmitter implements RunningStates {
await this.setResourceManager(resourceConfig);
this.resources.setLogger(this.logger);
if (footer !== undefined && this.resources !== undefined) {
this.resources.footer = footer;
}
this.logger.info('Subreddit-specific options updated');
this.logger.info('Building Runs and Checks...');

View File

@@ -112,9 +112,20 @@ import cloneDeep from "lodash/cloneDeep";
import {
asModLogCriteria,
asModNoteCriteria,
AuthorCriteria, CommentState, ModLogCriteria, ModNoteCriteria, orderedAuthorCriteriaProps, RequiredAuthorCrit,
StrongSubredditCriteria, SubmissionState,
SubredditCriteria, toFullModLogCriteria, toFullModNoteCriteria, TypedActivityState, TypedActivityStates,
AuthorCriteria,
cmToSnoowrapActivityMap,
CommentState,
ModLogCriteria,
ModNoteCriteria,
orderedAuthorCriteriaProps,
RequiredAuthorCrit,
StrongSubredditCriteria,
SubmissionState,
SubredditCriteria,
toFullModLogCriteria,
toFullModNoteCriteria, toFullUserNoteCriteria,
TypedActivityState,
TypedActivityStates,
UserNoteCriteria
} from "../Common/Infrastructure/Filters/FilterCriteria";
import {
@@ -144,7 +155,7 @@ import {
ActivityType,
AuthorHistorySort,
CachedFetchedActivitiesResult, FetchedActivitiesResult,
CachedFetchedActivitiesResult, FetchedActivitiesResult, MaybeActivityType,
SnoowrapActivity, SubredditRemovalReason
} from "../Common/Infrastructure/Reddit";
import {AuthorCritPropHelper} from "../Common/Infrastructure/Filters/AuthorCritPropHelper";
@@ -1221,6 +1232,156 @@ export class SubredditResources {
return false;
}
filterAuthorModActions(modActions: ModNote[], actionCriteria: (ModNoteCriteria | ModLogCriteria), referenceItem: SnoowrapActivity) {
const {search = 'current', count = '>= 1'} = actionCriteria;
const {
value,
operator,
isPercent,
duration,
extra = ''
} = parseGenericValueOrPercentComparison(count);
const cutoffDate = duration === undefined ? undefined : dayjs().subtract(duration);
let actionsToUse: ModNote[] = [];
if(asModNoteCriteria(actionCriteria)) {
actionsToUse = modActions.filter(x => x.type === 'NOTE');
} else {
actionsToUse = modActions;
}
if(search === 'current' && actionsToUse.length > 0) {
actionsToUse = [actionsToUse[0]];
}
let validActions: ModNote[] = [];
if (asModLogCriteria(actionCriteria)) {
const fullCrit = toFullModLogCriteria(actionCriteria);
const fullCritEntries = Object.entries(fullCrit);
validActions = actionsToUse.filter(x => {
// filter out any notes that occur before time range
if(cutoffDate !== undefined && x.createdAt.isBefore(cutoffDate)) {
return false;
}
for (const [k, v] of fullCritEntries) {
const key = k.toLocaleLowerCase();
if (['count', 'search'].includes(key)) {
continue;
}
switch (key) {
case 'type':
if (!v.includes((x.type as ModActionType))) {
return false
}
break;
case 'activitytype':
const anyMatch = v.some((a: MaybeActivityType) => {
switch (a) {
case 'submission':
return isSubmission(x.action.actedOn);
case 'comment':
return isComment(x.action.actedOn);
case false:
return x.action.actedOn === undefined || (!asSubmission(x.action.actedOn) && !asComment(x.action.actedOn));
}
});
if (!anyMatch) {
return false;
}
break;
case 'description':
case 'action':
case 'details':
const actionPropVal = x.action[key] as string;
if (actionPropVal === undefined) {
return false;
}
const anyPropMatch = v.some((y: RegExp) => y.test(actionPropVal));
if (!anyPropMatch) {
return false;
}
break;
case 'referencescurrentactivity':
const isCurrentActivity = x.action.actedOn !== undefined && referenceItem !== undefined && x.action.actedOn.name === referenceItem.name;
if((v === true && !isCurrentActivity) || (v === false && isCurrentActivity)) {
return false;
}
break;
} // case end
} // for each end
return true;
}); // filter end
} else if(asModNoteCriteria(actionCriteria)) {
const fullCrit = toFullModNoteCriteria(actionCriteria as ModNoteCriteria);
const fullCritEntries = Object.entries(fullCrit);
validActions = actionsToUse.filter(x => {
// filter out any notes that occur before time range
if(cutoffDate !== undefined && x.createdAt.isBefore(cutoffDate)) {
return false;
}
for (const [k, v] of fullCritEntries) {
const key = k.toLocaleLowerCase();
if (['count', 'search'].includes(key)) {
continue;
}
switch (key) {
case 'notetype':
if (!v.map((x: ModUserNoteLabel) => x.toUpperCase()).includes((x.note.label as ModUserNoteLabel))) {
return false
}
break;
case 'note':
const actionPropVal = x.note.note;
if (actionPropVal === undefined) {
return false;
}
const anyPropMatch = v.some((y: RegExp) => y.test(actionPropVal));
if (!anyPropMatch) {
return false;
}
break;
case 'activitytype':
const anyMatch = v.some((a: MaybeActivityType) => {
switch (a) {
case 'submission':
return isSubmission(x.action.actedOn);
case 'comment':
return isComment(x.action.actedOn);
case false:
return x.action.actedOn === undefined || (!asSubmission(x.action.actedOn) && !asComment(x.action.actedOn));
}
});
if (!anyMatch) {
return false;
}
break;
case 'referencescurrentactivity':
const isCurrentActivity = x.action.actedOn !== undefined && referenceItem !== undefined && x.action.actedOn.id === referenceItem.name;
if((v === true && !isCurrentActivity) || (v === false && isCurrentActivity)) {
return false;
}
break;
} // case end
} // for each end
return true;
}); // filter end
} else {
throw new SimpleError(`Could not determine if a modActions criteria was for Mod Log or Mod Note. Given: ${JSON.stringify(actionCriteria)}`);
}
return [validActions, actionsToUse];
}
async getAuthorModNotesByActivityAuthor(activity: Comment | Submission) {
const author = activity.author instanceof RedditUser ? activity.author : getActivityAuthorName(activity.author);
if (activity.subreddit.display_name !== this.subreddit.display_name) {
@@ -2654,13 +2815,22 @@ export class SubredditResources {
case 'flairTemplate':
case 'link_flair_text':
case 'link_flair_css_class':
if(asSubmission(item)) {
let propertyValue: string | null;
if(k === 'flairTemplate') {
propertyValue = await item.link_flair_template_id;
} else {
propertyValue = await item[k];
}
case 'link_flair_background_color':
case 'authorFlairText':
case 'authorFlairCssClass':
case 'authorFlairTemplateId':
case 'authorFlairBackgroundColor':
let actualPropName = cmToSnoowrapActivityMap[k] ?? k;
if(!asSubmission(item) && (actualPropName as string).includes('link_flair')) {
propResultsMap[k]!.passed = true;
propResultsMap[k]!.reason = `Cannot test for ${k} on Comment`;
log.warn(`Cannot test for ${k} on Comment`);
break;
} else {
// @ts-ignore
let propertyValue: string | null = await item[actualPropName];
propResultsMap[k]!.found = propertyValue;
@@ -2674,15 +2844,38 @@ export class SubredditResources {
// if crit is not a boolean but property is "empty" then it'll never pass anyway
propResultsMap[k]!.passed = !include;
} else {
const expectedValues = typeof itemOptVal === 'string' ? [itemOptVal] : (itemOptVal as string[]);
propResultsMap[k]!.passed = criteriaPassWithIncludeBehavior(expectedValues.some(x => x.trim().toLowerCase() === propertyValue?.trim().toLowerCase()), include);
// remove # if comparing hex values
const isHex = k.toLowerCase().includes('background');
const expectedValues = (typeof itemOptVal === 'string' ? [itemOptVal] : (itemOptVal as string[])).map(x => isHex ? x.replace('#','').trim() : x.trim());
const cleanProp = isHex ? propertyValue.replace('#','').trim() : propertyValue.trim();
let anyPassed = false;
const errorReasons = [];
for(const expectedVal of expectedValues) {
try {
const [regPassed] = testMaybeStringRegex(expectedVal,cleanProp);
if(regPassed) {
anyPassed = true;
}
} catch (err: any) {
if(err.message.includes('Could not convert test value')) {
errorReasons.push(`Could not convert ${expectedVal} to Regex, fallback to simple case-insenstive comparison`);
// fallback to simple comparison
anyPassed = expectedVal.toLowerCase() === cleanProp.toLowerCase();
} else {
errorReasons.push(err.message);
}
}
if(anyPassed) {
break;
}
}
if(errorReasons.length > 0) {
propResultsMap[k]!.reason = `Some errors occurred while testing: ${errorReasons.join(' | ')}`;
}
propResultsMap[k]!.passed = criteriaPassWithIncludeBehavior(anyPassed, include);
}
break;
} else {
propResultsMap[k]!.passed = true;
propResultsMap[k]!.reason = `Cannot test for ${k} on Comment`;
log.warn(`Cannot test for ${k} on Comment`);
break;
}
default:
@@ -3042,10 +3235,11 @@ export class SubredditResources {
}
break;
case 'userNotes':
const unCriterias = (authorOpts[k] as UserNoteCriteria[]).map(x => toFullUserNoteCriteria(x));
const notes = await this.userNotes.getUserNotes(item.author);
let foundNoteResult: string[] = [];
const notePass = () => {
for (const noteCriteria of authorOpts[k] as UserNoteCriteria[]) {
for (const noteCriteria of unCriterias) {
const {count = '>= 1', search = 'current', type} = noteCriteria;
const {
value,
@@ -3054,26 +3248,14 @@ export class SubredditResources {
duration,
extra = ''
} = parseGenericValueOrPercentComparison(count);
const cutoffDate = duration === undefined ? undefined : dayjs().subtract(duration);
const order = extra.includes('asc') ? 'ascending' : 'descending';
switch (search) {
case 'current':
if (notes.length > 0) {
const currentNoteType = notes[notes.length - 1].noteType;
foundNoteResult.push(`Current => ${currentNoteType}`);
if (currentNoteType === type) {
return true;
}
} else {
foundNoteResult.push('No notes present');
}
break;
case 'consecutive':
if (isPercent) {
throw new SimpleError(`When comparing UserNotes with 'consecutive' search 'count' cannot be a percentage. Given: ${count}`);
}
let orderedNotes = cutoffDate === undefined ? notes : notes.filter(x => x.time.isSameOrAfter(cutoffDate));
let orderedNotes = [...notes];
if (order === 'descending') {
orderedNotes = [...notes];
orderedNotes.reverse();
@@ -3081,7 +3263,7 @@ export class SubredditResources {
let currCount = 0;
let maxCount = 0;
for (const note of orderedNotes) {
if (note.noteType === type) {
if(note.matches(noteCriteria, item)) {
currCount++;
maxCount = Math.max(maxCount, currCount);
} else {
@@ -3093,8 +3275,10 @@ export class SubredditResources {
return true;
}
break;
case 'current':
case 'total':
const filteredNotes = notes.filter(x => x.noteType === type && cutoffDate === undefined || (x.time.isSameOrAfter(cutoffDate)));
const notesToUse = search === 'current' ? [notes[notes.length - 1]] : notes;
const filteredNotes = notesToUse.filter(x => x.matches(noteCriteria, item));
if (isPercent) {
// avoid divide by zero
const percent = notes.length === 0 ? 0 : filteredNotes.length / notes.length;
@@ -3104,7 +3288,7 @@ export class SubredditResources {
}
} else {
foundNoteResult.push(`${filteredNotes.length} are ${type}`);
if (comparisonTextOp(notes.filter(x => x.noteType === type).length, operator, value)) {
if (comparisonTextOp(filteredNotes.length, operator, value)) {
return true;
}
}
@@ -3132,7 +3316,6 @@ export class SubredditResources {
const {search = 'current', count = '>= 1'} = actionCriteria;
const {
value,
operator,
@@ -3140,146 +3323,10 @@ export class SubredditResources {
duration,
extra = ''
} = parseGenericValueOrPercentComparison(count);
const cutoffDate = duration === undefined ? undefined : dayjs().subtract(duration);
let actionsToUse: ModNote[] = [];
if(asModNoteCriteria(actionCriteria)) {
actionsToUse = actionsToUse.filter(x => x.type === 'NOTE');
} else {
actionsToUse = modActions;
}
if(search === 'current' && actionsToUse.length > 0) {
actionsToUse = [actionsToUse[0]];
}
let validActions: ModNote[] = [];
if (asModLogCriteria(actionCriteria)) {
const fullCrit = toFullModLogCriteria(actionCriteria);
const fullCritEntries = Object.entries(fullCrit);
validActions = actionsToUse.filter(x => {
// filter out any notes that occur before time range
if(cutoffDate !== undefined && x.createdAt.isBefore(cutoffDate)) {
return false;
}
for (const [k, v] of fullCritEntries) {
const key = k.toLocaleLowerCase();
if (['count', 'search'].includes(key)) {
continue;
}
switch (key) {
case 'type':
if (!v.includes((x.type as ModActionType))) {
return false
}
break;
case 'activitytype':
const anyMatch = v.some((a: ActivityType) => {
switch (a) {
case 'submission':
if (x.action.actedOn instanceof Submission) {
return true;
}
break;
case 'comment':
if (x.action.actedOn instanceof Comment) {
return true;
}
break;
}
});
if (!anyMatch) {
return false;
}
break;
case 'description':
case 'action':
case 'details':
const actionPropVal = x.action[key] as string;
if (actionPropVal === undefined) {
return false;
}
const anyPropMatch = v.some((y: RegExp) => y.test(actionPropVal));
if (!anyPropMatch) {
return false;
}
} // case end
} // for each end
return true;
}); // filter end
} else if(asModNoteCriteria(actionCriteria)) {
const fullCrit = toFullModNoteCriteria(actionCriteria as ModNoteCriteria);
const fullCritEntries = Object.entries(fullCrit);
validActions = actionsToUse.filter(x => {
// filter out any notes that occur before time range
if(cutoffDate !== undefined && x.createdAt.isBefore(cutoffDate)) {
return false;
}
for (const [k, v] of fullCritEntries) {
const key = k.toLocaleLowerCase();
if (['count', 'search'].includes(key)) {
continue;
}
switch (key) {
case 'notetype':
if (!v.map((x: ModUserNoteLabel) => x.toUpperCase()).includes((x.note.label as ModUserNoteLabel))) {
return false
}
break;
case 'note':
const actionPropVal = x.note.note;
if (actionPropVal === undefined) {
return false;
}
const anyPropMatch = v.some((y: RegExp) => y.test(actionPropVal));
if (!anyPropMatch) {
return false;
}
break;
case 'activitytype':
const anyMatch = v.some((a: ActivityType) => {
switch (a) {
case 'submission':
if (x.action.actedOn instanceof Submission) {
return true;
}
break;
case 'comment':
if (x.action.actedOn instanceof Comment) {
return true;
}
break;
}
});
if (!anyMatch) {
return false;
}
break;
} // case end
} // for each end
return true;
}); // filter end
} else {
throw new SimpleError(`Could not determine if a modActions criteria was for Mod Log or Mod Note. Given: ${JSON.stringify(actionCriteria)}`);
}
const [validActions, actionsToUse] = this.filterAuthorModActions(modActions, actionCriteria, item);
switch (search) {
case 'current':
if (validActions.length === 0) {
actionResult.push('No Mod Actions present');
} else {
actionResult.push('Current Action matches criteria');
return true;
}
break;
case 'consecutive':
if (isPercent) {
throw new SimpleError(`When comparing Mod Actions with 'search: consecutive' the 'count' value cannot be a percentage. Given: ${count}`);
@@ -3306,10 +3353,11 @@ export class SubredditResources {
return true;
}
break;
case 'current':
case 'total':
if (isPercent) {
// avoid divide by zero
const percent = notes.length === 0 ? 0 : validActions.length / actionsToUse.length;
const percent = actionsToUse.length === 0 ? 0 : validActions.length / actionsToUse.length;
actionResult.push(`${formatNumber(percent)}% of ${actionsToUse.length} matched criteria`);
if (comparisonTextOp(percent, operator, value / 100)) {
return true;

View File

@@ -16,6 +16,9 @@ import {Cache} from 'cache-manager';
import {isScopeError} from "../Utils/Errors";
import {ErrorWithCause} from "pony-cause";
import {UserNoteType} from "../Common/Infrastructure/Atomic";
import {FullUserNoteCriteria, UserNoteCriteria} from "../Common/Infrastructure/Filters/FilterCriteria";
import {parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
interface RawUserNotesPayload {
ver: number,
@@ -251,6 +254,44 @@ export class UserNote {
}
public matches(criteria: FullUserNoteCriteria, item?: SnoowrapActivity) {
if (criteria.type !== undefined) {
if(typeof this.noteType === 'string') {
if(this.noteType.toLowerCase() !== criteria.type.toLowerCase().trim()) {
return false
}
} else {
return false;
}
}
if (criteria.note !== undefined && !criteria.note.some(x => x.test(this.text ?? ''))) {
return false;
}
if(criteria.referencesCurrentActivity !== undefined) {
if(criteria.referencesCurrentActivity) {
if(item === undefined) {
return false;
}
if(this.link === null) {
return false;
}
if(!this.link.includes(item.id)) {
return false;
}
} else if(this.link !== null && item !== undefined && this.link.includes(item.id)) {
return false;
}
}
const {duration} = parseGenericValueOrPercentComparison(criteria.count ?? '>= 1');
if (duration !== undefined) {
const cutoffDate = dayjs().subtract(duration);
if (this.time.isSameOrAfter(cutoffDate)) {
return false;
}
}
return true;
}
public toRaw(constants: UserNotesConstants): RawNote {
let m = this.modIndex;
if(m === undefined && this.moderator !== undefined) {

View File

@@ -73,8 +73,10 @@ const logs = () => {
const requestedBots = bots.map(x => x.botName);
const origin = req.header('X-Forwarded-For') ?? req.header('host');
const stream = logger.stream();
try {
logger.stream().on('log', (log: LogInfo) => {
stream.on('log', (log: LogInfo) => {
if (isLogLineMinLevel(log, level as string)) {
const {subreddit: subName, bot, user} = log;
let canAccess = false;
@@ -105,13 +107,13 @@ const logs = () => {
logger.info(`${userName} from ${origin} => CONNECTED`);
await pEvent(req, 'close');
//logger.debug('Request closed detected with "close" listener');
res.destroy();
return;
} catch (e: any) {
if (e.code !== 'ECONNRESET') {
logger.error(e);
}
} finally {
stream.removeAllListeners();
logger.info(`${userName} from ${origin} => DISCONNECTED`);
res.destroy();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -169,6 +169,14 @@ a {
display: inherit;
}
.show {
display: inherit;
}
.invisible {
display: none;
}
.triggeredStateToggle {
cursor: pointer;
}
@@ -183,7 +191,7 @@ li > ul {
}
.smallLi:before {
margin-left: -10px;
margin-left: -5px;
content: ""
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -13,4 +13,9 @@
<meta name="robots" content="noindex">
<!--icons from https://heroicons.com -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/6.0.0/introjs.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="apple-touch-icon" sizes="180x180" href="/public/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/public/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/public/favicon-16x16.png">
<link rel="manifest" href="/public/site.webmanifest">
<link rel="icon" type="image/x-icon" href="/public/favicon.ico">
</head>

View File

@@ -1,4 +1,4 @@
<svg class="loading" version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg"
<svg class="loading invisible" version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" xml:space="preserve">
<path

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -689,7 +689,13 @@
</div>
<%- include('partials/logSettings') %>
</div>
<div class="liveLogIndicator">
<%- include('partials/loadingIcon') %>
<span class="liveLogErrorWrapper invisible">
<span class="iconify-inline red" style="display: inline;" data-icon="ci:error-outline"></span>
<span class="liveLogError"></span><a class="restartLogs ml-3" href="#">Restart Live Logs</a>
</span>
</div>
<div data-subreddit="<%= data.name %>" class="logs font-mono text-sm">
<% data.logs.forEach(function (logEntry){ %>
<%- logEntry %>
@@ -751,6 +757,17 @@
});
})
document.querySelectorAll('.restartLogs').forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
const subSection = e.target.closest('div.sub');
if (subSection !== null) {
getStreamingLogs(subSection.dataset.subreddit, subSection.dataset.bot);
}
});
});
document.querySelectorAll(".checkUrl").forEach(el => {
const toggleButtons = (e) => {
const subFilter = `.sub[data-subreddit="${e.target.dataset.subreddit}"]`;
@@ -962,7 +979,7 @@
}).observe(element);
}
function getStreamingLogs(sub, bot) {
function getStreamingLogs(sub, bot, restarts = 0) {
console.debug(`Getting stream for ${bot} ${sub}`);
@@ -1018,38 +1035,49 @@
bufferedLogs = [];
}
setLiveLogIndicator(bot, sub, true);
const fetchPromise = fetch(`/api/logs?instance=<%= instanceId %>&bot=${bot}&subreddit=${sub}&level=${level}&sort=${sort}&limit=${limitSel}&stream=true&streamObjects=true&formatted=false`, {signal})
.then(response => response.body)
.then(rs =>
rs.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
textBuffer += chunk;
const lines = textBuffer.split('\n');
for (const line of lines.slice(0, -1)) {
controller.enqueue(line);
.then(response => {
return response.body;
})
.then(rs => {
return rs.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
textBuffer += chunk;
const lines = textBuffer.split('\n');
for (const line of lines.slice(0, -1)) {
controller.enqueue(line);
}
textBuffer = lines.slice(-1)[0];
},
flush(controller) {
if (textBuffer) {
controller.enqueue(textBuffer);
}
}
textBuffer = lines.slice(-1)[0];
},
flush(controller) {
if (textBuffer) {
controller.enqueue(textBuffer);
}
}
}))
}))
// Parse JSON objects
.pipeThrough(new TransformStream({
transform(line, controller) {
if (line) {
controller.enqueue(
JSON.parse(line)
);
// Parse JSON objects
.pipeThrough(new TransformStream({
transform(line, controller) {
if (line) {
controller.enqueue(
JSON.parse(line)
);
}
}
}
}))
).catch((e) => {
console.warn(e);
}));
}
)
.catch((e) => {
if(e.name === 'AbortError') {
setLiveLogIndicator(bot, sub, false);
console.debug(`Log streaming for ${bot} ${sub} aborted`);
} else {
setLiveLogIndicator(bot, sub, false, `Live Log encountered an error: ${e.message}`);
console.warn(e);
}
});
fetchPromise.then(async res => {
@@ -1064,6 +1092,11 @@
if(done) {
keepReading = false;
console.debug(`${bot}.${sub} log stream reader signalled it is done`);
if(restarts < 3) {
getStreamingLogs(sub, bot, restarts + 1);
} else {
setLiveLogIndicator(bot, sub, false, `Tried to automatically restart stream too many times (${restarts +1}) which indicates something may be wrong with communication.`);
}
}
if(value) {
//console.log(`((Logged For ${bot} ${sub})) ${value.message}`);
@@ -1098,9 +1131,11 @@
}).catch((e) => {
if(e.name !== 'AbortError') {
console.debug(`Non-abort error occurred while streaming logs for ${bot} ${sub}`);
console.error(e);
console.warn(e);
setLiveLogIndicator(bot, sub, false, `Live Log encountered an error: ${e.message}`);
} else {
console.debug(`Log streaming for ${bot} ${sub} aborted`);
setLiveLogIndicator(bot, sub, false);
}
});
@@ -1108,6 +1143,36 @@
recentlySeen.set(`${bot}.${sub}`, {...existing, fetch: fetchPromise, controller, streamStart: Date.now()});
}
function setLiveLogIndicator(bot, sub, live, error = undefined) {
const liveIndicator = document.querySelector(`[data-bot="${bot}"][data-subreddit="${sub}"] .liveLogIndicator .loading`);
if(null !== liveIndicator) {
if(live) {
if(liveIndicator.classList.contains('invisible')) {
liveIndicator.classList.remove('invisible');
}
// if(!liveIndicator.classList.contains('show')) {
// liveIndicator.classList.add('show');
// }
} else {
if(!liveIndicator.classList.contains('invisible')) {
liveIndicator.classList.add('invisible');
}
}
}
const liveErrorWrapper = document.querySelector(`[data-bot="${bot}"][data-subreddit="${sub}"] .liveLogIndicator .liveLogErrorWrapper`);
if(null !== liveErrorWrapper) {
if(live && !liveErrorWrapper.classList.contains('invisible')) {
liveErrorWrapper.classList.add('invisible');
}
if(!live && error !== undefined) {
if(liveErrorWrapper.classList.contains('invisible')) {
liveErrorWrapper.classList.remove('invisible');
}
document.querySelector(`[data-bot="${bot}"][data-subreddit="${sub}"] .liveLogIndicator .liveLogError`).innerHTML = error;
}
}
}
const delayedItemsMap = new Map();
let lastSeenIdentifier = null;
const subIndicators = ['red', 'green', 'yellow'];

View File

@@ -2489,20 +2489,24 @@ export const mergeFilters = (objectConfig: RunnableBaseJson, filterDefs: FilterC
let derivedAuthorIs: AuthorOptions = buildFilter(authorIsDefault);
if (authorIsBehavior === 'merge') {
derivedAuthorIs = merge.all([authorIs, authorIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
} else if (Object.keys(authorIs).length > 0) {
} else if (!filterIsEmpty(authorIs)) {
derivedAuthorIs = authorIs;
}
let derivedItemIs: ItemOptions = buildFilter(itemIsDefault);
if (itemIsBehavior === 'merge') {
derivedItemIs = merge.all([itemIs, itemIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
} else if (Object.keys(itemIs).length > 0) {
} else if (!filterIsEmpty(itemIs)) {
derivedItemIs = itemIs;
}
return [derivedAuthorIs, derivedItemIs];
}
export const filterIsEmpty = (obj: FilterOptions<any>): boolean => {
return (obj.include === undefined || obj.include.length === 0) && (obj.exclude === undefined || obj.exclude.length === 0);
}
export const buildFilter = (filterVal: MinimalOrFullMaybeAnonymousFilter<AuthorCriteria | TypedActivityState | ActivityState>): FilterOptions<AuthorCriteria | TypedActivityState | ActivityState> => {
if(Array.isArray(filterVal)) {
const named = filterVal.map(x => normalizeCriteria(x));
@@ -2606,28 +2610,7 @@ export const normalizeCriteria = <T extends AuthorCriteria | TypedActivityState
criteria.description = Array.isArray(criteria.description) ? criteria.description : [criteria.description];
}
if(criteria.modActions !== undefined) {
criteria.modActions.map((x, index) => {
const common = {
...x,
type: x.type === undefined ? undefined : (Array.isArray(x.type) ? x.type : [x.type])
}
if(asModNoteCriteria(x)) {
return {
...common,
noteType: x.noteType === undefined ? undefined : (Array.isArray(x.noteType) ? x.noteType : [x.noteType]),
note: x.note === undefined ? undefined : (Array.isArray(x.note) ? x.note : [x.note]),
}
} else if(asModLogCriteria(x)) {
return {
...common,
action: x.action === undefined ? undefined : (Array.isArray(x.action) ? x.action : [x.action]),
details: x.details === undefined ? undefined : (Array.isArray(x.details) ? x.details : [x.details]),
description: x.description === undefined ? undefined : (Array.isArray(x.description) ? x.description : [x.description]),
activityType: x.activityType === undefined ? undefined : (Array.isArray(x.activityType) ? x.activityType : [x.activityType]),
}
}
return common;
})
criteria.modActions.map((x, index) => normalizeModActionCriteria(x));
}
}
@@ -2637,6 +2620,29 @@ export const normalizeCriteria = <T extends AuthorCriteria | TypedActivityState
};
}
export const normalizeModActionCriteria = (x: (ModNoteCriteria | ModLogCriteria)): (ModNoteCriteria | ModLogCriteria) => {
const common = {
...x,
type: x.type === undefined ? undefined : (Array.isArray(x.type) ? x.type : [x.type])
}
if(asModNoteCriteria(x)) {
return {
...common,
noteType: x.noteType === undefined ? undefined : (Array.isArray(x.noteType) ? x.noteType : [x.noteType]),
note: x.note === undefined ? undefined : (Array.isArray(x.note) ? x.note : [x.note]),
}
} else if(asModLogCriteria(x)) {
return {
...common,
action: x.action === undefined ? undefined : (Array.isArray(x.action) ? x.action : [x.action]),
details: x.details === undefined ? undefined : (Array.isArray(x.details) ? x.details : [x.details]),
description: x.description === undefined ? undefined : (Array.isArray(x.description) ? x.description : [x.description]),
activityType: x.activityType === undefined ? undefined : (Array.isArray(x.activityType) ? x.activityType : [x.activityType]),
}
}
return common;
}
export const asNamedCriteria = <T>(val: MaybeAnonymousCriteria<T> | undefined): val is NamedCriteria<T> => {
if(val === undefined || typeof val === 'string') {
return false;

View File

@@ -0,0 +1,193 @@
import {describe, it} from 'mocha';
import {assert} from 'chai';
import dayjs from "dayjs";
import dduration, {Duration, DurationUnitType} from 'dayjs/plugin/duration.js';
import utc from 'dayjs/plugin/utc.js';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import tz from 'dayjs/plugin/timezone';
import relTime from 'dayjs/plugin/relativeTime.js';
import sameafter from 'dayjs/plugin/isSameOrAfter.js';
import samebefore from 'dayjs/plugin/isSameOrBefore.js';
import weekOfYear from 'dayjs/plugin/weekOfYear.js';
import {SubredditResources} from "../src/Subreddit/SubredditResources";
import {NoopLogger} from '../src/Utils/loggerFactory';
import {Subreddit, Comment, Submission, RedditUser} from 'snoowrap/dist/objects';
import Snoowrap from "snoowrap";
import {getResource, getSnoowrap, getSubreddit, sampleActivity} from "./testFactory";
import {Subreddit as SubredditEntity} from "../src/Common/Entities/Subreddit";
import {Activity} from '../src/Common/Entities/Activity';
import {cmToSnoowrapActivityMap} from "../src/Common/Infrastructure/Filters/FilterCriteria";
import {SnoowrapActivity} from "../src/Common/Infrastructure/Reddit";
dayjs.extend(dduration);
dayjs.extend(utc);
dayjs.extend(relTime);
dayjs.extend(sameafter);
dayjs.extend(samebefore);
dayjs.extend(tz);
dayjs.extend(advancedFormat);
dayjs.extend(weekOfYear);
describe('Author Criteria', function () {
let resource: SubredditResources;
let snoowrap: Snoowrap;
let subreddit: Subreddit;
let subredditEntity: SubredditEntity;
before(async () => {
resource = await getResource();
snoowrap = await getSnoowrap();
subreddit = await getSubreddit();
subredditEntity = await resource.database.getRepository(SubredditEntity).save(new SubredditEntity({
id: subreddit.id,
name: subreddit.name
}));
});
const testAuthor = (userProps: any = {}, activityType: string = 'submission', activityProps: any = {}) => {
const author = new RedditUser({
name: 'aTestUser',
is_suspended: false,
...userProps,
}, snoowrap, true);
let activity: SnoowrapActivity;
if (activityType === 'submission') {
activity = new Submission({
created: 1664220502,
...activityProps,
}, snoowrap, false);
} else {
activity = new Comment({
created: 1664220502,
...activityProps,
}, snoowrap, false);
}
// @ts-ignore
author._fetch = author;
activity.author = author;
return activity;
};
describe('Moderator accessible criteria', function () {
// TODO isContributor
});
describe('Publicly accessible criteria', function () {
it('Should match name literal', async function () {
assert.isTrue((await resource.isAuthor(testAuthor(), {name: ['foo','test']}, true)).passed);
});
it('Should match name regex', async function () {
assert.isTrue((await resource.isAuthor(testAuthor(), {name: ['/fo.*/i','/te.*/i']}, true)).passed);
});
for(const prop of ['flairCssClass', 'flairTemplate', 'flairText']) {
let activityPropName = cmToSnoowrapActivityMap[prop] ?? prop;
if(activityPropName === 'link_flair_template_id') {
activityPropName = 'author_flair_template_id';
}
it(`Should detect specific ${prop} as single string`, async function () {
assert.isTrue((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: 'test',
}), {[prop]: 'test'}, true)).passed);
});
it(`Should detect specific ${prop} from array of string`, async function () {
assert.isTrue((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: 'test',
}), {[prop]: ['foo','test']}, true)).passed);
});
it(`Should detect specific ${prop} is not in criteria`, async function () {
assert.isFalse((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: 'test',
}), {[prop]: ['foo']}, true)).passed);
});
it(`Should detect any ${prop}`, async function () {
assert.isTrue((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: 'test',
}), {[prop]: true}, true)).passed);
});
it(`Should detect no ${prop}`, async function () {
assert.isTrue((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: null,
}), {[prop]: false}, true)).passed);
assert.isTrue((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: '',
}), {[prop]: false}, true)).passed);
assert.isFalse((await resource.isAuthor(testAuthor({}, 'submission',{
[activityPropName]: '',
}), {[prop]: 'foo'}, true)).passed);
});
/*it(`Should detect ${prop} as Regular Expression`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: 'test'
}, snoowrap, false), {[prop]: '/te.*!/'}, NoopLogger, true)).passed);
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: 'test'
}, snoowrap, false), {[prop]: ['foo', '/t.*!/']}, NoopLogger, true)).passed);
});*/
}
// TODO isMod
// TODO shadowbanned
it('Should detect age', async function () {
const time = dayjs().subtract(5, 'minutes').unix();
const agedAuthor = testAuthor({created: time})
assert.isTrue((await resource.isAuthor(agedAuthor, {age: '> 4 minutes'}, true)).passed);
assert.isTrue((await resource.isAuthor(agedAuthor, {age: '< 10 minutes'}, true)).passed);
});
it('Should match link karma', async function () {
const author = testAuthor({link_karma: 10})
assert.isTrue((await resource.isAuthor(author, {linkKarma: '> 4'}, true)).passed);
assert.isTrue((await resource.isAuthor(author, {linkKarma: '< 11'}, true)).passed);
});
it('Should match comment karma', async function () {
const author = testAuthor({comment_karma: 10})
assert.isTrue((await resource.isAuthor(author, {commentKarma: '> 4'}, true)).passed);
assert.isTrue((await resource.isAuthor(author, {commentKarma: '< 11'}, true)).passed);
});
it('Should match total karma', async function () {
const author = testAuthor({total_karma: 10})
assert.isTrue((await resource.isAuthor(author, {totalKarma: '> 4'}, true)).passed);
assert.isTrue((await resource.isAuthor(author, {totalKarma: '< 11'}, true)).passed);
});
it('Should check verfied email status', async function () {
const author = testAuthor({has_verified_mail: true})
assert.isTrue((await resource.isAuthor(author, {verified: true}, true)).passed);
});
it('Should match profile description literal', async function () {
const author = testAuthor({subreddit: new Subreddit({
display_name: {
public_description: 'this is a test'
}
}, snoowrap, true)});
assert.isTrue((await resource.isAuthor(author, {description: 'this is a test'}, true)).passed);
});
it('Should match profile description regex', async function () {
const author = testAuthor({subreddit: new Subreddit({
display_name: {
public_description: 'this is a test'
}
}, snoowrap, true)});
assert.isTrue((await resource.isAuthor(author, {description: '/te.*/i'}, true)).passed);
});
// TODO usernotes
// TODO modactions
});
});

View File

@@ -16,6 +16,7 @@ import Snoowrap from "snoowrap";
import {getResource, getSnoowrap, getSubreddit, sampleActivity} from "./testFactory";
import {Subreddit as SubredditEntity} from "../src/Common/Entities/Subreddit";
import {Activity} from '../src/Common/Entities/Activity';
import {cmToSnoowrapActivityMap} from "../src/Common/Infrastructure/Filters/FilterCriteria";
dayjs.extend(dduration);
dayjs.extend(utc);
@@ -229,49 +230,98 @@ describe('Item Criteria', function () {
}, snoowrap, false), {upvoteRatio: '> 33'}, NoopLogger, true)).passed);
});
it('Should detect specific link flair template', async function () {
assert.isTrue((await resource.isItem(new Submission({
link_flair_template_id: 'test',
}, snoowrap, false), {flairTemplate: 'test'}, NoopLogger, true)).passed);
assert.isTrue((await resource.isItem(new Submission({
link_flair_template_id: 'test',
}, snoowrap, false), {flairTemplate: ['foo','test']}, NoopLogger, true)).passed);
assert.isFalse((await resource.isItem(new Submission({
link_flair_template_id: 'test',
}, snoowrap, false), {flairTemplate: ['foo']}, NoopLogger, true)).passed);
});
it('Should detect any link flair template', async function () {
assert.isTrue((await resource.isItem(new Submission({
link_flair_template_id: 'test',
}, snoowrap, false), {flairTemplate: true}, NoopLogger, true)).passed);
});
it('Should detect no link flair template', async function () {
assert.isTrue((await resource.isItem(new Submission({
link_flair_template_id: null
}, snoowrap, false), {flairTemplate: false}, NoopLogger, true)).passed);
});
for(const prop of ['link_flair_text', 'link_flair_css_class', 'authorFlairCssClass', 'authorFlairTemplateId', 'authorFlairText', 'flairTemplate']) {
const activityPropName = cmToSnoowrapActivityMap[prop] ?? prop;
for(const prop of ['link_flair_text', 'link_flair_css_class']) {
it(`Should detect specific ${prop}`, async function () {
it(`Should detect specific ${prop} as single string`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[prop]: 'test',
[activityPropName]: 'test',
}, snoowrap, false), {[prop]: 'test'}, NoopLogger, true)).passed);
});
it(`Should detect specific ${prop} from array of string`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[prop]: 'test',
[activityPropName]: 'test',
}, snoowrap, false), {[prop]: ['foo','test']}, NoopLogger, true)).passed);
});
it(`Should detect specific ${prop} is not in criteria`, async function () {
assert.isFalse((await resource.isItem(new Submission({
[prop]: 'test',
[activityPropName]: 'test',
}, snoowrap, false), {[prop]: ['foo']}, NoopLogger, true)).passed);
});
it(`Should detect any ${prop}`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[prop]: 'test',
[activityPropName]: 'test',
}, snoowrap, false), {[prop]: true}, NoopLogger, true)).passed);
});
it(`Should detect no ${prop}`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[prop]: null
[activityPropName]: null
}, snoowrap, false), {[prop]: false}, NoopLogger, true)).passed);
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: ''
}, snoowrap, false), {[prop]: false}, NoopLogger, true)).passed);
assert.isFalse((await resource.isItem(new Submission({
[activityPropName]: ''
}, snoowrap, false), {[prop]: 'foo'}, NoopLogger, true)).passed);
});
it(`Should detect ${prop} as Regular Expression`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: 'test'
}, snoowrap, false), {[prop]: '/te.*/'}, NoopLogger, true)).passed);
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: 'test'
}, snoowrap, false), {[prop]: ['foo', '/t.*/']}, NoopLogger, true)).passed);
});
}
for(const prop of ['authorFlairBackgroundColor', 'link_flair_background_color']) {
const activityPropName = cmToSnoowrapActivityMap[prop] ?? prop;
it(`Should detect specific ${prop} as single string`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080',
}, snoowrap, false), {[prop]: '#400080'}, NoopLogger, true)).passed);
});
it(`Should detect specific ${prop} from array of string`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080',
}, snoowrap, false), {[prop]: ['#903480','#400080']}, NoopLogger, true)).passed);
});
it(`Should detect specific ${prop} is not in criteria`, async function () {
assert.isFalse((await resource.isItem(new Submission({
[activityPropName]: '#400080',
}, snoowrap, false), {[prop]: ['#903480']}, NoopLogger, true)).passed);
});
it(`Should detect any ${prop}`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080',
}, snoowrap, false), {[prop]: true}, NoopLogger, true)).passed);
});
it(`Should detect no ${prop}`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: null
}, snoowrap, false), {[prop]: false}, NoopLogger, true)).passed);
});
it(`Should detect ${prop} and remove # prefix`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080'
}, snoowrap, false), {[prop]: '400080'}, NoopLogger, true)).passed);
});
it(`Should detect ${prop} as Regular Expression`, async function () {
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080'
}, snoowrap, false), {[prop]: '/#400.*/'}, NoopLogger, true)).passed);
assert.isTrue((await resource.isItem(new Submission({
[activityPropName]: '#400080'
}, snoowrap, false), {[prop]: ['#903480', '/400.*/']}, NoopLogger, true)).passed);
});
}
for(const prop of ['link_flair_text', 'link_flair_css_class', 'flairTemplate', 'link_flair_background_color']) {
it(`Should PASS submission criteria '${prop}' with a reason when Activity is a Comment`, async function () {
const result = await resource.isItem(new Comment({}, snoowrap, false), {[prop]: true}, NoopLogger, true);
assert.isTrue(result.passed);
assert.equal(result.propertyResults[0].reason, `Cannot test for ${prop} on Comment`)
});
}

View File

@@ -79,6 +79,12 @@ export const getBot = async () => {
bot = new Bot(config.bots[0], NoopLogger);
await bot.cacheManager.set('test', {
logger: NoopLogger,
caching: {
authorTTL: false,
submissionTTL: false,
commentTTL: false,
provider: 'memory'
},
subreddit: bot.client.getSubreddit('test'),
client: bot.client,
statFrequency: 'minute',